Testing a MicroProfile or Jakarta EE application

duration 20 minutes

Learn how to use MicroShed Testing to test a MicroProfile or Jakarta EE application.

Prerequisites:

What you’ll learn

You’ll start with an existing REST application that runs on Open Liberty and use MicroShed Testing to write tests for the application that exercise the application in a Docker container.

Sometimes tests might pass in development and testing (dev/test) environments, but fail in production because the application runs differently in production than in dev/test. Fortunately, you can minimize these differences between dev/test and production by testing your application in the same Docker container that you’ll use in production.

What is Docker?

Docker is a tool that you can use to deploy and run applications with containers. You can think of Docker as a virtual machine that runs various applications. However, unlike with a typical virtual machine, you can run these applications simultaneously on a single system and independent of one another.

Learn more about Docker on the official Docker website.

Additional prerequisites

Before you begin, Docker needs to be installed. For installation instructions, refer to the official Docker documentation. You’ll test the application in Docker containers.

Make sure to start your Docker daemon before you proceed.

Getting started

The fastest way to work through this guide is to clone the Git repository and use the projects that are provided inside:

Copied to clipboard
git clone https://github.com/openliberty/guide-microshed-testing.git
cd guide-microshed-testing

The start directory contains the starting project that you will build upon.

The finish directory contains the finished project that you will build.

Before you begin, make sure you have all the necessary prerequisites.

Try what you’ll build

The finish directory in the root of this guide contains the finished application. Give it a try before you proceed.

First, review the PersonServiceIT class to see what the tests look like:

To try out the application, go to the finish directory and run the following Maven goal to build the application and run the integration tests on an Open Liberty server in a container:

Copied to clipboard
cd finish
mvn verify

This command might take some time to run initially because the dependencies and the Docker image for Open Liberty must download. If you run the same command again, it will be faster.

The previous example shows how you can run integration tests from a cold start. With Open Liberty dev mode, you can use MicroShed Testing to run tests on an active Open Liberty server. Run the following Maven goal to start Open Liberty in dev mode:

Copied to clipboard
mvn liberty:dev

After you see the following message, your application server in dev mode is ready:

**************************************************************
*    Liberty is running in dev mode.

After the Open Liberty server starts and you see the To run tests on demand, press Enter. message, you can press the enter/return key to run the integration tests. After the tests finish, you can press the enter/return key to run the tests again, or you can make code changes to the application or tests. Dev mode automatically recompiles and updates any application or test code changes that you make.

After you’re finished running tests, exit dev mode by pressing CTRL+C in the command-line session where you ran the server.

Bootstrapping your application for testing

Navigate to the start directory to begin.

When you run Open Liberty in dev mode, dev mode listens for file changes and automatically recompiles and deploys your updates whenever you save a new change. Run the following goal to start Open Liberty in dev mode:

Copied to clipboard
mvn liberty:dev

After you see the following message, your Liberty instance is ready in dev mode:

**************************************************************
*    Liberty is running in dev mode.

Dev mode holds your command-line session to listen for file changes. Open another command-line session to continue, or open the project in your editor.

Wait for the To run tests on demand, press Enter. message, and then press the enter/return key to run the tests. You see that one test runs:

 Running integration tests...

 -------------------------------------------------------
  T E S T S
 -------------------------------------------------------
 Running io.openliberty.guides.testing.PersonServiceIT
 Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.024 s - in io.openliberty.guides.testing.PersonServiceIT

 Results:

 Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

 Integration tests finished.

To begin bootstrapping, import the MicroShedTest annotation and annotate the PersonServiceIT class with @MicroShedTest. This annotation indicates that the test class uses MicroShed Testing.

The PersonServiceIT class outlines some basic information that informs how MicroShed Testing starts the application runtime and at which URL path the application is available:

Replace the PersonServiceIT class.
View Code
Copied to clipboard
src/test/java/io/openliberty/guides/testing/PersonServiceIT.java

Import the ApplicationContainer class and the Container annotation, create the ApplicationContainer application, and annotate the application with @Container annotation.

The withAppContextRoot(String) method indicates the base path of the application. The app context root is the portion of the URL after the hostname and port. In this case, the application is deployed at the http://localhost:9080/guide-microshed-testing URL, so the app context root is /guide-microshed-testing.

The withReadinessPath(String) method indicates what path is polled by HTTP to determine application readiness. MicroShed Testing automatically starts the ApplicationContainer application and waits for it to be ready before the tests start running. In this case, you’re using the default application readiness check at the http://localhost:9080/health/ready URL, which is enabled by the MicroProfile Health feature in the server.xml configuration file. When the readiness URL returns the HTTP 200 message, the application is considered ready and the tests begin running.

