window.pipedriveLeadboosterConfig = { base: 'leadbooster-chat.pipedrive.com', companyId: 11580370, playbookUuid: '22236db1-6d50-40c4-b48f-8b11262155be', version: 2, } ;(function () { var w = window if (w.LeadBooster) { console.warn('LeadBooster already exists') } else { w.LeadBooster = { q: [], on: function (n, h) { this.q.push({ t: 'o', n: n, h: h }) }, trigger: function (n) { this.q.push({ t: 't', n: n }) }, } } })() Test Containers – How to Make Tests Easier? - The Codest
The Codest
  • About us
  • Services
    • Software Development
      • Frontend Development
      • Backend Development
    • Staff Augmentation
      • Frontend Developers
      • Backend Developers
      • Data Engineers
      • Cloud Engineers
      • QA Engineers
      • Other
    • It Advisory
      • Audit & Consulting
  • Industries
    • Fintech & Banking
    • E-commerce
    • Adtech
    • Healthtech
    • Manufacturing
    • Logistics
    • Automotive
    • IOT
  • Value for
    • CEO
    • CTO
    • Delivery Manager
  • Our team
  • Case Studies
  • Know How
    • Blog
    • Meetups
    • Webinars
    • Resources
Careers Get in touch
  • About us
  • Services
    • Software Development
      • Frontend Development
      • Backend Development
    • Staff Augmentation
      • Frontend Developers
      • Backend Developers
      • Data Engineers
      • Cloud Engineers
      • QA Engineers
      • Other
    • It Advisory
      • Audit & Consulting
  • Value for
    • CEO
    • CTO
    • Delivery Manager
  • Our team
  • Case Studies
  • Know How
    • Blog
    • Meetups
    • Webinars
    • Resources
Careers Get in touch
Back arrow GO BACK
2022-07-26
Software Development

Test Containers – How to Make Tests Easier?

Bartlomiej Kuczynski

Are you looking for a way to make tests in an easier way? We got you! Check the following article and learn how to make it possible.

Modern application development is based on one simple rule:

Use composition

We compose classes, functions, and services into bigger pieces of software. That last element is the foundation of microservices and hexagonal architecture. We would like to use existing solutions, integrate them with our software and go straight onto the market.

Do you want to handle account registration and store user data? You can pick one of OAuth services. Maybe your application offers some kind of subscription or payment? There are many services that can help you to handle this. Do you need some analytics on your website, but do not understand GDPR? Feel free and take one of the ready-to-go solutions.

Something that makes development so easy from a business point of view could give you a headache – the moment when you need to write a simple test.

The Fantastic Beasts: Queues, databases and how to test them

Unit testing is pretty simple. If you only follow the rules, then your test environment and code are healthy. What rules are those?

  • Easy to write – a unit test should be easy to write because you write a lot of them. Less effort means more tests are written.
  • Readable – the test code should be easy to read. The test is a story. It describes the behavior of software and could be used as a documentation shortcut. A good unit test helps you to fix bugs without debugging the code.
  • Reliable – the test should fail only if there is a bug in the system that is being tested. Obvious? Not always. Sometimes tests pass if you run them one by one but fail when you run them as a set. They pass on your machine, but fail on CI (Works on My Machine). A good unit test has only one reason for failure.
  • Fast – tests should be fast. Preparation to run, start and test execution itself should be very swift. Otherwise you will write them, but not run them. Slow tests mean lost focus. You wait and look at the progress bar.
  • Independent – finally, the test should be independent. That rule stems from the previous ones. Only truly independent tests can become a unit. They are not interfering with each other, can be run in any order and potential failures do not depend on the results of other tests. Independent also means no dependency on any external resources like databases, messaging services or file system. If you need to communicate with externals, you can use mocks, stubs or dummies.

Everything becomes complicated when we want to write some integration tests. It’s not bad if we would like to test a few services together. But when we need to test services that use external resources like databases or messaging services, then we are asking for trouble.

To run the test, you need to install…

Many years ago, when we wanted to make some integration tests and use, e.g., databases, we had two options:

  1. We can install a database locally. Set up a schema and connect from our test;
  2. We can connect to an existing instance „somewhere in space”.

