Unit and Integration Testing of Spring Boot Application

krnlpn1c
5 min readApr 1, 2024

--

Intro

Testing is an essential part of any software. It ensures reliability, functionality, accelerates time-to-market, and provides a sense of security and confidence in the future.

In other words, testing is your older brother, who helps you to not mess up :)

Personally, I use two kinds of tests in my apps: Unit and Integration. So, let’s dive in, folks.

Unit tests

Unit tests allow you to test individual components (or units!), to verify that important parts of your app have an expected behavior. This approach, like any other, has its pros and cons.

Pros:

  1. Easy to use on early stage of the development cycle.
    They take a little bit of code and time, so they are great to verify that your fresh code works fine. Furthermore, it’s a good idea to write a unit test even before an actual method/class implementation, see Test-Driven Development (TDD).
  2. Edge cases coverage.
    It’s easier to cover each case possible within a small test, so unit testing comes in handy for various utils classes.
  3. Helps you write more modular, reusable and maintainable code.
    Since you have to organize your code in such a way that it can be covered by unit tests, code quality, such as reusability and maintainability, increases.

Cons:

  1. Time and cost of a high test-coverage.
    Since a unit test covers one unit, you’ll have to write a ton of tests to cover a significant part of your application.
  2. Illusion of reliability.
    Even if you cover different parts of your application, something could go sideways in the middle. Especially in a context of Spring Boot application — a bug could appear from an interaction of your tested code with the Spring framework, persistence layer or even third-party libraries.
  3. Isolation challenges.
    Sometimes it’s pretty hard to properly isolate the unit from its dependencies, which requires the huge number of mocks and stubs (and efforts!).

The most popular testing framework for Spring application (and JVM-based apps in general) is JUnit. So here is an example of a simple unit test in JUnit 5:

class RatingCalculatorTest {
@Test
fun `calculate average of two ratings`() {
val avg = calculateAvgRating(
listOf(
BookRating(bookId = 1, rating = BigDecimal("3")),
BookRating(bookId = 2, rating = BigDecimal("4")))
)
assertEquals(BigDecimal("3.50"), avg)
}
}

Unit tests are great for testing important parts of your application, for covering edge cases and TDD approach, but they are not enough for your application to live a happy, bug-free life. Although their cons won’t bother you, if you’ll have integration tests in your arsenal.

Integration Tests

Integration tests step beyond unit testing by combining multiple components of your applications and checking how well they work together, simulating real-world usage more closely than unit tests. Like unit testing, integration testing comes with its own set of advantages and challenges.

Pros:

  1. Detects system-level issues.
    Integration tests are invaluable for identifying problems that occur when different parts of the application interact, such as integration with databases, external services, or third-party libraries.
  2. Simulates user experience.
    By testing multiple components together, integration tests can simulate real user scenarios and workflows, ensuring the application behaves as expected.
  3. «Top-to-bottom» test coverage.
    Since integration tests execute code through each level of an application, a single test is able to cover a large codebase.
  4. Improved confidence in the build.
    Passing integration tests provides a higher level of confidence that the application will perform correctly in production, as they cover more of the application’s parts comparing to unit tests.

Cons:

  1. Development Complexity.
    Integration tests can be more complex to set up and maintain than unit tests.
  2. Longer Execution Time.
    Because they cover more ground and usually involve waiting for the full application context to be set up, integration tests usually take longer to execute than unit tests, which can slow down both the development process and CI/CD pipeline.
  3. Not great for edge cases.
    Because of longer execution time, each edge case significantly increases total tests execution time. So, for parts of your application, that have a lot of edge cases, unit tests would be much better option in terms of speed.
  4. The REAL Isolation challenges.
    Maybe in unit tests you can avoid this kind of challenges, but in integration tests it’s something you have to deal with. Every external dependency should be «taken care of» — mocked or stubbed. There are plenty libraries for these purposes, such as Mockito, Wiremock and Testcontainers for a database layer.

In Spring Boot integration tests works well with JUnit too with a little help of build-in Spring Boot Test package. Here is an example.

@SpringBootTest
@AutoConfigureMockMvc
class BookIntegrationTest {
@Autowired
lateinit var mockMvc: MockMvc

@Test
fun `books list returns success`() {
mockMvc.get("/api/v2/books")
.andDo { MockMvcResultHandlers.print() }
.andExpect {
status { isOk() }
}
}
}

Integration tests fill a critical role in the software development lifecycle by ensuring that different parts of the application work together as expected. Despite their development complexity and the potential for increased execution time, they provide a crucial layer of assurance that significantly contributes to the overall quality and reliability of your applications.

Bonus. Part 1. Parameterized Test Feature

There is a cool JUnit feature called Parameterized Test. It comes in handy when you need to run your test multiple times with different parameters. It works both with Unit tests and Integration tests, although I’d recommend to use it primary with Unit tests, since a Parameterized Integration test may give a significant increase of overall tests execution time.

Here is an example of Parameterized Unit Test:

class RatingCalculatorTest {
@ParameterizedTest
@MethodSource("ratingCalculatorParameters")
fun `calculate average rating`(ratings: List<String>, expected: String?) {
val bookRatings = ratings.mapIndexed { index, ratingString ->
BookRating(index, BigDecimal(ratingString))
}
val avg = calculateAvgRating(bookRatings)
assertEquals(expected?.let { BigDecimal(it) }, avg)
}

companion object {
@JvmStatic
fun ratingCalculatorParameters(): Stream<Arguments> =
Stream.of(
Arguments.of(listOf("3", "4"), "3.50"),
Arguments.of(listOf("4"), "4.00"),
Arguments.of(listOf("3", "4", "3"), "3.33"),
Arguments.of(emptyList<String>(), null),
)
}
}

Bonus. Part 2. Other Test Types

Application testing isn’t limited by Unit and Integration Tests, of course. There are plenty different techniques, that could be used for its own purposes. For example:

  • Tests for different layers of application.
    @WebMvcTest for testing a web layer and @DataJpaTest for testing repository layer.
  • End-to-End Tests.
    These types of tests are used for testing an entire system — from UI to different parts of backend.
  • Performance Tests.
    Used to evaluate how an application performs under a particular workload.

In The End

Both Unit and Integration Tests serve pivotal roles in the software development lifecycle, each offering unique benefits and facing distinct challenges. I believe it’s important to master integration testing, especially in microservice architecture, where a few tests could cover almost entire microservice and test execution time usually isn’t huge. But don’t forget unit tests, since they could save you much time in various situations I mentioned above, like TDD or edge cases testing.

That’s all! I wish you happy coding and even happier testing!

--

--

krnlpn1c
0 Followers

Cats, games and movies enjoyer. Software Engineer in my free time.