第4部分。Node.js + Express + TypeScript:使用Jest进行单元测试 (Part 4. Node.js + Express + TypeScript: Unit Tests with Jest)
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.
没有单元测试,就不可能有效地开发应用程序。 这句话不仅仅是关于HTTP API服务器的事实。 从字面上讲,开发新功能时我从不运行服务器,而是与代码一起开发单元测试,然后逐步运行单元测试以验证代码,直到完成功能为止。
In previous parts we developed a Node.js+Express+Open API App with GET /hello and GET /goodbye requests. If you just start from this part, you can clone it by:
在之前的部分中,我们使用GET / hello和GET / goodbye请求开发了Node.js + Express + Open API应用程序。 如果仅从这一部分开始,则可以通过以下方式克隆它:
$ git clone git@github.com:losikov/api-example.git
$ cd api-example
$ git checkout tags/v3.4.0
最初设定 (Initial Setup)
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:
我使用了不同的框架进行测试,但是后来我开始只对所有测试类型的所有项目都使用Jest 。 Jest有不错的文档 。 要使用TypeScript开发和运行测试,请使用ts-jest 。 让我们将它们安装为dev依赖项( -D标志),并创建默认的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:
要使jest测试文件查看@exmpl范围,请更新刚刚创建的jest.config.js并添加moduleNameMapper属性:
Now, we can run our tests with yarn jest command, but let’s add a new script to package.json for convenience:
现在,我们可以使用yarn jest命令运行测试,但是为了方便起见,我们向package.json添加一个新脚本:
"scripts": {
...
+ "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.
现在,我们可以使用yarn test:unit命令运行测试。 让我们继续进行第一个单元测试并尝试运行它。
简单测试 (Simple Test)
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:
没有任何依赖关系的最容易测试的功能是src / api / services / user.ts中的 auth函数。 创建src / api / services / __ tests__文件夹,并在其中创建user.ts文件。 如您所见,我们将测试文件放在测试文件所在的__tests__文件夹中,并将其命名为与测试文件相同的名称。 除非有特殊情况,否则我们始终遵循这种模式。 将以下内容复制并粘贴到其中:
Analyze the content. Pay attention to line 3,4 and 9:
分析内容。 注意第3,4和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.
让您和其他人清楚地命名。 您可以使用it()或test() ,然后使用适合您的命名风格的一种。 要组织测试,您可以使用多个describe()级别。
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.
在第5行和第10行中,我们执行一个动作,在第6行和第11行中,我们使用玩笑期望功能验证动作的结果。 我们使用toEqual方法之一 。 请参考文档以找到所需的值验证方法。
Run yarn test:unit to see the result:
运行纱线测试:单位以查看结果:
HTTP端点单元测试 (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:
现在,让我们通过对端点执行实际的HTTP请求来测试我们的控制器。 为了做到这一点,我们将使用supertest 。 通过以下方式安装:
$ yarn add -D supertest @types/supertest
Create src/api/controllers/__tests__ folder and greeting.ts file in it with the following content:
使用以下内容在其中创建src / api / controllers / __ tests__文件夹和greeting.ts文件:
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.
我们使用空参数列表,名称值和空名称值验证GET / hello 。 让我们回顾一下带有空名称参数的最后一个测试逻辑。 我们在beforeAll函数的第9行中创建服务器,该服务器在所有测试之前被调用一次。 我们创建get请求(第38行-39行),并期望在第40行中获取json Content-Type ,在第41行中获取400状态代码。最后,我们验证响应主体,该主体应在44中具有错误描述。end ()方法是异步的,这就是为什么我们在第49行中调用done()的原因,在第37行中将它作为参数。如果期望方法之一失败,它将中断整个测试并报告错误。
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 :
$ 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:
为了完成我们的问候控制器单元测试,在src / api / controllers / greeting.ts上附加一个describe函数,它将测试GET / goodbye请求:
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.
它类似于我们之前进行的GET / hello测试,唯一的区别是,在第5行和第18行中向请求添加了Authorization标头字段。在上一个测试中,如果省略了Authorization标头字段,我们将验证行为。
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:
要运行所有单元测试,请运行yarn test:unit:
If you want to see individual tests results with test suite hierarchy when you run all tests, append a --verbose flag:
如果要在运行所有测试时查看具有测试套件层次结构的单个测试结果,请添加-verbose标志:
代码覆盖率 (Code Coverage)
To see the code coverage just append --coverage flag:
要查看代码覆盖率,只需附加--coverage标志:
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:
显然, express_dev_logger.ts和logger.ts应该从结果表中排除,因为它们将注意力从我们要测试和跟踪的文件中转移了出来。 要将文件从覆盖范围中完全排除,请将以下内容添加到每个文件的头文件: express_dev_logger.ts和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:
server.ts具有记录器初始化,也可以将其从结果中排除,因为我们不测试记录器及其输出。 进行以下修改以排除这些块:
If we run yarn test:unit --coverage now, we won’t be destructed by the red output we are not interested in:
如果我们现在运行yarn test:unit --coverage,我们将不会被我们不感兴趣的红色输出所破坏:
嘲笑 (Mocking)
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.ts file in src/api/controllers/__tests__ folder and insert the following:
在上面的覆盖结果中,我们看到api / controllers / user.ts的第20行未被单元测试调用。 在该行中,如果用户服务中的auth函数拒绝并显示错误,它将向客户端写入响应。 我们有几种选择可以进行测试以到达第20行并检查结果。 让我们尝试嘲笑。 我们将模拟用户服务,并使其拒绝并出现错误。 我们不会直接测试用户控制器,但是会在GET / goodbye请求上调用它,因为它需要授权。 在src / api / controllers / __ tests__文件夹中创建user.ts文件,并插入以下内容:
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 21, and 24. If we check the coverage now, we’ll get:
它在第12行创建端点,并在第18-20行发出GET / goodbye请求。 由于需要授权,它将调用控制器/用户/身份验证方法,后者又调用服务/用户/身份验证方法。 但是,我们在上面测试的第8行中对services / auth进行了模拟,并使模拟在第17行中因错误而被拒绝。结果,调用了controllers.user.auth函数的catch块,并返回500一个错误。 我们在第21行和第24行验证结果。如果现在检查覆盖率,我们将得到:
We forced our code by mocking user service (auth fucntion) to get to the catch block, line # 20.
我们通过模拟用户服务( 身份验证功能)来强制代码到达catch块,第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.
您可以从git https://github.com/losikov/api-example下载项目源。 Git提交历史记录和标签是根据零件进行组织的。