Both had pros, both had cons. But both introduce additional levels of complexity. Sometimes it was technical complexity arising from the characteristics of certain tools, e.g. installation and management of Oracle DB on your localhost. Sometimes it was an inconvenience in the process, e.g. you need to agree with the test team about JMS usage… each time you want to run tests.

Containers to the rescue

Over the last 10 years, the idea of containerization has gained recognition in the industry. So, a natural decision is to pick the containers as a solution for our integration test issue. This is a simple, clean solution. You just run your process build and everything works! You can’t believe it? Take a look at this simple configuration of a maven build:

<build>
 <plugins>
   <plugin>
     <groupId>com.dkanejs.maven.plugins</groupId>
     <artifactId>docker-compose-maven-plugin</artifactId>
     <version>4.0.0</version>
     <executions>
       <execution>
         <id>up</id>
         <phase>test-compile</phase>
         <goals>
           <goal>up</goal>
         </goals>
         <configuration>
           <composeFile>${project.basedir}/docker-compose.yml</composeFile>
           <detachedMode>true</detachedMode>
         </configuration>
       </execution>
       <execution>
         <id>down</id>
         <phase>post-integration-test</phase>
         <goals>
           <goal>down</goal>
         </goals>
         <configuration>
           <composeFile>${project.basedir}/docker-compose.yml</composeFile>
           <detachedMode>true</detachedMode>
         </configuration>
       </execution>
     </executions>
   </plugin>
 </plugins>
</build>

And the docker-compose.yml file looks pretty nice, too!

version: "3.5"

services:

 postgres:
   container_name: reactivedb
   image: postgres:13.2
   restart: always
   environment:
     - POSTGRES_USER=admin
     - POSTGRES_PASSWORD=password
     - POSTGRES_DB=cities
   ports:
     - "5432:5432"
   volumes:
     - postgres_data:/data/db

 pgadmin:
   container_name: pgadmin4
   image: dpage/pgadmin4
   restart: always
   environment:
     PGADMIN_DEFAULT_EMAIL: [email protected]
     PGADMIN_DEFAULT_PASSWORD: password
   ports:
     - "15050:80"
   volumes:
     - pgadmin_data:/data/pgadmin

volumes:
 postgres_data:
 pgadmin_data:

But can you spot the issue here?

A cargo ship that blocks everything

The example above is very simple. Just one postgres database, pgAdmin and that’s all. When you run

bash
$ mvn clean verify

then the maven plugin starts the containers and after the tests turns them off. Problems start when the project grows and our compose file grows too. Each time you will need to start all containers, and they will be alive through the entire build. You can make the situation a little better by changing the plugin execution configuration, but it is not enough. In the worst-case scenario, your containers exhaust system resources before the tests start!

And this is not the only issue. You cannot run a single integration test from your IDE. Before that, you need to start the containers by hand. Moreover, the next maven run will tear down those containers (take a look at down execution).

So this solution is like a big cargo ship. If everything works well, then it’s ok. Any unexpected or uncommon behavior leads us to some kind of disaster.

Test containers – run containers from tests

But what if we could run our containers from tests? This idea looks good, and it is already being implemented. Testcontainers, because we are talking about this project, here is a solution for our problems. Not ideal, but nobody’s perfect.

This is a Java library, which supports JUnit and Spock tests, providing lightweight and easy ways to run the Docker container. Let’s take a look at it and write some code!

Prerequisites and configuration

Before we start, we need to check our configuration. Test containers need:

  • Docker in version v17.09,
  • Java minimum version 1.8,
  • Access to network, especially to docker.hub.

More about the requirements for specific OS and CI can be found
in documentation.

Now it’s time to add some lines to pom.xml.

I use spring boot in the project to reduce boilerplate. Test containers are independent from Spring Framework and you can use them without that.
<project>
 <dependencyManagement>
   <dependencies>
     <dependency>
       <groupId>org.testcontainers</groupId>
       <artifactId>testcontainers-bom</artifactId>
       <version>${testcontaines.version}</version>
       <type>pom</type>
       <scope>import</scope>
     </dependency>
   </dependencies>
 </dependencyManagement>
 <dependencies>
   <dependency>
     <groupId>org.postgresql</groupId>
     <artifactId>postgresql</artifactId>
     <scope>runtime</scope>
   </dependency>
   <dependency>
     <groupId>org.testcontainers</groupId>
     <artifactId>postgresql</artifactId>
     <scope>test</scope>
   </dependency>
   <dependency>
     <groupId>org.testcontainers</groupId>
     <artifactId>junit-jupiter</artifactId>
     <scope>test</scope>
   </dependency>
 </dependencies>
