Skip to Content
Course content

127: Integration Testing Basics

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I want to show you something that happened to me a few years ago. I had a feature—a simple user registration system—and my unit tests were all green. I used Mockito to mock the database layer, and the UserService behaved exactly as I expected. I pushed to staging, and within ten minutes, the logs were screaming about DataIntegrityViolationException. I was baffled. The logic was right; the mocks were right. So why was it breaking?

The Lie of the Green Checkmark

Let's recreate that scenario. Imagine we have a UserRepository that handles the SQL and a UserService that checks if an email is taken before saving. In a unit test, I’d do something like this:

@Test
void shouldSaveUserWhenEmailIsUnique() {
    // Mocking the repo to just say "Nope, this email isn't taken"
    when(userRepository.existsByEmail("test@example.com")).thenReturn(false);
    
    userService.registerUser(new User("test@example.com"));
    
    verify(userRepository).save(any(User.class));
}

This test passes. It's fast. It's clean. But here is the problem: the mock is a liar. It tells the UserService exactly what it wants to hear. It doesn't know that the actual database has a UNIQUE constraint on the email column, or that my SQL dialect is slightly different from what the JPA provider expects. I'm testing my assumption of how the database works, not the database itself.

Connecting the Actual Wires

To fix this, I need to stop pretending. I need an integration test. Instead of mocking the UserRepository, I'm going to let Spring actually boot up a slice of the application and connect to a real (though in-memory) database like H2. I'll swap @ExtendWith(MockitoExtension.class) for @SpringBootTest.

I'll try to run the same registration logic, but this time, I'll try to register the same email twice in a row. In my unit tests, I could just force the mock to return true the second time. But here, I want to see if the database actually throws the exception I expect.

@SpringBootTest
class UserIntegrationTest {
    @Autowired
    private UserService userService;

    @Test
    void shouldThrowExceptionWhenDatabaseConstraintIsViolated() {
        User user = new User("duplicate@example.com");
        userService.registerUser(user);

        // Now we try to save the exact same user again
        assertThrows(DataIntegrityViolationException.class, () -> {
            userService.registerUser(new User("duplicate@example.com"));
        });
    }
}

When I run this, it's slower. I can feel the JVM churning as the ApplicationContext loads. But suddenly, I see a failure. Not a logic failure, but a configuration failure: the H2 database isn't configured to be case-insensitive for emails, but my production Postgres DB is. My "unique" check is passing in the test but will fail in production. I just found a bug that a thousand unit tests would have missed.

The Messy Reality of State

Now I've hit a new problem. I run this test twice, and the second time it fails because the user from the first test is still sitting in the database. Unit tests are stateless; integration tests are not. They leave footprints.

I could manually delete the users in a tearDown method, but that's tedious and error-prone. Instead, I'll use @Transactional on the test method. In Spring, this tells the framework to wrap the entire test in a transaction and then roll it back once the test finishes. It's like the test happened in a parallel universe that gets erased the moment the assertion is done.

@Test
@Transactional
void shouldKeepDatabaseClean() {
    userService.registerUser(new User("clean@example.com"));
    // This user will be rolled back automatically after the test
}

This is the core of integration testing: you move away from "Does this method do X?" toward "Does this system actually work when the pieces are wired together?" You trade speed for confidence. You don't need an integration test for every edge case—that's what unit tests are for—but you absolutely need them for the boundaries where your code touches the outside world.




📋 Practical Task

Fixing the Order Repository Constraint

You have an OrderService and an OrderRepository. The Order entity has a orderNumber field that must be unique. You have a unit test using mocks that passes, but the system crashes in production when duplicate order numbers are submitted because the OrderService is failing to catch the database's UniqueConstraintViolation.

Your Task:

  • Create an integration test class OrderIntegrationTest annotated with @SpringBootTest.
  • Inject the real OrderService.
  • Write a test method annotated with @Transactional that attempts to save two Order objects with the same orderNumber.
  • Use assertThrows to verify that the database actually throws a DataIntegrityViolationException (or the specific persistence exception used in your environment) when the second order is saved.
  • Ensure the test proves that the database constraint is active and functioning, regardless of what the service-level mocks previously suggested.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.