Unit and Integration Tests
Let’s refresh our mind about what we developed so far in the introduction to vert.x series. In the first post, we developed a very simple Vert.x 3 application, and saw how this application can be tested, packaged and executed. In the second post, we saw how this application became configurable and how we can use a random port in test, and use another configurable port in production. Finally, the previous post has shown how to use vertx-web and how to implement a small REST API. However, we forgot an important task. We didn’t test the API. In this post we will increase the confidence we have on this application by implementing unit and integration tests.
The code of this post is available in the post-4 branch of the project. The starting post, however is the code available in the post-3 branch.
Tests, Tests, Tests…
This post is mainly about tests. We distinguish two types of tests: unit tests and integration tests. Both are equally important, but have different focus. Unit tests ensure that one component of your application, generally a class in the Java world, behaves as expected. The application is not tested as a whole, but pieces by pieces. Integration tests are more black box in the sense that the application is started and tested generally externally.
In this post we are going to start with some more unit tests as a warm up session and then focus on integration tests. If you already implemented integration tests, you may be a bit scared, and it makes sense. But don’t worry, with Vert.x there are no hidden surprises.
Warmup: Some more unit tests
Let’s start slowly. Remember in the first post we have implemented a unit test with vertx-unit. The test we did is dead simple:
- we started the application before the test
- we checks that it replies “Hello”
Just to refresh your mind, let’s have a look at the code
The setUp
method is invoked before each test (as instructed by the @Before
annotation). It, first, creates a new instance of Vert.x. Then, it gets a free port and then deploys our verticle with the right configuration. Thanks to the context.asyncAssertSuccess()
it waits until the successful deployment of the verticle.
The tearDown
is straightforward and just closes the Vert.x instance. It automatically un-deploys the verticles:
Finally, our single test is:
It is only checking that the application replies “Hello” when we emit a HTTP request on /
.
Let’s now try to implement some unit tests checkin that our web application and the REST API behave as expected. Let’s start by checking that the index.html
page is correctly served. This test is very similar to the previous one:
We retrieve the index.html
page and check:
- it’s there (status code 200)
- it’s a HTML page (content type set to “text/html”)
- it has the right title (“My Whisky Collection”)
As you can see, we can test the status code and the headers directly on the HTTP response, but ensure that the body is right, we need to retrieve it. This is done with a body handler that receives the complete body as parameter. Once the last check is made, we release the async
by calling complete
.
Ok, great, but this actually does not test our REST API. Let’s ensure that we can add a bottle to the collection. Unlike the previous tests, this one is using post
to post data to the server:
First we create the content we want to add. The server consumes JSON data, so we need a JSON string. You can either write your JSON document manually, or use the Vert.x method (Json.encodePrettily
) as done here. Once we have the content, we create a post
request. We need to configure some headers to be correctly read by the server. First, we say that we are sending JSON data and we also set the content length. We also attach a response handler very close to the checks made in the previous test. Notice that we can rebuild our object from the JSON document send by the server using the JSON.decodeValue
method. It’s very convenient as it avoids lots of boilerplate code. At this point the request is not emitted, we need to write the data and call the end()
method. This is made using .write(json).end();
.
The order of the methods is important. You cannot write data if you don’t have a response handler configured. Finally don’t forget to call end
.
So, let’s try this. You can run the test using:
We could continue writing more unit test like that, but it could become quite complex. Let’s see how we could continue our tests using integration tests.
IT hurts
Well, I think we need to make that clear, integration testing hurts. If you have experience in this area, can you remember how long did it take to setup everything correctly? I get new white hairs by just thinking about it. Why are integration tests more complicated? It’s basically because of the setup:
- We must start the application in a close to production way
- We must then run the tests (and configure them to hit the right application instance)
- We must stop the application
That does not sound unconquerable like that, but if you need Linux, MacOS X and Windows support, it quickly get messy. There are plenty of great frameworks easing this such as Arquillian, but let’s do it without any framework to understand how it works.
We need a battle plan
Before rushing into the complex configuration, let’s think a minute about the tasks:
Step 1 - Reserve a free port We need to get a free port on which the application can listen, and we need to inject this port in our integration tests.
Step 2 - Generate the application configuration Once we have the free port, we need to write a JSON file configuring the application HTTP Port to this port.
Step 3 - Start the application Sounds easy right? Well it’s not that simple as we need to launch our application in a background process.
Step 4 - Execute the integration tests Finally, the central part, run the tests. But before that we should implement some integration tests. Let’s come to that later.
Step 5 - Stop the application Once the tests have been executed, regardless if there are failures or errors in the tests, we need to stop the application.
There are multiple way to implement this plan. We are going to use a generic way. It’s not necessarily the better, but can be applied almost everywhere. The approach is tight to Apache Maven. If you want to propose an alternative using Gradle or a different tool, I will be happy to add your way to the post.
Implement the plan
As said above, this section is Maven-centric, and most of the code goes in the pom.xml file. If you never used the different Maven lifecycle phases, I recommend you to look at the introduction to the Maven lifecycle.
We need to add and configure a couple of plugins. Open the pom.xml
file and in the <plugins>
section add:
We use the build-helper-maven-plugin
(a plugin to know if you are often using Maven) to pick up a free port. Once found, the plugin assigns the http.port
variable to the picked port. We execute this plugin early in the build (during the process-sources
phase), so we can use the http.port
variable in the other plugin. This was for the first step.
Two actions are required for the second step. First, in the pom.xml
file, just below the <build>
opening tag, add:
This instructs Maven to filter resources from the src/test/resources
directory. Filter means replacing placeholders by actual values. That’s exactly what we need as we now have the http.port
variable. So create the src/test/resources/my-it-config.json
file with the following content:
This configuration file is similar to the one we did in previous posts. The only difference is the ${http.port}
which is the (default) Maven syntax for filtering. So, when Maven is going to process or file it will replace ${http.port}
by the selected port. That’s all for the second step.
The step 3 and 5 are a bit more tricky. We should start and stop the application. We are going to use the maven-antrun-plugin
to achieve this. In the pom.xml
file, below the build-helper-maven-plugin
, add:
That’s a huge piece of XML, isn’t it ? We configure two executions of the plugin. The first one, happening in the pre-integration-test
phase, executes a set of bash command to start the application. It basically executes:
The fat jar embedding our application is created in the package
phase, preceding the pre-integration-test
, so yes, the fat jar is created.
As mentioned above, we launch the application as we would in a production environment.
Once, the integration tests are executed (step 4 we didn’t look at it yet), we need to stop the application (so in the the post-integration-test
phase). To close the application, we are going to invoke some shell magic command to find our process in with the ps
command and send the SIGTERM
signal. It is equivalent to:
I mentioned it above, we want Windows to be supported and these commands are not going to work on Windows. Don’t worry, Windows configuration is below …
We should now do the fourth step we (silently) skipped. To execute our integration tests, we use the maven-failsafe-plugin
. Add the following plugin configuration to your pom.xml
file:
As you can see, we pass the http.port
property as a system variable, so our tests are able to connect on the right port.
That’s all! Wow… Let’s try this (for windows users, you will need to be patient or to jump to the last section).
We should not use mvn integration-test
because the application would still be running. The verify
phase is after the post-integration-test
phase and will analyse the integration-tests results. Build failures because of integration tests failed assertions are reported in this phase.
Hey, we don’t have integration tests !
And that’s right, we set up everything, but we don’t have a single integration test. To ease the implementation, let’s use two libraries: AssertJ and Rest-Assured.
AssertJ proposes a set of assertions that you can chain and use fluently. Rest Assured is a framework to test REST API.
In the pom.xml
file, add the two following dependencies just before </dependencies>
:
Then, create the src/test/java/io/vertx/blog/first/MyRestIT.java
file. Unlike unit test, integration test ends with IT
. It’s a convention from the Failsafe plugin to distinguish unit (starting or ending with Test) from integration tests (starting or ending with IT). In the created file add:
The methods annotated with @BeforeClass
and @AfterClass
are invoked once before / after all tests of the class. Here, we just retrieve the http port (passed as a system property) and we configure REST Assured.
You may need to wait in the configureRestAssured
method that the HTTP server has been started. We recommend the awaitility test framework to check that the request can be served. It would fail the test if the server does not start.
It’s now time to implement a real test. Let’s check we can retrieve an individual product:
Here you can appreciate the power and expressiveness of Rest Assured. We retrieve the list of product, ensure the response is correct, and extract the id of a specific bottle using a JSON (Groovy) Path expression.
Then, we try to retrieve the metadata of this individual product, and check the result.
Let’s now implement a more sophisticated scenario. Let’s add and delete a product:
So, now we have integration tests let’s try:
Simple no? Well, simple once the setup is done right… You can continue implementing other integration tests to be sure that everything behave as you expect.
Dear Windows users…
This section is the bonus part for Windows user, or people wanting to run their integration tests on Windows machine too. The command we execute to stop the application is not going to work on Windows. Luckily, it’s possible to extend the pom.xml
with a profile executed on Windows.
In your pom.xml
, just after </build>
, add:
This profile replaces the actions described above to stop the application with a version working on windows. The profile is automatically enabled on Windows. As on others operating systems, execute with:
Conclusion
Wow, what a trip ! We are done… In this post we have seen how we can gain confidence in Vert.x applications by implementing both unit and integration tests. Unit tests, thanks to vert.x unit, are able to check the asynchronous aspect of Vert.x application, but could be complex for large scenarios. Thanks to Rest Assured and AssertJ, integration tests are dead simple to write… but the setup is not straightforward. This post have shown how it can be configured easily. Obviously, you could also use AssertJ and Rest Assured in your unit tests.
In the next post, we replace the in memory backend with a database, and use asynchronous integration with this database.
Stay Tuned & Happy Coding !