Programmers T-shirts

Testcontainers - Advanced

Updated: Mar 1, 2021



In the previous tutorial, I explained the basics of Testcontainers, make sure to read that first.

In this tutorial, I'll explain how to use it to test your Microservice, which in the real world, usually requires more than one container.

Also, make sure to visit the Testcontainers official page.


Codes

User-Management-API project

Testcontainers-Demo project


In this example, I'm going to use the User-Management-API Microservice that I created.

To run it, it must be connected to a Postgres DB, hence I'm going to define them both in Testcontainers.

The concept is the same as on "docker-compose". You define a subnet, usually a bridge network, and you add your containers. They all encapsulated inside that subnet, hence, to communicate with them you need to expose their ports to the outside world.

For example:

  • user-mngmnt-api internal port is 8080, exposed outside on port 3373

  • postgres internal port is 5432, exposed outside on port 3215

In this case, both containers will communicate over internal IP and internal ports. When the Test communicates with them it will use an external IP and external port.

E.g: http://localhost:<extPort>/path



The system we are going to test

The system contains 2 containers:

  1. User-Management-API

  2. Postgres DB

How it works:

The user-mngmt-api is a user management system. You can Add Users, Get Users and Delete Users. It's a REST API. So for example we can go to http://localhost:8080/api/vi/getAllUsers

and we'll get a JSON response with all the Users that are currently in the system.

We can also add Users to the system by sending a POST request to the /api/addUser URL, with a JSON body:

{
"firstName": "jon",
"lastName": "dao"
}

Users are being stored in a User's Table in Postgres DB.

when we Add/Get Users from the system, it communicates with the DB over HTTP, hence we must make sure both the API and the DB can perform that communication.


Test Flow

  1. Configure and spin up the containers

  2. Add a new User to the system

  3. Verify we can fetch that User we just added

  4. Close the containers



Code Explanation

First I start by defining the Testcontainers Network instance "network".

All containers that I want to communicate with under the same subnet will get this instance as their network.

Then at the @Bforer method, I define the containers and start them -

Postgres Container

  1. 10-sec sleep. I make sure my Microservice is starting only after Postgres is already up and ready for connections. There are better ways to do so, but I rather keep this tutorial simple.

  2. withNetwork("network") - pass the container a network instance. To communicate, all containers in this environment will have to get this same network instance.

  3. withNetworkAliases("postgres") - no matter which IP it will get when it starts up, any container in the network will be able to communicate with it under the address "postgres"

  4. withExposedPorts(5432) - expose port 5432 to the outside network. same as running via the command line with -p 5432:5432.

  5. withEnv() - here we can pass the container any environment variables. Same as running Docker container from the CLI with the -e variable. In this case, I make sure to run the container with the same password that my Microservice is using.

  6. start() to start the container. At this point, we can print "docker ps" in the terminal to see the container up and running.

user-mngmnt-api Container

  1. I provided it with a name

  2. I defined alias name for easy access its IP

  3. Exposed port 8080

  4. withEnv("postgres_ip", "postgres") - when the Microservice is starting, it tries to connect to Postgres. For that, it must get the IP where Postgres is listening.

At this point, the whole environment is up and running.

Type "docker ps" in the terminal and see all the information.

Notice: port 8080 is exposed to 32840 and port 5432 is exposed to 32839.

Now I'm going to add a User to the service. For that, I'm sending a POST request to "http//localhost:32840/api/vi/addUser" with User in the body.

Notice the URL! the user-mngmnt-api's IP is "usersApi", not localhost. But I'm communicating with it on localhost.

Note: there are 2 ways of communicating with any of the containers.

  1. Inside the subnet - 1 container is communicating with the other container.

  2. From outside the subnet - the Test is communicating with the containers.

When my Test is communicating with address "usersApi" he's coming from outside, hence I use the localhost:32840.

Regarding the external port - I get it from the getMappedPort(8080) method.


At this point, there is a new User in the DB -

{
"firstName": "jon",
"lastName": "dao"
}

Now it's time to send a GET request to "http//localhost:32840/api/v1/getAllUsers", extract the response to JsonObject (actually JsonArray in this case) with Gson, and validate the field with the Junit Assertion class.

Summary

In this tutorial, we learned how to test multiple containers that depend on each other - a typical Microservice system.

We used the Testcontainers library to configure and run them, and we managed to interact with them from our Test.


Practical Programming Tutorials for Developers

Work Desk

The SW Developer

The SW Developer was built in order to provide Practical and Simple Tutorials for programmers.

Created by Dotan Raz, Michael Rodov & Kobi Atiya

  • github
  • LinkedIn

Subscribe

Programmers T-shirts