</project>

I use Test containers version 1.17.3, but feel free to use the newest one.

Tests with Postgres container

The first step is to prepare our instance of a container. You can do that directly in the test, but an independent class looks better.

public class Postgres13TC extends PostgreSQLContainer<Postgres13TC> {

 private static final Postgres13TC TC = new Postgres13TC();

 private Postgres13TC() {
   super("postgres:13.2");
 }

 public static Postgres13TC getInstance() {
   return TC;
 }

 @Override
 public void start() {
   super.start();
   System.setProperty("DB_URL", TC.getJdbcUrl());
   System.setProperty("DB_USERNAME", TC.getUsername());
   System.setProperty("DB_PASSWORD", TC.getPassword());
 }

 @Override
 public void stop() {
   // do nothing. This is a shared instance. Let JVM handle this operation.
 }
}

At the beginning of the tests, we will create an instance of Postgres13TC. This class can handle information about our container. The most important here are the database connection strings and credentials. Now it’s time to write a very simple test.

@Testcontainers
class SimpleDbTest {

 @Container
 private static Postgres13TC = Postgres13TC.getInstance();

 @Test
 void testConnection() {
   assumeThat(postgres13TC.isRunning());
   var connectionProps = new Properties();
   connectionProps.put("user", postgres13TC.getUsername());
   connectionProps.put("password", postgres13TC.getPassword());

   try (Connection = DriverManager.getConnection(postgres13TC.getJdbcUrl(),
       connectionProps)) {
     var resultSet = connection.prepareStatement("Select 1").executeQuery();
     resultSet.next();
     assertThat(resultSet.getInt(1)).isEqualTo(1);
   } catch (SQLException sqlException) {
     assertThat((Exception) sqlException).doesNotThrowAnyException();
   }
 }
}

I use JUnit 5 here. Annotation @Testcontainers is a part of the extensions that control containers in the test environment. They find all fields with @Container annotation and start and stop containers respectively.

Tests with Spring Boot

As I mentioned before, I use Spring Boot in the project. In this case, we need to write a little more code. The first step is to create an additional configuration class.

@Slf4j
public class ContainerInit implements
   ApplicationContextInitializer<ConfigurableApplicationContext> {

 public static Postgres13TC;

 static {
   postgres13TC = Postgres13TC.getInstance();
   postgres13TC.start();
 }

 @Override
 public void initialize(ConfigurableApplicationContext applicationContext) {
   TestPropertySourceUtils.addInlinedPropertiesToEnvironment(
       applicationContext,
       "spring.datasource.url=" + postgres13TC.getJdbcUrl(),
       "spring.datasource.username=" + postgres13TC.getUsername(),
       "spring.datasource.password=" + postgres13TC.getPassword(),
       "db.host=" + postgres13TC.getHost(),
       "db.port=" + postgres13TC.getMappedPort(postgres13TC.POSTGRESQL_PORT),
       "db.name=" + postgres13TC.getDatabaseName(),
       "db.username=" + postgres13TC.getUsername(),
       "db.password=" + postgres13TC.getPassword()
   );
 }
}

This class overrides the existing properties with values from the test container. The first three properties are standard Spring properties. The next five are additional, custom properties that can be used to configure other resources and extensions like liquibase, e.g.:

spring.liquibase.change-log=classpath:/db/changelog/dbchangelog.xml
spring.liquibase.url=jdbc:postgresql://${db.host:localhost}:${db.port:5432}/${db.name:cities}
spring.liquibase.user=${db.username:admin}
spring.liquibase.password=${db.password:password}
spring.liquibase.enabled=true

Now it’s time to define a simple integration test.

@SpringBootTest(webEnvironment = RANDOM_PORT)
@AutoConfigureTestDatabase(replace = NONE)
@ContextConfiguration(initializers = ContainerInit.class)
@Testcontainers
class DummyRepositoryTest {