Save your changes to the PersonServiceIT class and press the enter/return key in your console window to rerun the tests. You still see only one test running, but the output is different. Notice that MicroShed Testing is using a hollow configuration mode. This configuration mode means that MicroShed Testing is reusing an existing application runtime for the test, not starting up a new application instance each time you initiate a test run.

Talking to your application with a REST client

With MicroShed Testing, applications are exercised in a black-box fashion. Black-box means the tests can’t access the application internals. Instead, the application is exercised from the outside, usually with HTTP requests. To simplify the HTTP interactions, a REST client is injected into the tests. To do this, you imported the org.microshed.testing.jaxrs.RESTClient annotation, created a PersonService REST client, and annotated the REST client with @RESTClient.

In this example, the PersonService injected type is the same io.openliberty.guides.testing.PersonService class that is used in your application. However, the instance that gets injected is a REST client proxy. So, if you call personSvc.createPerson("Bob", 42), the REST client makes an HTTP POST request to the application that is running at http://localhost:9080/guide-microshed-testing/people URL, which triggers the corresponding Java method in the application.

Writing your first test

Now that the setup is complete, you can write your first test case. Start by testing the basic "create person" use case for your REST-based application. To test this use case, use the REST client that’s injected by MicroShed Testing to make the HTTP POST request to the application and read the response.

Replace the PersonServiceIT class.
View Code
Copied to clipboard
src/test/java/io/openliberty/guides/testing/PersonServiceIT.java

Replace the PersonServiceIT class to include the assertNotNull static method and write the test logic in the testCreatePerson() method.

Save the changes. Then, press the enter/return key in your console window to run the test. You see that the test ran again and exercised the REST endpoint of your application, including the response of your application’s endpoint:

[INFO] Building rest client for class io.openliberty.guides.testing.PersonService with base path: http://localhost:9080/guide-microshed-testing/ and providers: [class org.microshed.testing.jaxrs.JsonBProvider]
[INFO] Response from server: 1809686877352335426

Next, add more tests.

Replace the PersonServiceIT class.
View Code
Copied to clipboard
src/test/java/io/openliberty/guides/testing/PersonServiceIT.java

The following tests are added: testMinSizeName(), testMinAge(), testGetPerson(), testGetAllPeople(), and testUpdateAge().

Save the changes, and press the enter/return key in your console window to run the tests.

Testing outside of dev mode

Running tests in dev mode is convenient for local development, but it can be tedious to test against a running Open Liberty server in non-development scenarios such as CI/CD pipelines. For this reason, MicroShed Testing can start and stop the application runtime before and after the tests are run. This process is primarily accomplished by using Docker and Testcontainers.

To test outside of dev mode, exit dev mode by pressing CTRL+C in the command-line session where you ran the server.

Next, use the following Maven goal to run the tests from a cold start:

Copied to clipboard
mvn verify

Running tests from a cold start takes a little longer than running tests from dev mode because the application runtime needs to start each time. However, tests that are run from a cold start use a clean instance on each run to ensure consistent results. These tests also automatically hook into existing build pipelines that are set up to run the integration-test phase.

Sharing configuration across multiple classes

Typically, projects have multiple test classes that all use the same type of application deployment. For these cases, it’s useful to reuse an existing configuration and application lifecycle across multiple test classes.

First, create another test class.

Create the ErrorPathIT class.
View Code
Copied to clipboard
src/test/java/io/openliberty/guides/testing/ErrorPathIT.java

The ErrorPathIT test class has the same @Container configuration and PersonService REST client as the PersonServiceIT class.

Now, run the tests again outside of dev mode:

Copied to clipboard
mvn verify

Notice that tests for both the PersonServiceIT and ErrorPathIT classes run, but a new server starts for each test class, resulting in a longer test runtime.

Creating a common configuration

To solve this issue, common configuration can be placed in a class that implements SharedContainerConfiguration.

Create the AppDeploymentConfig class.
View Code
Copied to clipboard
src/test/java/io/openliberty/guides/testing/AppDeploymentConfig.java

After the common configuration is created, the test classes can be updated to reference this shared configuration.

Updating the PersonServiceIT class

Remove the container code from the PersonServiceIT class. Remove import statements for ApplicationContainer and Container and the ApplicationContainer app field.

Next, annotate the PersonServiceIT class with the @SharedContainerConfig annotation that references the AppDeploymentConfig shared configuration class.

Replace the PersonServiceIT class.
View Code
Copied to clipboard
src/test/java/io/openliberty/guides/testing/PersonServiceIT.java

Import the SharedContainerConfig annotation and annotate the PersonServiceIT class with @SharedContainerConfig.

