Testing and Debugging a Containerized Node application
Introduction
Containers in general and Docker in specific have changed the way how we think about running and deploying software quite a bit. Running an application in a container instead of directly on your computer or server has a lot of advantages. But what about testing and debugging? Are we still able to run tests and debug an application as easy and straight forward when we run it inside a container as we do when we run it directly on our host? In this post I want to give a step by step introduction on how to configure your application and how to adjust your development workflow to achieve exactly that. The source code accompanying this post can be found here.
Creating a sample application
I have chosen to build a simple node JS application that I’ll use to demonstrate testing and debugging when running in a container. The principles show here can be easily adapted to other types of applications, e.g. Python, Go or .NET Core.
Initialize the application
Create a new folder for the project. In the terminal navigate to this folder and run the command
npm init
server.js
and when asked for the test
command then enter jasmine-node spec
. This will generate our package.json
This sample application will use express JS and thus we install this library using
npm install express --save
server.js
primes.js
which contains the logic to determine whether or not a given number is a prime. Thus let’s add such a file primes.js
isPrime
which is used on line 11 in the server.js
Adding a Dockerfile
Dockerfile
jasmine-node
which we need to run our tests and how we expose not only port 3000 but also port 5858. The latter will be used to attach the debugger. Also note how we first copy only the package.json
into the image and run npm install
and only after this copy the remainder of the application folder into the image. This helps us to optimize the build of the Docker image. When building an image Docker will only rebuild the layers of the image that have changed. And it is much less likely that we change the content of the package.json
Running the application in a container
Now let’s first build the Docker image of our application. Execute this command in the terminal
docker build -t my-app .
(Don’t forget the point at the end of the command!) And now we can run a container from this image
docker run -d --name my-app -p 3000:3000 my-app
docker ps
If for some reason the container is not running we can use the docker logs myApp
Adding tests
jasmine-node
npm install -g jasmine-node
spec
to your application. Then add a file called primes-spec.js
I am not going to discuss the syntax of a Jasmine test here. Please consult the excellent documentation here if you are not familiar with Jasmine. Once we have defined a test we can now execute the following command in our terminal
npm test
jasmine-node spec
as we have defined in our project.json
We can of course also add some integration or API tests to the application. For this we need to add the request
module to our application. Use this command to add the modulenpm install request --save
server-spec.js
to the spec
npm test
docker-compose
. Let’s a add a file docker-compose.test.yml
entrypoint
as defined in the Dockerfile
with the value npm test
. Now on in the terminal enter the following commanddocker-compose -f docker-compose.test.yml up --build
entrypoint
. If all works well then we should see an output like this in the terminal
And as expected, the logs tell us that after building the image a container is run and all tests are executed. In this specific case all tests were successful and thus the exit code is 0 (zero). If some test would fail the exit code would be 1. This is important to know when we execute the tests as part of Continuous Integration (CI). Now we can tear down everything by using
docker-compose -f docker-compose.test.yml down
With this we have shown that to run tests against a node application we do not need to do this on our host but can execute them directly in a Docker container. The advantage of it is that we do not need to install any specific software other than a decent editor to edit our project file on our host (e.g. laptop). We can run our application in a Docker container and test it manually or using automated tests. This is huge when we think about it. Everything application specific is in the container. Our host remains clean. That also means that you can now work on different projects with potentially conflicting frameworks or versions of the same framework. Since you’re always only installing the necessary libraries and frameworks in the container and each container is totally opaque there are no more conflicts!
Debugging the application
Running automated tests against our application is one thing but sometimes we still need to debug our application. We want to be able to step through our code line by line and inspect the value of variables, etc. Can we do that if our application runs in a container? The answer is a bold YES. In the following I am showing how we can achieve this for our node application. I am using Visual Studio Code (which just got released in version 1.0 and runs on Windows, Mac and Linux) as my editor. The technique I am showing here has first been published by Alex Zeitler here. I have tweaked it a bit to fit with my needs. First we need to know on which IP address our Docker host runs. Use the following command to find out
docker-machine ip default
default
. In my case the IP address is 192.168.99.100
which is default when you are using Docker Toolbox. Next we add another docker-compose
file which we’ll be using for debugging. Add a file called docker-compose.debug.yml
docker-compose
file for testing we are using the Dockerfile
to build the image, we open port 3000 and(!) port 5858 and map them to the same ports on the host. Finally we override the entrypoint
with node --debug=5858 server.js
. That is we start node in debug mode listening on port 5858
. We can start a debug session by using this commanddocker-compose -f docker-compose.debug.yml up --build -d
.vscode
to our project. Inside this folder we add a file launch.json
.
The content of this file should be like this
Attach
. We specifically need to make sure that the entry address
is set to the correct IP address of our Docker host (192.168.99.100 in my case). Also double check that the port
corresponds to the one node is listening at. Once we have added the launch configuration we can click on the debug symbol in VS Code. Make sure that the Attach
configuration is selected and then hit the run
server.js
file. Open a browser and navigate to 192.168.99.100:3000
In the debug window of VS Code we can now see all kinds of interesting information like the content of variables in scope and the call stack. We can also define watches. With other words: we have a full debugging experience similar to what we’re used if we debug directly on our host instead in a container.
Summary
In this post I have shown how we can run automated tests against a node JS application running in a Docker container as well as how we can debug an application running in a container. We can achieve the same results even though the application is now not running natively on our development system but encapsulated in a container. The benefit of all this is that we don’t need to pollute our host (laptop) with libraries and frameworks to support testing and debugging. Everything is encapsulated in the containers and we only need a nice code editor on our host. What I have shown here can also be applied to other type of applications. In a next post I will do this for a ASP.NET Core application.