Advanced Testing in Java: Beyond JUnit (2025)


Advanced Testing in Java

Modern Java testing goes far beyond basic JUnit assertions. This comprehensive guide explores advanced testing techniques, frameworks, and patterns for building robust test suites.

Pro Tip: Understanding advanced testing patterns helps developers create more maintainable and reliable test suites.

JUnit 5 Advanced Features

Note: JUnit 5 introduces powerful features for parameterized tests, dynamic tests, and test lifecycle management.

Parameterized Tests


@ParameterizedTest
@MethodSource("provideOrderScenarios")
void testOrderValidation(Order order, boolean expectedValid) {
    OrderValidator validator = new OrderValidator();
    assertEquals(expectedValid, validator.isValid(order));
}

private static Stream provideOrderScenarios() {
    return Stream.of(
        Arguments.of(new Order(100.0, "USD"), true),
        Arguments.of(new Order(-100.0, "USD"), false),
        Arguments.of(new Order(100.0, null), false)
    );
}

@ParameterizedTest
@CsvSource({
    "100.0, USD, true",
    "-100.0, USD, false",
    "100.0, '', false"
})
void testOrderValidationWithCsv(
    double amount, 
    String currency, 
    boolean expectedValid) {
    Order order = new Order(amount, currency);
    OrderValidator validator = new OrderValidator();
    assertEquals(expectedValid, validator.isValid(order));
}

Dynamic Tests


@TestFactory
Stream generateDynamicTests() {
    return Stream.of(
        new Order(100.0, "USD"),
        new Order(200.0, "EUR"),
        new Order(300.0, "GBP")
    ).map(order -> DynamicTest.dynamicTest(
        "Test order: " + order,
        () -> {
            OrderValidator validator = new OrderValidator();
            assertTrue(validator.isValid(order));
        }
    ));
}

@TestFactory
Collection generateDynamicTestCollection() {
    return Arrays.asList(
        DynamicContainer.dynamicContainer(
            "Order Validation Tests",
            Arrays.asList(
                DynamicTest.dynamicTest(
                    "Valid order",
                    () -> {
                        Order order = new Order(100.0, "USD");
                        assertTrue(new OrderValidator().isValid(order));
                    }
                ),
                DynamicTest.dynamicTest(
                    "Invalid order",
                    () -> {
                        Order order = new Order(-100.0, "USD");
                        assertFalse(new OrderValidator().isValid(order));
                    }
                )
            )
        )
    );
}

Testcontainers

Pro Tip: Testcontainers provides lightweight, disposable instances of databases, message queues, and other services for integration testing.

Database Testing


@Testcontainers
class OrderRepositoryIntegrationTest {
    @Container
    static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:13");
    
    @Autowired
    private OrderRepository orderRepository;
    
    @DynamicPropertySource
    static void postgresProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }
    
    @Test
    void shouldSaveAndRetrieveOrder() {
        // Arrange
        Order order = new Order(100.0, "USD");
        
        // Act
        Order savedOrder = orderRepository.save(order);
        Optional retrievedOrder = orderRepository.findById(savedOrder.getId());
        
        // Assert
        assertTrue(retrievedOrder.isPresent());
        assertEquals(order.getAmount(), retrievedOrder.get().getAmount());
    }
}

@Testcontainers
class KafkaIntegrationTest {
    @Container
    static KafkaContainer kafka = new KafkaContainer(
        DockerImageName.parse("confluentinc/cp-kafka:6.2.1")
    );
    
    @DynamicPropertySource
    static void kafkaProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.kafka.bootstrap-servers", kafka::getBootstrapServers);
    }
    
    @Test
    void shouldSendAndReceiveMessage() {
        // Test implementation
    }
}

Advanced Mocking

Note: Advanced mocking techniques help create more realistic test scenarios and verify complex interactions.

Mockito Advanced Features


@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
    @Mock
    private OrderRepository orderRepository;
    
    @Mock
    private PaymentService paymentService;
    
    @InjectMocks
    private OrderService orderService;
    
    @Test
    void shouldProcessOrderWithRetry() {
        // Arrange
        Order order = new Order(100.0, "USD");
        when(paymentService.processPayment(any()))
            .thenThrow(new PaymentException("Temporary failure"))
            .thenReturn(new PaymentResult("SUCCESS"));
        
        // Act
        OrderResult result = orderService.processOrder(order);
        
        // Assert
        assertEquals("SUCCESS", result.getStatus());
        verify(paymentService, times(2)).processPayment(any());
    }
    
    @Test
    void shouldHandleConcurrentOrders() {
        // Arrange
        Order order1 = new Order(100.0, "USD");
        Order order2 = new Order(200.0, "EUR");
        
        when(orderRepository.save(any()))
            .thenAnswer(invocation -> {
                Thread.sleep(100); // Simulate network delay
                return invocation.getArgument(0);
            });
        
        // Act
        CompletableFuture future1 = CompletableFuture.supplyAsync(
            () -> orderService.createOrder(order1)
        );
        CompletableFuture future2 = CompletableFuture.supplyAsync(
            () -> orderService.createOrder(order2)
        );
        
        // Assert
        assertDoesNotThrow(() -> {
            Order result1 = future1.get(5, TimeUnit.SECONDS);
            Order result2 = future2.get(5, TimeUnit.SECONDS);
            assertNotNull(result1);
            assertNotNull(result2);
        });
    }
}

Spock Framework

Pro Tip: Spock provides a more expressive and readable way to write tests using Groovy's powerful features.

