Unit Tests vs. Component Tests: Which One’s Better for Software Quality?

Unit Tests vs. Component Tests: Which One’s Better for Software Quality?

Dive into the good and bad of unit tests and component tests in software development. Find out how to get the best of both worlds for top-notch code quality.

Introduction

If you’re considering software testing and are torn between unit tests and component tests, this article is for you. We will present the advantages and disadvantages of each testing method, helping you select the right one for your project. Additionally, we’ll discuss how combining both techniques can lead to improved software quality.

Our Code Example

Here’s a very simple 3 layer spring REST endpoint example that has a POST endpoint to create a book and a GET endpoint to retrieve a book

// Controller
@RestController
public class BookController {
    @Autowired
    private BookService bookService;

    @GetMapping("/book/{id}")
    public ResponseEntity<Book> getBookById(@PathVariable Long id) {
        Book book = bookService.findById(id);
        return ResponseEntity.ok(book);
    }

    @PostMapping("/book")
    public ResponseEntity<Book> createBook(@RequestBody Book book) {
        Book savedBook = bookService.createBook(book);
        return ResponseEntity.ok(savedBook);
    }
}

// Service
@Service
public class BookService {
    @Autowired
    private BookRepository bookRepository;

    public Book findById(Long id) {
        return bookRepository.findById(id).orElseThrow(() -> new RuntimeException("Book not found"));
    }

    public Book createBook(Book book) {
        return bookRepository.save(book);
    }
}

// Repository
public interface BookRepository extends JpaRepository<Book, Long> {
}

The Drawbacks of Unit Tests

Unit tests focus on individual components or functions within a codebase. They’re vital for ensuring your code is up to par, but they come with some challenges:

  1. Too much focus on the nitty-gritty: Unit tests can be fragile since they’re all about specific implementation details. They might break when you change the codebase.
  2. A lot to keep up with: When you’ve got a ton of unit tests, keeping them up to date can be a real headache.
  3. Not enough integration coverage: Unit tests might not cover how components work together, so you could miss some integration problems.
  4. False confidence: A bunch of passing unit tests could make you feel good about your code quality, even if you haven’t covered all the bases.
  5. Enforcing the design: Isolating each class via mocks creates a hardened interface between each service. If you aim to modify your design, you’ll find that you’ll have to change not only multiple classes but also multiple tests.

Here’s what the unit tests covering these endpoints would look like, using mocks to isolate each layer:

Controller:

