Core Java

Test Everything: Advanced Unit and Integration Testing with JUnit 5

Software testing has evolved far beyond the simple “write a test for each function” mindset. With modern architectures—microservices, cloud deployments, and distributed systems—applications require robust testing strategies that go deeper than unit tests. This is where JUnit 5 shines.

JUnit 5 is not just an update of JUnit 4; it’s a complete redesign that embraces modularity, extensibility, and advanced features for both unit and integration testing. Let’s explore how to master it.

Unit vs. Integration Testing: The Foundation

Before diving into code, let’s clarify the difference.

AspectUnit TestingIntegration Testing
ScopeIndividual methods or classesMultiple components/modules working together
DependenciesMocked/stubbedReal or semi-real (e.g., database, API)
SpeedVery fastSlower due to environment setup
Tools in JUnit 5Assertions, parameterized tests, mocks (Mockito)Testcontainers, Spring Test, real services

In practice, a balanced strategy includes both—unit tests for correctness and integration tests for system reliability.

Advanced Unit Testing with JUnit 5

Nested Tests for Structure

JUnit 5 introduces @Nested tests, allowing you to organize related test cases more clearly.

import org.junit.jupiter.api.*;

class CalculatorTest {

    @Nested
    @DisplayName("Addition Tests")
    class Addition {
        @Test
        void shouldAddTwoNumbers() {
            Assertions.assertEquals(5, 2 + 3);
        }
    }

    @Nested
    @DisplayName("Subtraction Tests")
    class Subtraction {
        @Test
        void shouldSubtractTwoNumbers() {
            Assertions.assertEquals(1, 3 - 2);
        }
    }
}

This improves readability, especially in larger projects.

Parameterized Tests

Avoid repetitive test code by using @ParameterizedTest.

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class PalindromeTest {
    @ParameterizedTest
    @ValueSource(strings = {"radar", "level", "madam"})
    void shouldIdentifyPalindromes(String word) {
        Assertions.assertTrue(new StringBuilder(word).reverse().toString().equals(word));
    }
}

Using Mocks with Mockito

Mockito integrates seamlessly with JUnit 5 for isolating units under test

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static org.mockito.Mockito.*;

class UserServiceTest {
    @Test
    void shouldFetchUserById() {
        UserRepository repo = mock(UserRepository.class);
        when(repo.findById(1)).thenReturn(new User(1, "Alice"));

        UserService service = new UserService(repo);
        User user = service.getUser(1);

        Assertions.assertEquals("Alice", user.getName());
    }
}

Integration Testing with JUnit 5

Testing with Spring Boot

JUnit 5 works perfectly with Spring Boot via @SpringBootTest.

import org.springframework.boot.test.context.SpringBootTest;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;

@SpringBootTest
class UserIntegrationTest {

    @Autowired
    private UserService userService;

    @Test
    void shouldCreateUser() {
        User user = userService.createUser("Alice");
        Assertions.assertNotNull(user.getId());
    }
}

This spins up the Spring context, enabling full-stack integration testing.

Testcontainers for Real Environments

A common pain point in integration testing is database or message queue setup. With Testcontainers, you can spin up lightweight Docker containers inside tests.

import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;

class DatabaseIntegrationTest {

    @Test
    void testDatabaseConnection() {
        try (PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")) {
            postgres.start();

            String jdbcUrl = postgres.getJdbcUrl();
            // connect to db and run assertions
            Assertions.assertTrue(jdbcUrl.contains("postgresql"));
        }
    }
}

This ensures integration tests run consistently across environments.

Best Practices

  1. Fast feedback: Keep unit tests lightweight and run them on every build.
  2. Layered strategy: Write more unit tests, fewer integration tests—but ensure integration covers critical flows.
  3. Use tags and filters:
@Tag("integration")
@Test
void slowTest() { ... }

Run them selectively with Maven/Gradle when needed.

4. Leverage extensions: JUnit 5’s Extension API allows custom lifecycle management, mocking, or logging.

Final Thoughts

JUnit 5 empowers developers to move beyond “just unit testing” into a world where integration testing is equally first-class. With nested tests, parameterization, dependency injection, and Testcontainers, you can build a comprehensive testing strategy that ensures both correctness and resilience.

Modern development demands: test everything. And with JUnit 5, that’s not just a mantra—it’s practical.

Further Reading

Eleftheria Drosopoulou

Eleftheria is an Experienced Business Analyst with a robust background in the computer software industry. Proficient in Computer Software Training, Digital Marketing, HTML Scripting, and Microsoft Office, they bring a wealth of technical skills to the table. Additionally, she has a love for writing articles on various tech subjects, showcasing a talent for translating complex concepts into accessible content.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button