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.

Kubernetes

For Kubernetes deployments, please follow our dedicated how-to.

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

To upload the coverage to the correct commit in Teamscale, add a git.properties file to your jar/war. Depending on your environment there are multiple options:

Just add the following plugin to your build:

xml
<build>
    <plugins>
        <plugin>
            <groupId>pl.project13.maven</groupId>
            <artifactId>git-commit-id-plugin</artifactId>
            <version>4.0.0</version>
        </plugin>
    </plugins>
</build>

Now build and deploy your application.

Adjust Your Dockerfile

This step is necessary so all remaining coverage is uploaded when your application is shut down.

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

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

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

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

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

Set Up a Profiler Configuration

The profiler's configuration can be stored within Teamscale. This makes it easy to change and audit. To configure the profiler for your application, go to the Project Configuration > Coverage Profilers view. Open the dropdown of the New profiler configuration button and click on Create for a JVM (Java, Kotlin, ...) project.

The JaCoCo Agent Configuration Wizard

You will be presented with a dialog that lets you generate a profiler configuration for a Teamscale project.

The Teamscale JaCoCo Agent Profiler Configuration Wizard

  • Select the project that you want to collect coverage for.
  • Choose a configuration ID. This ID will be used when starting the profiler so that it knows which configuration to use.
  • Select a partition, which is a logical name that groups related coverage, e.g., the type of test that will be profiled (e.g., Manual Test, Unit Test, Regression Test).
  • Select all packages that should be profiled. Subpackages are included as well.

Selecting Appropriate Packages

Teamscale automatically suggests packages to profile. Please review them carefully.

Please ensure the packages you select are "future-proof", i.e., they also match packages you might add in the future. This saves you time later, because you won't have to adjust the profiler configuration whenever you add new packages.

Make sure not to profile widespread packages that are also used by third-party software, e.g. net.java or com.

Finally, click Save.

The generated profiler configuration looks similar to the following:

properties
includes=*com.company.product.*;*com.company.commons.*
teamscale-project=my-application
teamscale-partition=Manual Tests

Next, please go back to the Project Configuration > Coverage Profilers overview. Click on the button next to the profiler configuration that you just created and assign Viewer permissions to the technical user that the profiler uses to connect to Teamscale.

Next, please go to Admin > Users and select the technical user whose credentials should be used for the coverage upload. Ensure that the user has the Perform External Uploads permission for the project. You can achieve this by assigning the pre-defined Build project role to the user.

Build user permission settings

Then generate an access key by clicking on Generate New Access Key and save it. We will need it in the next step.

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.

Activate the Agent

In order to put all parts together and activate the agent for your application, you'll need to set the following environment variable:

shell
JAVA_TOOL_OPTIONS="-javaagent:/agent/teamscale-jacoco-agent.jar=teamscale-server-url=https://your.teamscale.url,teamscale-user=your-build-user-name,teamscale-access-token=your-access-token,config-id=my-application-backend"

Make sure to replace my-application-backend with the configuration ID you have chosen in the previous step and the connection details with your Teamscale URL and technical user and access token. If you are using an application server like WebSphere, JBoss, Wildfly etc. please have look at the specific instructions in the agent's documentation.

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

Now you can restart your container and the agent will record coverage and upload it to Teamscale in regular intervals. You should also see the running profiler under Project Configuration > Coverage Profilers.

Debugging Setup Problems

The agent by default logs to /tmp/teamscale-java-profiler-<PID>-<random string>/logs/ 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 sh -c "cat /tmp/teamscale-java-profiler-*/logs/*"

You can also make the profiler log debug information to stdout by appending ,debug=true to the JAVA_TOOL_OPTIONS.

Example: Docker Compose

yaml
# Add a volume that will hold the profiler files.
# It can be shared between profiler's and your app's container.
volumes:
  teamscale-java-profiler:
    
services:
  # On start, the container will copy the profiler files to /transfer,
  # which is mounted to the previously defined volume, and exit.
  teamscale-java-profiler:
    image: cqse/teamscale-jacoco-agent:v34.0.1
    volumes:
      - teamscale-java-profiler:/transfer:rw

  yourapp:
    image: your/image
    # Wait for the agent container to have copied the agent jar
    depends_on:
      teamscale-java-profiler:
        condition: service_completed_successfully
    # Use the profiler's volume.
    volumes:
      - teamscale-java-profiler:/teamscale-java-profiler:ro
    # Enable the profiler.
    # Replace with your Teamscale instance's URL, username and access token
    # as well as the profiler configuration ID from the previous step.
    environment:
      JAVA_TOOL_OPTIONS: "-javaagent:/teamscale-java-profiler/teamscale-jacoco-agent.jar=teamscale-server-url=https://your.teamscale.url,teamscale-user=your-build-user-name,teamscale-access-token=your-access-token,config-id=my-application-backend"