Few things frustrate developers more than flaky tests — tests that sometimes pass and sometimes fail without any change in the codebase. Flaky tests erode trust in the test suite, slow down CI/CD pipelines, and waste developer time. In modern Java development with JUnit, Mockito, and Testcontainers, understanding and eliminating flakiness is essential for building reliable and scalable applications.
In this tutorial, we’ll explore the causes of flaky tests in Java, demonstrate how to fix them, and share best practices for writing stable, production-grade test suites.
What Are Flaky Tests?
A flaky test is one that produces inconsistent results when executed multiple times under the same conditions. For example:
- The test passes locally but fails in CI/CD.
- The test fails randomly without code changes.
- The test outcome depends on environment, order, or timing.
Causes of Flaky Tests in Java
1. Timing and Concurrency Issues
- Tests relying on
Thread.sleep()
for async events. - Race conditions in multi-threaded code.
2. External Dependencies
- Database connections, APIs, or message brokers not available.
- Environment-specific differences (e.g., Docker vs local).
3. Test Order Dependencies
- Tests not isolated properly.
- Shared state between tests.
4. Improper Mocking/Stubbing
- Mockito mocks not reset correctly.
- Overuse of stubs causing behavior mismatches.
5. Testcontainers Issues
- Containers not ready before tests start.
- Network delays or slow startup times.
Fixing Flaky Tests: Strategies and Best Practices
1. Use Proper Synchronization
Instead of Thread.sleep()
, use:
await().atMost(5, TimeUnit.SECONDS).until(() -> service.isReady());
2. Isolate Tests
- Reset mocks before each test using
@BeforeEach
. - Avoid sharing static state between test classes.
3. Testcontainers Readiness Checks
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.waitingFor(Wait.forListeningPort())
.withStartupTimeout(Duration.ofSeconds(30));
4. Use JUnit 5 Features
@TestMethodOrder
for ordering if necessary.- Parameterized tests for edge cases.
5. Adopt Retry Logic (Cautiously)
Tools like @RepeatedTest
in JUnit 5 or custom retry extensions can help, but should be last resorts.
Example: Fixing a Flaky Database Test
Flaky Test (Before)
@Test
void testFindUser() {
User user = userRepository.findById(1L).get();
assertEquals("Alice", user.getName()); // Sometimes fails due to DB init timing
}
Fixed with Testcontainers Readiness
@Testcontainers
class UserRepositoryTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.waitingFor(Wait.forListeningPort())
.withStartupTimeout(Duration.ofSeconds(30));
@Autowired
private UserRepository userRepository;
@Test
void testFindUser() {
User user = userRepository.findById(1L).orElseThrow();
assertEquals("Alice", user.getName());
}
}
Real-World Importance
- CI/CD Stability: Flaky tests waste pipeline resources and block deployments.
- Developer Confidence: Reliable tests improve trust in refactoring and code reviews.
- Microservices Reliability: With Testcontainers, services can be tested against realistic dependencies.
Best Practices
- Prefer awaitility over
Thread.sleep()
. - Keep tests idempotent and deterministic.
- Ensure proper teardown of containers and mocks.
- Regularly review flaky tests and fix root causes instead of ignoring.
Version Tracker
- JUnit 4 → JUnit 5 improved annotations and parameterized testing.
- Mockito 3+ allows mocking static/final methods, reducing flakiness from improper workarounds.
- Testcontainers now provides improved readiness checks, LocalStack support, and faster container reuse.
Conclusion and Key Takeaways
Flaky tests are not just annoying; they’re a serious threat to CI/CD reliability. By understanding their causes and applying strategies such as better synchronization, isolation, Testcontainers readiness, and proper mocking, you can build a stable test suite.
Key takeaways:
- Identify flaky test patterns early.
- Use JUnit 5 and Mockito best practices for isolation.
- Leverage Testcontainers with proper readiness checks.
- Prioritize fixing flaky tests for long-term productivity.
FAQ
1. What are flaky tests?
Tests that pass or fail inconsistently without code changes.
2. How do I debug flaky tests?
Re-run tests multiple times, log system states, and isolate dependencies.
3. Can Mockito cause flaky tests?
Yes, if mocks aren’t reset or configured properly.
4. How do Testcontainers help reduce flakiness?
By providing disposable, consistent environments with readiness checks.
5. What’s the difference between retrying and fixing flaky tests?
Retries mask the issue; fixing addresses the root cause.
6. Are flaky tests common in CI/CD?
Yes, due to timing and environment differences.
7. Should I use Thread.sleep()
in tests?
Avoid it; use awaitility or event-driven checks instead.
8. How do I handle flaky API integration tests?
Use WireMock or Pact for contract testing.
9. Can parameterized tests reduce flakiness?
Yes, by consolidating multiple cases and ensuring consistency.
10. What’s the long-term strategy for flaky tests?
Track, prioritize fixes, and enforce best practices in large teams.