@RunWith(SpringRunner.class)
@WebMvcTest(BookController.class)
public class BookControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private BookService bookService;

    @Test
    public void getBookByIdTest() throws Exception {
        Book book = new Book(1L, "Test Title", "Test Author");
        when(bookService.findById(1L)).thenReturn(book);

        mockMvc.perform(get("/book/1"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.title", is(book.getTitle())));
    }

    @Test
    public void createBookTest() throws Exception {
        Book book = new Book(1L, "Test Title", "Test Author");
        when(bookService.createBook(any(Book.class))).thenReturn(book);

        mockMvc.perform(post("/book")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(new ObjectMapper().writeValueAsString(book)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.title", is(book.getTitle())));
    }
}

Service:

@RunWith(SpringRunner.class)
public class BookServiceTest {

    @InjectMocks
    private BookService bookService;

    @Mock
    private BookRepository bookRepository;

    @Test
    public void findByIdTest() {
        Book book = new Book(1L, "Test Title", "Test Author");
        when(bookRepository.findById(1L)).thenReturn(Optional.of(book));

        Book foundBook = bookService.findById(1L);

        assertEquals(book.getTitle(), foundBook.getTitle());
    }

    @Test
    public void createBookTest() {
        Book book = new Book(1L, "Test Title", "Test Author");
        when(bookRepository.save(any(Book.class))).thenReturn(book);

        Book savedBook = bookService.createBook(book);

        assertEquals(book.getTitle(), savedBook.getTitle());
    }
}

Repository:

@RunWith(SpringRunner.class)
@DataJpaTest
public class BookRepositoryTest {

    @Autowired
    private TestEntityManager entityManager;

    @Autowired
    private BookRepository bookRepository;

    @Test
    public void findByIdTest() {
        Book book = new Book(1L, "Test Title", "Test Author");
        entityManager.persistAndFlush(book);

        Optional<Book> foundBook = bookRepository.findById(1L);

        assertTrue(foundBook.isPresent());
        assertEquals("Test Title", foundBook.get().getTitle());
    }

    @Test
    public void saveBookTest() {
        Book book = new Book(1L, "Test Title", "Test

As you can see, this is a lot of test code. This code has to be maintained just the same as the functional code. Additionally, the use of mocks hardens the interfaces between classes, making changes between classes difficult because of the cascading changes to the tests that will result. This might be desired in a mature system, but the reduction in flexibility is usually not desired.

The Advantages of Component Tests

Component tests (also called integration tests) look at groups of components or the whole system. They’ve got some advantages over unit tests:

  1. Real-life scenarios: Component tests show you how the system works in the real world, so you get a better idea of how components interact and what the overall behavior is.
  2. Better integration coverage: By testing components together, you can find issues that unit tests might miss, like data flow, communication, or dependency management problems.
  3. Less brittleness: Since they focus on the big picture instead of tiny details, component tests are less likely to break when you change the code.
  4. Efficient testing: Sometimes, component tests can give you better test coverage with fewer tests. That means less maintenance and an easier time finding problems.

Here’s example tests that should cover the same functionality as the unit tests:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class BookControllerComponentTest {

    @Autowired
    private WebTestClient webTestClient;

    @Autowired
    private BookRepository bookRepository;

    @AfterEach
    public void tearDown() {
        bookRepository.deleteAll();
    }

    @Test
    void getBookByIdTest() {
        Book book = new Book(1L, "Test Title", "Test Author");
        bookRepository.save(book);

        webTestClient.get()
                .uri("/book/{id}", book.getId())
                .exchange()
                .expectStatus().isOk()
                .expectBody()
                .jsonPath("$.title").isEqualTo(book.getTitle())
                .jsonPath("$.author").isEqualTo(book.getAuthor());
    }

    @Test
    void createBookTest() {
        Book book = new Book(1L, "Test Title", "Test Author");

        webTestClient.post()
                .uri("/book")
                .contentType(MediaType.APPLICATION_JSON)
                .bodyValue(book)
                .exchange()
                .expectStatus().isOk()
                .expectBody()
                .jsonPath("$.title").isEqualTo("Test Title")
                .jsonPath("$.author").isEqualTo("Test Author");

        // Fetch the book from the database
        List<Book> books = bookRepository.findByTitle("Test Title");

        // Validate that the book was correctly saved
        Assertions.assertFalse(books.isEmpty(), "No books found");
        Assertions.assertEquals(book.getTitle(), books.get(0).getTitle());
        Assertions.assertEquals(book.getAuthor(), books.get(0).getAuthor());
    }
    }
}

In this case, the @SpringBootTest annotation instructs Spring Boot to launch your application, including creating the ApplicationContext and starting the server.

We’re using WebTestClient instead of MockMvc to perform HTTP requests, and we’re using @Autowired to get an instance of our BookRepository to setup and tear down our data for each test.

Please note that these component tests will run slower compared to unit tests as they are loading the entire application context and not just a slice of it. Also, they would need an actual database connection to run, as opposed to the mocked repository in unit tests.

Personally, I use H2 as a quick and dirty in memory database for testing. Here’s how you could set it up for this example

First, add H2 dependency in your pom.xml file:

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>test</scope>
</dependency>

Then, you need to create a application-test.properties file in your src/test/resources directory. This is where you’ll specify the configurations for your H2 database. Here’s what that file might look like:

spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.h2.console.enabled=true

In this file, we’re configuring Spring Boot to use an in-memory H2 database for the data source and Hibernate to automatically create and drop the database schema.

Finally, you need to tell Spring Boot to use this profile during testing. You can do this with the @ActiveProfiles annotation in your test class:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class BookControllerComponentTest {
    // ...
}

Yes, there’s a little bit more configuration (for the test environment), but the test code is extremely small. Additionally, the test code tests the interface of our service only (the REST endpoints) which tests the behavior we care about with a high level of coverage.

Striking the Perfect Balance for Software Quality

The best approach to software testing involves using both unit tests and component tests. Unit tests are great for testing individual functions or components in isolation, while component tests make sure that the system works as expected when all its parts are put together. By combining these testing strategies, developers can achieve a more comprehensive and reliable assessment of their software’s quality.

For our Spring Boot example, we can further enhance the application by adding more features like update and delete book. To maintain software quality, we would create both unit tests and component tests for each new feature. This way, we can ensure that the individual components work correctly in isolation and that the system as a whole functions as expected.

Conclusion

Understanding the strengths and weaknesses of unit tests and component tests is crucial for any software development project. By carefully considering the benefits and drawbacks of each approach and using a blend of both testing strategies, developers can create software with enhanced quality, functionality, and reliability.

With a solid understanding of both testing methodologies, developers can make informed decisions and create robust, reliable software. Remember, the key is to strike a balance between the two testing approaches to get the best of both worlds for top-notch code quality.

This is my ultimate recommendation:

  • Rely on component tests heavily, you can use them as your TDD starting point if you want (and if you use TDD).
  • Use unit tests to cover individual cases with multiple branches, clauses, or complicated calculations. Whenever possible, utilize parameterized tests to cover those multiple cases.
  • Also, apply unit tests to areas of the code that are extremely important to mitigate the risk of breaking the system. You may want a combination of both unit and component tests in these critical areas.

In summary, understanding and applying both unit tests and component tests in software development can significantly improve the quality and reliability of the end product. Unit tests allow developers to verify individual components in isolation, while component tests assess the integrated functionality of these components. Striking a balance between these two testing methodologies, rather than relying solely on one, can help developers to address a wider range of potential issues and ensure a more robust and reliable software system.

Remember, the primary goal is to create software that not only meets the desired functionality but is also easy to maintain, understand, and enhance. By employing a balanced testing strategy that includes both unit tests and component tests, you can achieve this goal more effectively. After all, it’s not just about having a bug-free code; it’s about building a system that stands the test of time and is resilient in the face of future changes.

0 comments on “Unit Tests vs. Component Tests: Which One’s Better for Software Quality?Add yours →

Leave a Reply

Your email address will not be published. Required fields are marked *