Node.js Testing and Deployment
Introduction
What is the need ?
Automating testing and deployment is a need for any type of application development and also for Web applications. In this context, CI/CD, standing for Continuous integration/Continuous Delivery, is a method that helps developers to frequently deliver applications to users by introducing automation in the development process.
When working in developer groups with many developers, continuous integration helps developers merge their code changes back to a shared branch on a regular basis (often daily). Once developer’s changes are merged, those changes need to be validated. This is done by automatically building the application and running different levels of automated testing, typically unit and integration tests. If automated testing discovers failures, then reports are made to developers for fixing those bugs before a new version of the application is delivered.
When automated builds and tests succeed in CI, continuous delivery allows the developers to automate the release of that validated code to a repository. From this perspective, CD can only happen in a CI/CD environment, where CI is already built into the development pipeline. At the end of the CI/CD process, the application is ready for deployment to production.
In this codelab, we will demonstrate how to develop automated tests for your Node.js Express application. We will demonstrate how single components of the application can be tested as well as how the server can be tested. Another part of the codelab will demonstrate how these tests can be integrated in the CI/CD pipeline of your gitlab repository.
What you’ll build
You will first learn how to build automated tests for your server application that was developed in the Node.js and OpenAPI codelab.
What you’ll learn
- How to build automated tests for JavaScript components and for Node.js applications.
- The CI/CD pipeline integrated in gitlab.
- How to run and integrate automated tests in this pipeline.
What you’ll need
- WebStorm.
- Node.js framework.
- The Node.js and OpenAPI codelab is a prerequisite for this codelab.
Motivations for Building a Test Environment
Like any other product used by customers, software needs to be tested before it is delivered to users. Of course, the simplest and obvious way to test a product is to use it for a while and to make sure it behaves as expected. Since the developer knows the application and thus knows how to quickly test the changes made to it, one may think that this is a reliable way of testing a software. Of course, this statement is mostly wrong:
- Developers are biased towards the parts of the software that they know best and towards the changes that they made. One might easily forget to test some parts or might not realize the ramifications of a change.
- Environments on which the software runs may be different from the environment of the machine on which the development was made and on which tests are made. Very often, the environment has an influence on the way a specific software may run.
- Last but not least, testing is a boring task that is time-consuming. Very often, developers will minimize the time that they spend on testing. In some situations, people are hired for running test activities, but the problem remains.
Even in situations where developers and testers are two different groups of people, the drawback is that developers are not involved anymore in testing and may lose the overall picture. On the other hand, testers have little knowledge of changes made and have to bother developers whenever they find something they don’t understand.
Also, in modern software development, it is necessary to build ways for rapid and safe ways to modify code. From this perspective, automated testing plays a key role, together with a clearly defined test strategy including different types of tests. Generally, tests range from unit tests, which are focused on the technical details, to acceptance tests, which show that the application objectives are being met. Tests can thus be different, but good tests mostly share the same characteristics:
- A good test is deterministic: it doesn’t matter where and when it runs, it should always produce the same outputs given the same inputs. It must also be verifiable. This of course makes sometimes the task of writing tests difficult.
- A good test is fully-automated: since it must be endlessly repeated, and since machines are good at repetitive tasks, tests have to be run as automated tasks by a machine.
- A good test is responsive: it must provide quick and straight feedback. Integrating testing feedback in the development process is essential and it must be done quickly and efficiently.
Be also aware that:
- Testing does not slow down development: in the long term, the time spent writing tests is an investment that allows changes in software in an efficient and robust way.
- Testing is not only for finding bugs: finding bugs is an important purpose, but making bugs easily detectable and fixable after every single change is even more important. This gives developers a safety net that makes changes in software easier. Without a safety net, developers would only make very conservative changes, while some less conservatives changes may be required.
Start Testing your Node.js Application
The first thing that you need to do for establishing a testing environment and strategy for your application is “Start Testing”. Of course, Node.js developers have put in place a number of modules that makes automated testing easier. So, the first thing that we need to do for testing our Node.js application is to find which modules we wish to integrate. Basically, the needs are:
- A test framework that allows to define tests for JavaScript components and to run them. Mocha, a JavaScript test framework, is a very good candidate for such a framework.
- A framework that allows to validate the results of tests. Chai is an assertion JavaScript library that makes test results validation quite easy.
- A framework for validating the HTTP interaction and the RESTful API is also required. supertest is a good candidate for this part of the job. This point will be addressed in the next section.
For demonstrating how to use the different components, we will develop a small measurement unit converter application. As a first step, you may create a new Node.js Express empty application. Within this application, we will start the development by developing the test framework. At this point, it is enough to say that our unit converter application must be able to convert measurement values from the metric system to the US/UK measurement system, and vice versa.
Mocha gives us the ability to describe
the features that we are implementing by offering a describe()
function that
encapsulates our expectations. The function takes two arguments: the first
argument is a string that describes the feature and the second argument is a
function that represents the body of the description. Our unit converter
description body looks like this:
describe("Unit Converter", () => { });
In the body of that description, we may create more detailed segments that represent different features to be tested:
describe("Unit Converter", () => {
describe("Metric to US/UK conversion", () => {});
describe("US/UK to metric conversion", () => {});
});
The describe()
function allows us to define and organize tests. For defining
concrete things to be tested, we may use the it()
function. This function is
very similar to the describe()
function and it also takes two arguments: the
first argument is a string describing the thing to be tested and the second
argument is a function that contains statements for checking the expectations.
For our unit converter, the use of the it()
function is as follows:
describe("Unit Converter", () => {
describe("Metric to US conversion", () => {
it("converts from the metric system to the US/UK system", () => {
const foot = converter.meterToFoot(1);
const inch = converter.cmToInch(1);
expect(foot).to.equal(3.280839895);
expect(inch).to.equal(0.3937007874);
});
});
describe("US/UK to metric conversion", () => {
...
});
});
As you can read, the tests defined above perform conversions from one
measurement system to another and verify that the obtained results match the
expectations. For verifying the results, we make use of the expect()
component
from the Chai assertion library. With this library,
you can verify (assert) whether results match the expectation in different
styles, using different assertion styles like the assert assertion
style or the expect assertion
style. Based on the metric to
US/UK conversion test definition, you may complete the US/UK to metric
conversion test definition.
Before running the tests, a few more things must be accomplished:
- You must require the Chai library in your “test/converter.js” file.
- You must add development dependencies to “Mocha” and “Chai” in your
“package.json” file and install those dependencies by running “npm install”.
Note that these dependencies should be added as “devDependencies” rather than
“dependencies”, as shown below:
package.json
... "devDependencies": { "mocha": "^10.1.0", "chai": "^4.3.7" } ...
- You must add your script test in the package.json.
package.jsonOnce these modifications are made, you should be able to run your tests for testing the converter component. This can be done simply by showing the npm scripts (right click on the “package.json” file) and running the test script.
... "scripts": { "start": "node index.js", "test": "mocha --reporter spec test/converter.js" }, ...
At this point, if you run the test, you should get the following error message:
> mocha --reporter spec test/converter.js
Unit Converter
Metric to US conversion
1) converts from the metric system to the US/UK system
US to metric conversion
2) converts from the US/UK system to the metric system
0 passing (6ms)
2 failing
1) Unit Converter
Metric to US conversion
converts from the metric system to the US/UK system:
ReferenceError: converter is not defined
at Context.<anonymous> (test\converter.js:9:20)
at processImmediate (node:internal/timers:464:21)
2) Unit Converter
US to metric conversion
converts from the US/UK system to the metric system:
ReferenceError: converter is not defined
at Context.<anonymous> (test\converter.js:24:21)
at processImmediate (node:internal/timers:464:21)
As you can read in the message above, the two tests defined in the
“test/converter.js” file are executed and results are displayed using the string
messages defining these tests. Of course, at this point, we have not implemented
and required the converter functions, so we obviously get the error message
ReferenceError: converter is not defined.
For completing the tests, you must thus implement the converter functions in the “converter.js” file of your application and require it in the “test/converter.js” file.
Once this is done, and depending on the way you implemented the conversion and the assertions, you may get the following result:
> mocha --reporter spec test/converter.js
Unit Converter
Metric to US conversion
1) converts from the metric system to the US/UK system
US to metric conversion
√ converts from the US/UK system to the metric system
1 passing (8ms)
1 failing
1) Unit Converter
Metric to US conversion
converts from the metric system to the US/UK system:
AssertionError: expected 3.280839895013123 to equal 3.280839895
+ expected - actual
-3.280839895013123
+3.280839895
at Context.<anonymous> (test\converter.js:14:23)
at processImmediate (node:internal/timers:464:21)
The error displayed in the console above is due to the fact that we check
equality for floating point numbers, which is subject to rounding errors and
precision issues. In the case that you get this error, you must find a way to
express the assertion which is compatible with floating point numbers (hint: you
may use the approximately
assertion rather than the to.equal
assertion). Once your
“converter.js” and “test/converter.js” are implemented correctly, you should get the
following message on the console:
> mocha --reporter spec test/converter.js
Unit Converter
Metric to US conversion
√ converts from the metric system to the US/UK system
US to metric conversion
√ converts from the US/UK system to the metric system
2 passing (7ms)
Testing the Web Server Application
In the previous section, we have demonstrated how to test specific components of any JavaScript application like a Node.js Express application. Testing the web server application and its RESTful API is also important and will be demonstrated in this section.
One very popular module for testing HTTP applications and the associated APIs is the supertest Node.js module. Among other possibilities, supertest allows to test asynchronous HTTP requests against the expected return code or content. It is also easy to use. So you first need to install the development dependency:
...
"devDependencies": {
"mocha": "^10.1.0",
"chai": "^4.3.7"
"supertest": "^6.3.1"
}
...
Once this is done, you may add another test description for testing the Express application (and not directly the components):
describe("Unit Converter Web Server", () => {
describe("GET method", () => {
it('should get 1 inch in cm', (done) => {
...
});
Since we use the supertest component for testing the Node.js Express application, the test described above must be accomplished using a request object as shown below:
const server = require("../index").server;
const app = require("../index").express_app;
request(app)
.get('/inchToCm?inches=1')
.expect(200)
.expect('Content-Type', 'application/json; charset=utf-8')
.end((err, res) => {
expect(res.body.unit).to.equal('cm');
expect(res.body.value).to.equal(2.54);
server.close(done);
app
stands for your Express application instance and
server
for the http.Server
object (created for initializing the Express
application or returned from the call to app.listen()
).
The request formulated in the code snippet above does the following:
- It first performs a
GET
request on the ‘/inchToCm?inches=1’ path. - It then checks that the HTTP status of the response denotes a successful call.
- It then checks that the response is of the expected type.
- It then checks that the response body content is as expected and terminates the checks. When checking the response body, it assumes that the responses are formulated as JSON objects with two unit and value properties.
- It then closes the server connections, so that the Node.js application can exit and so that the test can terminate. This is required when running the test in the CI/CD pipeline.
The Express application implements two routes, one on 'inchToCm'
and one
on 'cmToInch'
. Both paths require the associated 'inches'
and 'cm'
query
parameters.
Deliverables
You must deliver the measurement unit Express application, integrating all components:
* The entire Express application with all required files.
* The test/converter.js file that contains all tests, including components and server tests.
* The modified package.json file that allows running tests from a console and from the WebStorm IDE.
Tests must all pass successfully. You must also add the following tests which are not described above:
* The test validating US/UK to metric conversion must be completed.
* The test validating the ‘’/cmToInch?cm=1’ path of the web server application must be added.
Integrating Test and Deployment (CI/CD)
Now that we have developed the Unit Measurement converter application together with test programs, we may benefit from those test programs for automating both the tests and the deployment. We will use gitlab for CI/CD. As documented on GitLab CI/CD | GitLab, GitLab CI/CD, like other CI/CD environments, allows identifying errors in a timely and efficient manner.
With GitLab CI/CD, you can automatically build, test, and deploy your applications.
CI/CD Workflow
In the picture below (taken from GitLab CI/CD | GitLab), the typical development workflow is depicted. Our workflow will be simplified but it is useful to have an overview of the global picture:
- Once changes have been made to a software under development, these changes can be pushed to a specific branch in a remote Gitlab repository. As we will experience later, this push triggers the CI/CD pipeline for your project.
- The GitLab CI/CD usually runs automated scripts to build and test your application and then deploy the changes in a review application (different from the production application).
- If all tests and deployment are successful, the code changes get reviewed and approved, a merge of the specific branch into the production branch is made and all changes are deployed to the production environment.
- If something goes wrong, changes are rolled back or further changes are made for correcting the detected problems.
In our case, we will simplify the process and skip the branch/merge steps.
Integrate the Test Stage
In the previous step, we have developed test programs for our Unit Measurement converter application. For automating test runs, we will now integrate those tests into the CI/CD of GitLab. With GitLab CI/CD, we can automatically build, test, and deploy any application. Using GitLab CI/CD is straightforward and for doing so it is enough:
- To have runners available to run the jobs. Luckily enough, runners are already installed on the HEIA-FR GitLab.
- To create a “.gitlab-ci.yml” file at the root of your repository. This file is the file containing the CI/CD jobs definitions.
The first time the “.gitlab-ci.yml” file will be committed to your repository, the runner runs your jobs. The same applies for any further commit to the repository. Whenever the runner runs a job, the job results are displayed in the pipeline window.
As a starting point, you must add a “.gitlab-ci.yml” file at the root of your repository. The first version of this file is shown below
# Official framework image. Look for the different tagged releases at:
# https://hub.docker.com/r/library/node/tags/
default:
image: node:latest
# This folder is cached between builds
# https://docs.gitlab.com/ee/ci/yaml/index.html#cache
cache:
paths:
- node_modules/
# Definition of the names and order of the pipeline stages/jobs.
stages:
- test
# Job 1:
Test:
stage: test
script:
- npm install
- npm run test
Before describing the details of the file content above, please note that the
full reference documenting the definition of the .gitlab-ci.yml file is
available on Keyword reference for the .gitlab-ci.yml
file |
GitLab. The explanations for understanding
this “.gitlab-ci.yml” file are as follows:
- All lines starting with a ‘#’ are comments
- The syntax follows the yaml standard. It follows the key/values syntax.
- Keywords can be global or related to specific jobs.
- The image keyword specifies the Docker image used for running the job. This keyword can be defined in a job or like in the example above in the default section. In this case, it means that this image is the default one for running all jobs.
- The stages global keyword defines the names and order of the pipeline stages/jobs.
- The other definitions should be clear from the comments.
If you add the “.gitlab-ci.yml” file to the root of your repository, a pipeline should be created and you should be able to observe the job progress and status in the pipeline view:
The picture above shows a pipeline with two jobs. The first job has succeeded and the second job is still running. The pipeline is thus still running. By clicking on any of the job status icon, you may view the job details as shown below:
In this pipeline job log, you can see that the Mocha tests were run successfully after Node.js modules were installed - only a part of the log is shown. Please be aware that jobs are run in the root directory of your repository. If some parts of the jobs must run in a subdirectory, you may add a cd command and account for the directory structure when using paths in the .gitlab-ci.yml file.
Integrate the deploy stage
Node.js server applications, like server PHP or WordPress applications, can be hosted on various cloud platforms. Choosing the right platform for a specific application is beyond the scope of this codelab. There are however two most preferred ways to host Node.js applications:
- Use a managed platform: the developer focuses on the application itself and not on the infrastructure that is maintained by a service provider.
- Use a Virtual Machine (VM) or Virtual Private Server (VPS) hosted on the cloud: the developer gets the OS of his choice and installs, deploys, and manages everything on this virtual environment..
In our case, we do not want to get into system administration and we want to experiment with a simple way to deploy our Node.js application. We are not considering any specific technical specification or any commercial aspect of the different platforms and simplicity is the only consideration in our case.
Heroku for Node.js offers this simplicity. However, it does not offer a free plan anymore. You will receive instructions for deployment during the lesson.
Heroku is quite easy to integrate in GitLab CI/CD and it allows you to deploy an application in a few steps, as well as to automate the process. It is also well documented ( Node.js documentation) and comes with many plugins and services.
The steps for integrating deployment to Heroku in your GitLab CI/CD is as follows:
- Define two environmental variables on GitLab:
HEROKU_APP_NAME
andHEROKU_API_KEY
.HEROKU_APP_NAME
is the name of your application on Heroku andHEROKU_API_KEY
is your user account API key on Heroku. HEROKU_APP_NAME
isbackend-grXX
where XX is your group number (two digits).HEROKU_API_KEY
will be provided to you over the Teams channel.- Add the following jobs to be run before scripts in your .gitlab-ci.yml file:
gitlab-ci.yml
...
# Set of commands that are executed before the jobs
before_script:
- apt-get update -qy
- apt-get install -y ruby-dev
- gem install dpl
...
The apt-get update -qy allows to resynchronize silently the package index before installing packages. The two other lines install required packages. Dpl is a deploy tool made for continuous deployment that can be used with GitLab CI/CD.
- Add a deploy job to your .gitlab-ci.yml file. The script and the image to be used for this job are shown below:
gitlab-ci.yml
...
Deploy:
stage: deploy
image: ruby:latest
script:
- dpl --provider=heroku --app=$HEROKU_APP_NAME --api-key=$HEROKU_API_KEY
...
In the definition above, HEROKU_APP_NAME
and HEROKU_API_KEY
correspond to
the environment variables defined previously.
If you commit the changes made in the “.gitlab-ci.yml” file to your repository, then a new pipeline should be created and the two pipeline jobs should be executed. If everything was done correctly, you should be able to navigate to your application on Heroku using an URL like https://backend-unit-converter.herokuapp.com/inchtocm?inches=1 (where ‘backend-unit-converter’ stands for the name of your Heroku application).
Port to be used on Heroku
For running your Node.js application, you need to let Heroku choose the port to be used with a statement like
const port = process.env.PORT || 8080;
Deliverables
You must deliver the full measurement unit converter application, including
the .gitlab-ci.yml file and the test files to your repository. You must as
well deliver a test report that shows that:
* The Test job completes successfully as the first job of the pipeline.
* The Deploy job completes successfully as the second job of the pipeline.
* Your measurement unit application runs successfully on Heroku.