It is not possible to develop an application efficiently without Unit Tests. This statement is more than the truth about HTTP API server. Literally speaking, I never run the server when I develop new functionality, but develop Unit Tests together with the code, and run Unit Tests to verify the code, step by step until I finish the functionality.
In previous parts of the tutorial, we developed a Node.js+Express+Open API App with GET /hello and GET /goodbye requests. If you start from this part, you can get it by:
$ git clone https://github.com/losikov/api-example.git
$ cd api-example
$ git checkout tags/v3.3.0
$ yarn install
I used different frameworks for the testing, but then I came to use jest only for all projects for all tests types. Jest has nice documentation. To develop and run the tests with TypeScript I use ts-jest. Let’s install them as dev dependencies (-D flag), and create default jest.config.js:
$ yarn add -D jest @types/jest ts-jest
$ yarn ts-jest config:init
To make jest tests files to see @exmpl scope, update just created jest.config.js and add moduleNameMapper property:
Now, we can run our tests with yarn jest command, but let’s add a new script to package.json for convenience:
+ "test:unit": "ENV_FILE=./config/.env.test jest"
As you see, we pass it test env file. We don’t add any variables to it yet. Just create an empty file:
$ touch config/.env.test
Now, we can run our tests with yarn test:unit command. Let’s move on to our first unit test and try to run it.
The easiest function to test, which doesn’t have any dependencies is auth function in src/api/services/user.ts. Create src/api/services/__tests__ folder, and user.ts file in it. As you see, we put our test files in __tests__ folder where our tested file is located, and name it the same as a tested file. We follow this pattern all the time, unless there’s a specific case. Copy and paste the following content to it:
Analyze the content. Pay attention to line 3,4 and 9:
auth/it should resolve with true and valid userId for hardcoded token
auth/it should resolve with false for invalid token
Keep the naming clear to you and others. You can use it() or test(), and use the one which fits your naming style. To organize tests you can use multiple describe() levels.
In line 5 and 10, we perform an action, and on line 6 and 11 we validate the actions’ result with jest expect function. we use one of the methods toEqual. Refer to the documentation to find the method you need for a value validation.
Run yarn test:unit to see the result:
HTTP Endpoint Unit Tests
Now, let’s test our controllers by performing actual HTTP requests to our endpoint. In order to do it, we’ll use supertest. Install it with:
$ yarn add -D supertest @types/supertest
Create src/api/controllers/__tests__ folder and greeting.ts file in it with the following content:
We verify GET /hello with an empty params list, with a name value, and with an empty name value. Let’s review the last test logic with an empty name param. We create our server in line 9 in beforeAll function which is called once before all tests. We create our get request (line 38 -39), and expect to get json Content-Type in line 40, 400 status code in line 41. Finally, we verify response body which should have an error description in 44. end() method is async, that’s why we call done() in line 49 which we got as an argument in line 37. If one of expect methods fails, it will interrupt the entire test and reports the error.
We can run a single test file to check the result passing the file name as an argument to yarn test:unit:
$ yarn test:unit src/api/controllers/__tests__/greeting.ts
To complete our greeting controller unit tests append one more describe function to src/api/controllers/greeting.ts which will test GET /goodbye request:
It is similar to GET /hello tests we did before, and the only difference, that we add an Authorization header field to our requests in lines 5 and 18. In the last test, we verify the behavior if the Authorization header field is omitted.
You can run the test for the file again on your own to see the result, and also, change the expected values to the wrong ones to see the error output.
To run all unit test, run yarn test:unit:
If you want to see individual tests results with test suite hierarchy when you run all tests, append a --verbose flag:
To see the code coverage just append --coverage flag:
It is obvious, that express_dev_logger.ts and logger.ts should be excluded from the results table as they divert attention from the files we want to test and track. To exclude the file completely from the coverage, add the following to the header of each file, to express_dev_logger.ts and logger.ts:
/* istanbul ignore file */
... imports ...
server.ts has logger initialization which also can be excluded from the results, as we don’t test the logger and its output. Make the following modification to exclude the blocks:
If we run yarn test:unit --coverage now, we won’t be destructed by the red output we are not interested in:
In the coverage results above, we see that line 20 of api/controllers/user.ts is not called by unit tests. In that line, it writes a response to a client, if auth function in user service rejects with an error. We have a few options to make the test to get to the line # 20 and check the result. Let’s try mocking. We’ll mock user service, and make it to reject with an error. We don’t test user controller directly, but it is called on GET /goodbye request as it requires authorization. Create user_failure.ts file in src/api/controllers/__tests__ folder and insert the following:
It creates the endpoint on line 12, and make a GET /goodbye request in lines 18–20. As it requires authorization, it will call controllers/user/auth method which calls services/user/auth method. But, we mock services/auth in line 8 of the test above, and make the mock to reject with an error in line 17. As a result, a catch block of controllers.user.auth function is called, and it returns 500 with an error. We verify the result in line 20, and 23. If we check the coverage now, we’ll get:
We forced our code by mocking user service (auth fucntion) to get to the catch block, line # 20.
You can download the project sources from git https://github.com/losikov/api-example. Git commit history and tags are organized based on the parts.