 @Autowired
 private DummyRepository;

 @Test
 void shouldReturnDummy() {
   var byId = dummyRepository.getById(10L);
   var expected = new Dummy();
   expected.setId(10L);
   assertThat(byId).completes().emitsCount(1).emits(expected);
 }
}

We have some extra annotations here.

  • @SpringBootTest(webEnvironment = RANDOM_PORT) – marks the test as a Spring Boot test and starts the spring context.
  • @AutoConfigureTestDatabase(replace = NONE) – these annotations say that the spring test extension should not replace postgres database configuration with H2 in the memory configuration.
  • @ContextConfiguration(initializers = ContainerInit.class) – an additional spring context
    configuration where we set up properties from Test containers.
  • @Testcontainers – as previously mentioned, this annotation controls the container lifecycle.

In this example, I use reactive repositories, but it works the same with common JDBC and JPA repositories.

Now we can run this test. If it’s the first run, the engine needs to pull images from docker.hub. That could take a moment. After that, we will see that two containers have run. One is postgres and other is Testcontainers controller. That second container manages running containers and even if JVM unexpectedly stops, then it turns off the containers and cleans up the environment.

Let’s sum up

Test containers are very easy-to-use tools that help us to create integration tests that use Docker containers. That gives us more flexibility and increases development speed. Proper setup of test configuration reduces the time needed to board new developers. They don’t need to set up all dependencies, just run the written tests with selected configuration files.

cooperation banner

Related articles

Software Development

Build Future-Proof Web Apps: Insights from The Codest’s Expert Team

Discover how The Codest excels in creating scalable, interactive web applications with cutting-edge technologies, delivering seamless user experiences across all platforms. Learn how our expertise drives digital transformation and business...

THECODEST
Software Development

Top 10 Latvia-Based Software Development Companies

Learn about Latvia's top software development companies and their innovative solutions in our latest article. Discover how these tech leaders can help elevate your business.

thecodest
Enterprise & Scaleups Solutions

Java Software Development Essentials: A Guide to Outsourcing Successfully

Explore this essential guide on successfully outsourcing Java software development to enhance efficiency, access expertise, and drive project success with The Codest.

thecodest
Software Development

The Ultimate Guide to Outsourcing in Poland

The surge in outsourcing in Poland is driven by economic, educational, and technological advancements, fostering IT growth and a business-friendly climate.

TheCodest
Enterprise & Scaleups Solutions

The Complete Guide to IT Audit Tools and Techniques

IT audits ensure secure, efficient, and compliant systems. Learn more about their importance by reading the full article.

The Codest
Jakub Jakubowicz CTO & Co-Founder

Subscribe to our knowledge base and stay up to date on the expertise from the IT sector.

    About us

    The Codest – International software development company with tech hubs in Poland.

    United Kingdom - Headquarters

    • Office 303B, 182-184 High Street North E6 2JA
      London, England

    Poland - Local Tech Hubs

    • Fabryczna Office Park, Aleja
      Pokoju 18, 31-564 Kraków
    • Brain Embassy, Konstruktorska
      11, 02-673 Warsaw, Poland

      The Codest

    • Home
    • About us
    • Services
    • Case Studies
    • Know How
    • Careers
    • Dictionary

      Services

    • It Advisory
    • Software Development
    • Backend Development
    • Frontend Development
    • Staff Augmentation
    • Backend Developers
    • Cloud Engineers
    • Data Engineers
    • Other
    • QA Engineers

      Resources

    • Facts and Myths about Cooperating with External Software Development Partner
    • From the USA to Europe: Why do American startups decide to relocate to Europe
    • Tech Offshore Development Hubs Comparison: Tech Offshore Europe (Poland), ASEAN (Philippines), Eurasia (Turkey)
    • What are the top CTOs and CIOs Challenges?
    • The Codest
    • The Codest
    • The Codest
    • Privacy policy
    • Website terms of use

    Copyright © 2025 by The Codest. All rights reserved.

    en_USEnglish
    de_DEGerman sv_SESwedish da_DKDanish nb_NONorwegian fiFinnish fr_FRFrench pl_PLPolish arArabic it_ITItalian jaJapanese ko_KRKorean es_ESSpanish nl_NLDutch etEstonian elGreek en_USEnglish