概念

在开发应用的时候无论是我们写的函数或定义的应用接口,在做这些东西的时候我们多少都会做一些手工的测试,比如确定函数是否做了它该做的事情,是否返回了正确的值,接口是否按照我们当初设计的那样去提供服务。应用会越来越复杂,功能也会越来越多,也会随时发生变化,这会导致之前正常运行的功能会出现一些问题,手工测试会越来越麻烦。所以需要引入自动化测试,也就是把之前手工地测试函数,请求应用接口,人工地去观察结果这些动作全部用代码的形式来表达出来。这样来测试应用是否能正常运转的时候,只需要运行这些测试代码就可以了,最终会给我们测试结果,哪里出了问题就会在结果里显示。

自动化测试有很多类型,比如测试应用里某一个独立的单元,比如测试某一个函数,这个叫做单元测试
有的函数之间会有一些联系,测试这些相互关联的功能就是集成测试
还可以像真实的用户那样测试功能,比如测试一个应用接口的用法,这是 ere 类型的测试


测试框架

创建和运行测试需要一套测试框架,在测试中需要根据测试的东西做出一些断言,所以还需要一个断言库。测试框架和断言库是两套东西,这里使用的 jest 框架同时包含了这两套东西。


jest 测试框架

_安装了 jest 后会有一个 jest 命令,可以在 node_modules/.bin 中找到这个命令_

  1. 安装:npm install jest –save-dev
    如果使用了 ts,还需要安装类型定义:npm install @types/jest –save-dev
    如果想用 jest 测试 ts 写的应用需要再去安装 ts-jest: npm install ts-jest –save-dev
  2. 创建配置文件
    jest.config.js

    1
    2
    3
    4
    5
    module.exports = {
    preset: 'ts-jest',
    testEnvironment: 'node',
    rootDir: 'src'
    }
  3. 在 package.json 的 script 中配置一下运行命令

    1
    2
    "test":"jest",
    "test:watch":"jest --watch"

举个例子

新建文件 app/playground/demo.ts:

1
2
3
4
5
6
7
export const greet = (name: string) => {
return `你好,${name}`;
};

const greeting = greet("哈哈");

console.log(greeting);

node 运行一下:node dist/app/playground/demo
打印出 你好,哈哈

通过观察控制台打印出来的结果我们就可以判断 greet 这个函数是不是正常工作了

创建与运行测试

将上面的例子用自动化测试来做一下。删除 demo.ts 中的

1
2
3
const greeting = greet("哈哈");

console.log(greeting);

新建文件 app/app.test.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { greet } from "./playground/demo";

/**
* 单元测试
*/

//使用describe组织一组测试,第一个参数是这次测试的标题,第二个参数是回调函数,在函数中可以使用test方法创建测试
//test第一个参数是标题,第二个参数也是回调函数,在回调函数里可以断言。
describe("演示单元测试", () => {
// 测试
test("测试greet函数", () => {
// 准备
const greeting = greet("陈奕迅");

// 断言
expect(greeting).toBe("你好,陈奕迅"); //这里的意思是我们断言greeting执行后的结果等于 你好,陈奕迅
});
});

运行 node run test ,查看命令行输出的结果为通过测试。


准备接口测试

之前是使用 http 客户端的软件 insomnia 来进行手工测试,现在就可以转换为自动化测试了,安装一下 supertest 去请求应用接口,然后断言去得到的响应应该是什么样的。
安装:npm install supertest –save-dev
如果使用 ts 还需安装类型定义:npm install @types/supertest –save-dev

新建 app/app.router.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import express, { response } from "express";
import { request } from "http";

const router = express.Router();

router.get("/", (request, response) => {
response.send({ title: "服务之路" });
});

router.post("/echo", (request, response) => {
response.status(201).send(request.body);
});

/**
* 导出路由
*/
export default router;

在 app/index.ts 中导入

1
2
3
4
5
import appRouter from "../app/app.router";

app.use(
appRouter
);

创建接口测试