Updating the ErrorPathIT class

Similarly, replace the ErrorPathIT class to remove the container code. Remove import statements for ApplicationContainer and Container and the ApplicationContainer app field.

Next, annotate the ErrorPathIT class with the @SharedContainerConfig annotation.

Replace the ErrorPathIT class.
View Code
Copied to clipboard
src/test/java/io/openliberty/guides/testing/ErrorPathIT.java

Import the SharedContainerConfig annotation and annotate the ErrorPathIT class with @SharedContainerConfig.

If you rerun the tests now, they run in about half the time because the same server instance is being used for both test classes:

Copied to clipboard
mvn verify
 13package io.openliberty.guides.testing;
 14
 15import static org.junit.jupiter.api.Assertions.assertEquals;
 16import static org.junit.jupiter.api.Assertions.assertNotNull;
 17import static org.junit.jupiter.api.Assertions.assertTrue;
 18
 19import java.util.Collection;
 20
 21import org.junit.jupiter.api.Test;
 23import org.microshed.testing.SharedContainerConfig;
 25import org.microshed.testing.jaxrs.RESTClient;
 26import org.microshed.testing.jupiter.MicroShedTest;
 27
 28@MicroShedTest
 30@SharedContainerConfig(AppDeploymentConfig.class)
 32public class PersonServiceIT {
 33
 34    @RESTClient
 35    public static PersonService personSvc;
 36
 37    @Test
 38    public void testCreatePerson() {
 39        Long createId = personSvc.createPerson("Hank", 42);
 40        assertNotNull(createId);
 41    }
 42
 43    @Test
 44    public void testMinSizeName() {
 45        Long minSizeNameId = personSvc.createPerson("Ha", 42);
 46        assertEquals(new Person("Ha", 42, minSizeNameId),
 47                     personSvc.getPerson(minSizeNameId));
 48    }
 49
 50    @Test
 51    public void testMinAge() {
 52        Long minAgeId = personSvc.createPerson("Newborn", 0);
 53        assertEquals(new Person("Newborn", 0, minAgeId),
 54                     personSvc.getPerson(minAgeId));
 55    }
 56
 57    @Test
 58    public void testGetPerson() {
 59        Long bobId = personSvc.createPerson("Bob", 24);
 60        Person bob = personSvc.getPerson(bobId);
 61        assertEquals("Bob", bob.name);
 62        assertEquals(24, bob.age);
 63        assertNotNull(bob.id);
 64    }
 65
 66    @Test
 67    public void testGetAllPeople() {
 68        Long person1Id = personSvc.createPerson("Person1", 1);
 69        Long person2Id = personSvc.createPerson("Person2", 2);
 70
 71        Person expected1 = new Person("Person1", 1, person1Id);
 72        Person expected2 = new Person("Person2", 2, person2Id);
 73
 74        Collection<Person> allPeople = personSvc.getAllPeople();
 75        assertTrue(allPeople.size() >= 2,
 76            "Expected at least 2 people to be registered, but there were only: "
 77            + allPeople);
 78        assertTrue(allPeople.contains(expected1),
 79            "Did not find person " + expected1 + " in all people: " + allPeople);
 80        assertTrue(allPeople.contains(expected2),
 81            "Did not find person " + expected2 + " in all people: " + allPeople);
 82    }
 83
 84    @Test
 85    public void testUpdateAge() {
 86        Long personId = personSvc.createPerson("newAgePerson", 1);
 87
 88        Person originalPerson = personSvc.getPerson(personId);
 89        assertEquals("newAgePerson", originalPerson.name);
 90        assertEquals(1, originalPerson.age);
 91        assertEquals(personId, Long.valueOf(originalPerson.id));
 92
 93        personSvc.updatePerson(personId,
 94            new Person(originalPerson.name, 2, originalPerson.id));
 95        Person updatedPerson = personSvc.getPerson(personId);
 96        assertEquals("newAgePerson", updatedPerson.name);
 97        assertEquals(2, updatedPerson.age);
 98        assertEquals(personId, Long.valueOf(updatedPerson.id));
 99    }
100}101

Prerequisites:

Nice work! Where to next?

Nice work! You developed automated tests for a REST service in Open Liberty by using MicroShed Testing and Open Liberty dev mode.

Testing a MicroProfile or Jakarta EE application by Open Liberty is licensed under CC BY-ND 4.0

What did you think of this guide?

Extreme Dislike Dislike Like Extreme Like

What could make this guide better?

Raise an issue to share feedback

Create a pull request to contribute to this guide

Need help?

Ask a question on Stack Overflow

Like Open Liberty? Star our repo on GitHub.

Star