Skip to content

Recording Test Coverage for Java with Docker

This how-to will show you how to set up test coverage recording for a dockerized Java application or any other language running on the Java Virtual Machine (JVM), so you can use the Test Gap analysis. This setup works for both automated and manual tests. The following graphic gives a quick overview over the final setup:

Overview Over the Setup

Coverage is recorded using the Teamscale JaCoCo Agent, which is a Java agent that can attach to any JVM to record the lines of code executed during any test (manual or automated). The agent also handles the automatic upload of the recorded test coverage to Teamscale on shutdown of the JVM or in configurable time intervals.

Prerequisite: Generate git.properties

The agent needs to be able to find a git.properties file, which contains the Git SHA1 of the commit that was used to build your Docker image. You can easily generate such a file and include it in your Jar/War/Ear/... files by using the corresponding Maven or Gradle build plugin.

Adjust Your Dockerfile

TIP

This step is necessary when you want to upload coverage when your application is shut down. If you have a long-running process and want to upload coverage at regular intervals, you can skip this adjustment and use the interval parameter instead.

By default, the agent sends the coverage it collected when the JVM inside your Docker container is shut down. This requires that the JVM receives the SIGTERM signal sent by your orchestration tooling. Thus, you'll need to ensure that the JVM is the main process inside the docker image. This can be achieved by using exec to replace the shell process with your JVM process:

docker
# this will not work:
CMD java -jar /your.jar

# use this instead:
CMD exec java -jar /your.jar
# this will not work:
CMD java -jar /your.jar

# use this instead:
CMD exec java -jar /your.jar

The same applies if you're using ENTRYPOINT instead of CMD:

docker
# this will not work:
ENTRYPOINT java -jar /your.jar

# use this instead:
ENTRYPOINT exec java -jar /your.jar
# this will not work:
ENTRYPOINT java -jar /your.jar

# use this instead:
ENTRYPOINT exec java -jar /your.jar

If your application is wrapped inside a start script, you'll likewise need to ensure that you use exec to replace the shell process with the JVM process.

Testing if Your JVM Is the Main Process

To list all processes inside a running Docker container (e.g. named determined_dijkstra), run

bash
docker container top determined_dijkstra
docker container top determined_dijkstra

Identify the PID of your java process. Then run the following for your container to obtain the main PID of the container:

bash
docker inspect -f '{{.State.Pid}}' determined_dijkstra
docker inspect -f '{{.State.Pid}}' determined_dijkstra

Your java process's PID must be reported as the main PID by this command.

Deploy the Agent

You'll need to deploy the cqse/teamscale-jacoco-agent image next to your own Docker images. This image will do nothing by itself, it simply provides the agent's Jar file to your containers so you don't have to include the agent in your images.

To make the agent available to your JVM, you'll need to mount the /agent volume of the cqse/teamscale-jacoco-agent container into every Docker container for which you'd like to record coverage. This volume contains the agent's jar file.

Configure the Agent

The agent expects a configuration file as its input. You'll have to include it in your Docker image. Teamscale has a wizard to generate this config file for you. Include this file in your Docker images in a well-known path, e.g. /jacocoagent.properties.

Don't Store the Configuration File Under /agent

It would then be overwritten by the volume mount you configured above.

Advanced Agent Configuration

For a list of all agent configuration options, please refer to the agent's documentation.

Activate the Agent

In order to put all parts together and activate the agent for your application, you'll need to set a JVM argument. This is most often done via the environment variable JAVA_TOOL_OPTIONS, which is automatically picked up by the JVM itself. However, if your setup overrides JAVA_TOOL_OPTIONS, e.g. in a start script, you may need to use a different environment variable that is picked up by your start scripts (e.g. JAVA_OPTS is used by many frameworks). The agent's documentation contains detailed instructions for many web application servers.

bash
JAVA_TOOL_OPTIONS="-javaagent:/agent/teamscale-jacoco-agent.jar=config-file=/jacocoagent.properties"
JAVA_TOOL_OPTIONS="-javaagent:/agent/teamscale-jacoco-agent.jar=config-file=/jacocoagent.properties"

You'll need to set this environment variable in your Docker orchestration tooling (e.g. Docker Compose, Helm, ECS, Kubernetes).

Now you can restart your container and the agent will record coverage and upload it to Teamscale in regular intervals.

To Quickly Test Whether the Setup Worked

Simply restart your container and check the external uploads page of your project in the Projects view. A new upload should be shown there from your test environment.

Debugging Setup Problems

The agent by default logs to /logs/teamscale-jacoco-agent.log inside your Docker container. If the setup does not work as expected, please have a look at the problems reported in this log file, e.g. by running:

bash
docker exec -it YOUR_CONTAINER_ID cat /logs/teamscale-jacoco-agent.log | less
docker exec -it YOUR_CONTAINER_ID cat /logs/teamscale-jacoco-agent.log | less

Kubernetes: Set an Appropriate Grace Period

When the pod running your application and our agent is stopped, the agent will upload all collected coverage to Teamscale. This adds some seconds to your application's shutdown time. By default, Kubernetes will kill any pod that takes longer than 30s to shut down. This will interrupt the agent and the coverage data will not be uploaded and is lost.

Thus, you must set an appropriate terminationGracePeriodSeconds value that is large enough so your application can shut down cleanly and the agent has a chance to send its coverage data to Teamscale. What "appropriate" means depends on your application. We recommend you run a few experiments to determine how long your application takes to shut down cleanly with and without our agent enabled and then add some buffer on top of the largest measured value.

Example: Docker Compose 3

yaml
{
  version: '3',
  services: {
    yourapp: {
      image: your/image,
      # activate the agent by setting an appropriate environment variable
      environment: {
        JAVA_TOOL_OPTIONS: "-javaagent:/agent/teamscale-jacoco-agent.jar=config-file=/jacocoagent.properties"
      },
      # use the agent's volume
      volumes: [
        agent-jar:/agent:ro
      ]
    },
    agent: {
      image: cqse/teamscale-jacoco-agent:v23.1.1,
      # make the agent's volume available
      volumes: [
        agent-jar:/agent:ro
      ]
    }
  },
  # declares the agent's volume
  volumes: [
    agent-jar:
  ]
}
{
  version: '3',
  services: {
    yourapp: {
      image: your/image,
      # activate the agent by setting an appropriate environment variable
      environment: {
        JAVA_TOOL_OPTIONS: "-javaagent:/agent/teamscale-jacoco-agent.jar=config-file=/jacocoagent.properties"
      },
      # use the agent's volume
      volumes: [
        agent-jar:/agent:ro
      ]
    },
    agent: {
      image: cqse/teamscale-jacoco-agent:v23.1.1,
      # make the agent's volume available
      volumes: [
        agent-jar:/agent:ro
      ]
    }
  },
  # declares the agent's volume
  volumes: [
    agent-jar:
  ]
}