app/app.test.ts 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 测试接口
*/
describe("演示接口测试", () => {
// 测试后要断开连接,否则会卡住

//在所有测试以后会执行这个方法
afterAll(async () => {
// 断开数据服务连接
connection.end();
});

test("测试 GET /", async () => {
// 请求接口
// response表示请求接口得到的响应,请求接口可以用request这个方法然后把app交给它,这样就可以请求应用里的接口了。
const response = await request(app).get("/"); //用get方法请求 "/"

// 做出断言
expect(response.status).toBe(200);
expect(response.body).toEqual({ title: "服务之路" });
});

test("测试 POST /echo", async () => {
// 请求接口
const response = await request(app)
.post("/echo")
.send({ message: "你好~" }); //在请求里面包含的数据可以交给send方法

// 做出断言
expect(response.status).toBe(201);
expect(response.body).toEqual({
message: "你好~",
});
});
});

使用 npm run test ,发现全部通过了


准备测试用户接口

写一下删除用户的功能,user.service.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 删除用户
*/
export const deleteUser = async (userId: number) => {
// 准备查询
const statement = `
DELETE FROM user
WHERE id = ?
`;
// 执行查询
const [data] = await connection.promise().query(statement, userId);

// 提供数据
return data;
};

新建文件 user/user.test.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import request from "supertest";
import bcrypt from "bcrypt";
import app from "../app";
import { connection } from "../app/database/mysql";
import { signToken } from "../auth/auth.service";
import { deleteUser, getUserById } from "./user.service";
import { UserModel } from "./user.model";

/**
* 准备测试
*/

//测试的时候需要创建一个测试的用户
const testUser: UserModel = {
name: "xb2-test-user-name",
password: "111111",
};

//用户更新的测试
const testUserUpdate: UserModel = {
name: "xb2-test-user-new-name",
password: "222222",
};

//把创建的测试用户交给它,等测试完成的时候删除这些用户
let testUserCreated: UserModel;

/**
* 所有测试结束后
*/
afterAll(async () => {
//删除测试用户
if (testUserCreated) {
await deleteUser(testUserCreated.id);
}

//断开数据服务连接
connection.end();
});

测试创建用户接口

user.test.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 创建用户
*/
describe("测试创建用户接口", () => {
test("创建用户的时候必须提供用户名", async () => {
// 请求接口
const response = await request(app)
.post("/users")
.send({ password: testUser.password });

// 做出断言
expect(response.status).toBe(400);
expect(response.body).toEqual({
message: "请提供用户名",
});
});

test("创建用户的时候必须提供密码", async () => {
// 请求接口
const response = await request(app)
.post("/users")
.send({ name: testUser.name });

// 做出断言
expect(response.status).toBe(400);
expect(response.body).toEqual({
message: "请提供用户密码",
});
});

test("成功创建用户以后,响应状态码应该是201", async () => {
// 请求接口
const response = await request(app)
.post("/users")
.send(testUser);

// 设置创建的测试用户
testUserCreated = await getUserById(response.body.insertId, {
password: true,
});

// 做出断言
expect(response.status).toBe(201);
});
});

使用 npm run test ,测试全部通过。


测试用户账户接口

user.test.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 用户账户
*/
describe("测试用户账户接口", () => {
test("响应里应该包含指定的属性", async () => {
// 请求接口
const response = await request(app).get(`/users/${testUserCreated.id}`);

// 做出断言
expect(response.status).toBe(200);
expect(response.body.name).toBe(testUser.name);
expect(response.body).toMatchObject({
id: expect.any(Number),
name: expect.any(String),
avatar: null,
});
});

test("当用户不存在时,响应的状态码为404", async () => {
// 请求接口
const response = await request(app).get("/users/-1");

// 做出断言
expect(response.status).toBe(404);
});
});

测试更新用户接口

user.test.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

/**
* 更新用户接口
*/
describe("测试更新用户接口", () => {
test("更新用户的时候需要验证用户身份", async () => {
// 请求接口
const response = await request(app).patch("/users");

// 做出断言
expect(response.status).toBe(401);
});
test("更新用户数据", async () => {
// 签发令牌
const token = signToken({
payload: { id: testUserCreated.id, name: testUserCreated.name },
});

// 请求接口
const response = await request(app)
.patch("/users")
.set("Authorization", `Bearer ${token}`)
.send({
validate: {
password: testUser.password,
},
update: {
name: testUserCreated.name,
password: testUserCreated.password,
},
});

// 调取用户
const user = await getUserById(testUserCreated.id, { password: true });

// 对比密码
const matched = await bcrypt.compare(
testUserCreated.password,
user.password
);

// 做出断言
expect(response.status).toBe(200);
expect(matched).toBeTruthy();
expect(user.name).toBe(testUserUpdated.name);
});
});