Introduction
Hello, I’m Natsuki, a backend engineer in Yatai team at Money Forward’s Fukuoka office. Since joining in May 2024, I’ve been immersed in server-side Kotlin development for web applications. Today, I’ll be sharing insights on optimizing test performance using Testcontainers, a robust tool that enables us to verify database-related code in an environment close to the production scenarios.
Background
Testcontainers is a valuable tool for verifying database-related code within realistic environments. However, starting and stopping a Testcontainer for each test class can add around 10 seconds to the test duration, leading to longer overall test times. This article explores two methods to optimize test performance: sharing a single Testcontainer instance and utilizing Testcontainers’ Reusable feature.
1. Sharing a Single Instance
Sharing a single Testcontainer instance means creating only one container for an entire test suite, instead of creating a new container for each test class. This approach, which can be implemented in both local and CI environments using Kotlin, reduces the container creation time within a single test run. However, it doesn’t persist between different test executions.
2. Reusable Feature of Testcontainers
The Reusable feature of Testcontainers keeps containers alive even after the test execution ends. This feature is allowed only in local environments for security reasons. When enabled, Testcontainers won’t create another container regardless of how many Testcontainer instances are created in your test code. It automatically checks if a suitable container is alive and, if so, reuses it. You can verify that containers are still running after one test execution completes.
So, which one should you use?
We recommend to use both strategies to reduce the test execution time.
Always: Reduce Testcontainer Creation Time for Each Test Class in Local and CI Environments → Follow the “Share a Single Testcontainer Instance to Reduce Testing Duration in CI” section.
Optionally: Reduce Test Time by Keeping Containers Alive After Entire Test Executions → Follow the “How to enable Reusable feature of Testcontainer in local environment” section.
Share a single Testcontainer instance to reduce testing duration in CI
In CI environments, where Testcontainer’s Reusable feature is not an option, we can still optimize performance by sharing a single Testcontainer instance across tests. This approach avoids the time-consuming process of recreating the container for each test class.
Inefficient Approach: Creating New Instances
Don’t: Create a new instance whenever a test class is set up, as this causes longer testing duration due to container recreation.
class TestContainerSetup {
private val mysql = MySQLContainer("mysql:8")
init {
mysql.start()
}
}
// In test classes
class TestWithDatabase {
// Creating a new instance for each test class, leading to slower tests
testContainers = TestContainerSetup()
// Tests
}
Efficient Approach: Sharing a Single Instance
Do: Use a companion object to share one instance across all test classes.
class TestContainerSetup {
companion object {
// This Testcontainer instance is shared across all tests
private val mysql = MySQLContainer("mysql:8")
}
init {
mysql.start()
}
}
// In test classes
class TestWithDatabase {
// Sharing a single Testcontainer instance
testContainers = TestContainerSetup()
// Tests
}
By using this approach, we initialize the Testcontainer only once for entire test execution, significantly reducing the overall test duration.
How to enable Reusable feature of Testcontainer in local environment
What is Testcontainer Reusable feature?
The Reusable feature keeps containers alive after test executions and uses the same container configuration for next test executions. This significantly reduces initialization and creation time for subsequent tests. However, it’s important to note that reusing Testcontainer is prohibited in CI environments due to security reasons.
ref: Testcontainers official documentation about Reusable feature
1. Add withReuse(true)
To enable the Reusable feature, add withReuse(true)
when creating a Testcontainer instance:
private val mysql = MySQLContainer("mysql:8").withReuse(true)
Additionally, remove any container.stop()
calls if they exist, as containers are automatically stopped after CI tests, and local reuse negates the need for manual stopping.
When running tests with this configuration, you may encounter the following warning:
WARN tc.mysql:8 - Reuse was requested but the environment does not support the reuse of containers
To enable reuse of containers, you must set 'testcontainers.reuse.enable=true' in a file located at /Users/username/.testcontainers.properties
This warning indicates that you need to opt-in for the Reusable feature. This feature cannot work until you enable it in your local environment, thus preventing the reuse of Testcontainers in the CI pipeline.
2. Enable Reusable feature by setting property
To opt-in for the Reusable feature, create a .testcontainers.properties
file in your home directory and add testcontainers.reuse.enable=true
. You can do this by executing the following command:
echo "testcontainers.reuse.enable=true" >> ~/.testcontainers.properties
The official documentation mentions that you can also opt-in by addingTESTCONTAINERS_REUSE_ENABLE=true
to your environmental variables. However, environmental variables are not always parsed by IntelliJ or other test runners, so this method is not recommended. In addition, as the official documentation mentions, project-wide basis configuration is not available for this reusable settings. Therefore, we recommend using the.testcontainers.properties
file to opt-in.
Note: This setting affects all your local projects. If you want to use separate container instances per project, you can add a label to your Testcontainer configuration.
3. Verify Reusable feature
After running your tests, execute docker ps
to check if any test containers remain. You should see test containers still running, indicating that Testcontainer Reusable feature is enabled:
Now it’s reusing Testcontainer! 🎉
You can also verify this from the test logs:
Check if the container ID matches the one reused during testing.
Caution
By default, tests are executed sequentially, so sharing a single container won’t affect other tests. However, be cautious when running tests in parallel, as this approach may lead to unexpected behavior or conflicts between tests.
Conclusion
Sharing a single container instance across tests can significantly enhance the efficiency of your tests. In addition, by enabling the Reusable feature in your local environment, containers will remain alive for future test executions. By combining these strategies, you can test database-related code without the overhead of creating a new container for each test class. Give it a try and happy testing! 🚀