Insights on Integration Tests with Foresight
Issues with Integration Testing
Why do so few teams use integration tests? One likely reason is that making a good integration test requires much more effort than making a unit test. But the real problem is all the dependencies, not the tests themselves. Software never runs alone. It uses a database to store its data, and it communicates with other software. So when you go beyond unit testing, you have to provide these dependencies to the system under testing. Often developers will use mocks for the integration tests because they’re faster, easier to configure and more reliable. But using mocks contradicts the very idea of integration testing. An integration test should use real dependencies because the point is to check the integration between them. Historically, the first approach was to run the test in a controllable, dedicated environment. As you can imagine, this doesn’t work well for big projects. Different teams might deploy different versions of the application and its data into the testing environment, and sharing versions almost always leads to conflict. To get around this, you may try to manage all of the dependencies for the test manually. But your setup phase will rapidly outgrow the test and become a nightmare to maintain and keep in sync with the real world. Eventually, tools like Docker, Docker Compose and even Kubernetes were developed to simplify the setup of integration testing. Today, these are generally considered the de facto tools for integration test isolation. Those solutions are generic and work with any technology. But Testcontainers, a technology beloved by many Java developers, takes things a step further. It’s basically Docker but wrapped into a convenient API. And yet, even with top-notch tools like Testcontainers, it’s easy to screw up integration tests. A single flaw in the configuration could lead to abnormal test execution times or flaky tests. Instead of blaming developers and leaving them to find their own bugs, Thundra has developed tooling that provides improved test observability. Let’s check it out.Integration Tests Insights with Foresight
Imagine a simple quotes API. It stores quotes in Redis for simplicity and speed. It’s a very simple Spring Boot application, consisting of five classes, including a configuration, an entity and a Spring Data interface. This means only two of the classes contain the code: the service and the controller. The project is configured to run some checks with GitHub Actions.
Figure 2: Demo project architecture
@SpringBootTest
@Testcontainers
public class QuotesServiceTest {
@Container
private final GenericContainer<?> redis = new GenericContainer<>(DockerImageName.parse("redis:6")).withExposedPorts(6379);
private QuotesService sut;
@BeforeEach
public void initialize() {
final var lettuceConnectionFactory = new LettuceConnectionFactory(redis.getHost(), redis.getMappedPort(6379));
// Omitting some trivial configuration
final var quotesRepository = redisRepositoryFactory.getRepository(QuotesRepository.class);
sut = new QuotesService(quotesRepository);
}
@ParameterizedTest(name = "{0}")
@MethodSource
public void test(Quote quote) {
sut.save(quote);
var savedQuote = sut.get(quote.getId());
Assertions.assertTrue(savedQuote.isPresent());
Assertions.assertEquals(quote, savedQuote.get());
}
static Stream<Arguments> test() {
return IntStream
.range(0, 10)
.mapToObj(Integer::toString)
.map(test -> Arguments.of(Named.of(test, new Quote(test, test))));
}
}
- name: Collect Workflow Telemetry
uses: runforesight/foresight-workflow-kit-action@v1
if: success() || failure()
with:
api_key: ${{ secrets.THUNDRA_APIKEY }}
- name: Foresight test kit
if: success() || failure()
uses: runforesight/foresight-test-kit-action@v1
with:
api_key: ${{ secrets.THUNDRA_APIKEY }}
test_format: JUNIT
test_framework: JUNIT
test_path: ./build/test-results/test/TEST-io.thundra.foresight.quotes.QuotesServiceTest.xml
coverage_format: JACOCO/XML
coverage_path: ./build/reports/jacoco/test/jacocoTestReport.xml

Figure 4: Test execution times breakdown in Foresight

Figure 5: Redis servers are initialized several times during the tests
static modifier to the container field:
@Container
private static final GenericContainer<?> redis = new GenericContainer<>(DockerImageName.parse("redis:6")).withExposedPorts(6379);

Figure 8: Redis server is initiated only once