Spock Test Examples


class OrderServiceSpec extends Specification {
    OrderRepository orderRepository = Mock()
    PaymentService paymentService = Mock()
    OrderService orderService = new OrderService(orderRepository, paymentService)
    
    def "should process valid order"() {
        given: "a valid order"
        def order = new Order(100.0, "USD")
        def paymentResult = new PaymentResult("SUCCESS")
        
        when: "processing the order"
        def result = orderService.processOrder(order)
        
        then: "payment should be processed"
        1 * paymentService.processPayment(order)
        and: "order should be saved"
        1 * orderRepository.save(order)
        and: "result should be successful"
        result.status == "SUCCESS"
    }
    
    def "should handle invalid orders"() {
        given: "an invalid order"
        def order = new Order(-100.0, "USD")
        
        when: "processing the order"
        def result = orderService.processOrder(order)
        
        then: "payment should not be processed"
        0 * paymentService.processPayment(_)
        and: "order should not be saved"
        0 * orderRepository.save(_)
        and: "result should be failed"
        result.status == "FAILED"
        result.error == "Invalid order amount"
    }
    
    def "should retry failed payments"() {
        given: "a valid order"
        def order = new Order(100.0, "USD")
        def paymentResult = new PaymentResult("SUCCESS")
        
        when: "processing the order with initial failure"
        def result = orderService.processOrder(order)
        
        then: "payment should be retried"
        2 * paymentService.processPayment(order) >> 
            throw new PaymentException("Temporary failure") >>
            paymentResult
        and: "order should be saved after successful payment"
        1 * orderRepository.save(order)
        and: "result should be successful"
        result.status == "SUCCESS"
    }
}

Integration Testing

Note: Integration tests verify the interaction between different components of the system.

Spring Boot Integration Testing


@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrderControllerIntegrationTest {
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Autowired
    private OrderRepository orderRepository;
    
    @Test
    void shouldCreateOrder() {
        // Arrange
        CreateOrderRequest request = new CreateOrderRequest(
            new CustomerId("CUST-1"),
            List.of(new OrderLineRequest(new ProductId("PROD-1"), 2))
        );
        
        // Act
        ResponseEntity response = restTemplate.postForEntity(
            "/api/orders",
            request,
            OrderResponse.class
        );
        
        // Assert
        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertNotNull(response.getBody());
        assertEquals("DRAFT", response.getBody().getStatus());
        
        // Verify persistence
        Optional savedOrder = orderRepository.findById(
            response.getBody().getOrderId()
        );
        assertTrue(savedOrder.isPresent());
    }
    
    @Test
    void shouldHandleValidationErrors() {
        // Arrange
        CreateOrderRequest request = new CreateOrderRequest(
            null, // Invalid customer ID
            List.of(new OrderLineRequest(new ProductId("PROD-1"), -1)) // Invalid quantity
        );
        
        // Act
        ResponseEntity response = restTemplate.postForEntity(
            "/api/orders",
            request,
            ErrorResponse.class
        );
        
        // Assert
        assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
        assertNotNull(response.getBody());
        assertTrue(response.getBody().getErrors().size() > 0);
    }
}

Performance Testing

Pro Tip: Performance testing ensures that your application meets performance requirements under various conditions.

JMeter and JUnit Integration


@SpringBootTest
class OrderServicePerformanceTest {
    @Autowired
    private OrderService orderService;
    
    @Test
    void shouldHandleHighConcurrentLoad() throws InterruptedException {
        // Arrange
        int numberOfThreads = 100;
        CountDownLatch latch = new CountDownLatch(numberOfThreads);
        List> futures = new ArrayList<>();
        
        // Act
        for (int i = 0; i < numberOfThreads; i++) {
            Order order = new Order(100.0 + i, "USD");
            futures.add(CompletableFuture.supplyAsync(() -> {
                try {
                    return orderService.processOrder(order);
                } finally {
                    latch.countDown();
                }
            }));
        }
        
        // Assert
        assertTrue(latch.await(30, TimeUnit.SECONDS));
        
        // Verify all orders were processed successfully
        List results = futures.stream()
            .map(future -> {
                try {
                    return future.get(5, TimeUnit.SECONDS);
                } catch (Exception e) {
                    fail("Order processing failed: " + e.getMessage());
                    return null;
                }
            })
            .collect(Collectors.toList());
        
        assertTrue(results.stream().allMatch(result -> 
            result != null && "SUCCESS".equals(result.getStatus())
        ));
    }
    
    @Test
    void shouldMeetResponseTimeRequirements() {
        // Arrange
        Order order = new Order(100.0, "USD");
        
        // Act & Assert
        assertTimeout(Duration.ofSeconds(2), () -> {
            OrderResult result = orderService.processOrder(order);
            assertEquals("SUCCESS", result.getStatus());
        });
    }
}

Best Practices

Note: Following testing best practices ensures maintainable and reliable test suites.

Testing Best Practices

  • Use appropriate test categories (unit, integration, performance)
  • Follow the Arrange-Act-Assert pattern
  • Keep tests independent and isolated
  • Use meaningful test names
  • Implement proper test data management
  • Use appropriate mocking strategies
  • Maintain test code quality
  • Regularly review and refactor tests

Conclusion

Advanced testing in Java goes beyond basic JUnit assertions, providing powerful tools and patterns for building comprehensive test suites. By understanding and implementing these advanced testing techniques, developers can create more reliable and maintainable applications.