End-to-end (E2E) testing ensures that your entire system works correctly by validating real-world workflows across multiple layers of your application. While unit tests verify small, isolated components and integration tests validate module interactions, end-to-end tests simulate real production environments — databases, APIs, and message brokers included.
In this tutorial, we’ll explore how to combine JUnit 5 (modern test framework), Mockito (mocking/stubbing), and Testcontainers (containerized dependencies) to build reliable, scalable, and production-ready end-to-end tests.
What is End-to-End Testing?
End-to-end testing validates the full flow of your application, from the UI or API layer down to the database, ensuring that:
- Data flows correctly across layers.
- External services are properly integrated.
- Realistic infrastructure failures are handled.
Think of it as a dress rehearsal for your application, where all pieces must work together seamlessly.
Why Combine JUnit, Mockito, and Testcontainers?
- JUnit 5: Flexible annotations (
@Test
,@ParameterizedTest
,@Nested
) and modern extensions for structured testing. - Mockito: Mock dependencies like APIs or services you don’t want to run in production mode during tests.
- Testcontainers: Spin up real, disposable Docker containers for databases, message brokers, or APIs.
This trio provides a balanced test pyramid: mocks where appropriate, real containers for external dependencies, and structured test execution.
Project Setup
Maven Dependencies
<dependencies>
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.0</version>
<scope>test</scope>
</dependency>
<!-- Mockito -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.5.0</version>
<scope>test</scope>
</dependency>
<!-- Testcontainers (PostgreSQL + Kafka example) -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>kafka</artifactId>
<version>1.19.3</version>
<scope>test</scope>
</dependency>
</dependencies>
Writing Your First End-to-End Test
Example: Spring Boot + PostgreSQL + Kafka
@SpringBootTest
@Testcontainers
class OrderServiceE2ETest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@Container
static KafkaContainer kafka = new KafkaContainer("7.2.1")
.withEmbeddedZookeeper();
@Autowired
private OrderService orderService;
@MockBean
private PaymentGatewayClient paymentGateway;
@Test
void shouldProcessOrderSuccessfully() {
// Mock external payment service
Mockito.when(paymentGateway.charge(any(), anyDouble()))
.thenReturn(new PaymentResponse("SUCCESS"));
// Run actual order processing (database + Kafka involved)
Order order = orderService.placeOrder("customer123", 250.00);
// Verify interactions and persistence
assertNotNull(order.getId());
assertEquals("CONFIRMED", order.getStatus());
Mockito.verify(paymentGateway).charge(eq("customer123"), eq(250.00));
}
}
Explanation
- PostgreSQLContainer: Spins up a disposable PostgreSQL DB.
- KafkaContainer: Provides an in-memory Kafka cluster for testing event-driven flows.
- Mockito MockBean: Simulates the external payment gateway.
- Assertions + Verifications: Ensure persistence and external interactions occurred.
Best Practices for End-to-End Testing
- Use mocks only for external, unstable, or paid services (e.g., payment APIs).
- Run real dependencies via Testcontainers for databases, caches, and brokers.
- Keep tests deterministic — no reliance on external internet services.
- Parallelize cautiously: Containers are heavy; reuse when possible.
- Integrate with CI/CD pipelines (GitHub Actions, Jenkins).
CI/CD Integration
GitHub Actions Example
name: Java CI with Testcontainers
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
services:
docker:
image: docker:20.10.16-dind
steps:
- uses: actions/checkout@v3
- name: Set up JDK 21
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 21
- name: Build with Maven
run: mvn clean verify
Version Tracker
- JUnit 4 → 5: More annotations, parameterized/nested tests, extensions.
- Mockito: Supports mocking static/final/constructor methods.
- Testcontainers: Expanded modules (Kafka, LocalStack, Docker Compose).
Real-World Case Study
In a microservices-based e-commerce app:
- JUnit 5 drives the test structure.
- Mockito mocks payment and shipping providers.
- Testcontainers spins up PostgreSQL, Kafka, and Redis for persistence and messaging.
Result: Faster release cycles, fewer production outages, and safer refactoring.
Conclusion & Key Takeaways
- JUnit 5 + Mockito + Testcontainers provide a powerful, flexible, and production-grade testing stack.
- Use Mockito for unstable/paid services, Testcontainers for real dependencies.
- Integrate into CI/CD pipelines for automated, reliable test coverage.
- Write end-to-end tests sparingly but meaningfully to cover real workflows.
FAQ
1. What’s the difference between unit and end-to-end tests?
Unit tests validate isolated methods, while end-to-end tests validate complete workflows including DBs and services.
2. Can I mock databases with Mockito instead of Testcontainers?
Yes, but Testcontainers ensures real DB behavior, reducing false positives.
3. Does Testcontainers work with CI/CD pipelines?
Yes, it works seamlessly in GitHub Actions, Jenkins, and GitLab CI.
4. How do I mock static methods in Mockito?
Use Mockito.mockStatic()
(since Mockito 3.4+).
5. Should I run end-to-end tests for every build?
Prefer running them in CI nightly or before merges, as they’re heavier than unit tests.
6. Can Testcontainers simulate failures?
Yes, you can simulate network delays, dropped connections, or container restarts.
7. How do I speed up Testcontainers tests?
Enable container reuse (~/.testcontainers.properties
) and use lightweight images.
8. Can I combine Testcontainers with WireMock?
Yes, useful for testing external REST APIs while running real databases.
9. What’s the difference between TDD and BDD?
TDD focuses on developer-centric tests; BDD emphasizes behavior and collaboration with stakeholders.
10. How do I fix flaky Testcontainers tests?
Check resource limits (Docker memory/CPU), use explicit waits, and pin image versions.