概念
在开发应用的时候无论是我们写的函数或定义的应用接口,在做这些东西的时候我们多少都会做一些手工的测试,比如确定函数是否做了它该做的事情,是否返回了正确的值,接口是否按照我们当初设计的那样去提供服务。应用会越来越复杂,功能也会越来越多,也会随时发生变化,这会导致之前正常运行的功能会出现一些问题,手工测试会越来越麻烦。所以需要引入自动化测试,也就是把之前手工地测试函数,请求应用接口,人工地去观察结果这些动作全部用代码的形式来表达出来。这样来测试应用是否能正常运转的时候,只需要运行这些测试代码就可以了,最终会给我们测试结果,哪里出了问题就会在结果里显示。
自动化测试有很多类型,比如测试应用里某一个独立的单元,比如测试某一个函数,这个叫做单元测试
有的函数之间会有一些联系,测试这些相互关联的功能就是集成测试
还可以像真实的用户那样测试功能,比如测试一个应用接口的用法,这是 ere 类型的测试
测试框架
创建和运行测试需要一套测试框架,在测试中需要根据测试的东西做出一些断言,所以还需要一个断言库。测试框架和断言库是两套东西,这里使用的 jest 框架同时包含了这两套东西。
jest 测试框架
_安装了 jest 后会有一个 jest 命令,可以在 node_modules/.bin 中找到这个命令_
- 安装:npm install jest –save-dev
如果使用了 ts,还需要安装类型定义:npm install @types/jest –save-dev
如果想用 jest 测试 ts 写的应用需要再去安装 ts-jest: npm install ts-jest –save-dev
创建配置文件
jest.config.js
1 2 3 4 5
| module.exports = { preset: 'ts-jest', testEnvironment: 'node', rootDir: 'src' }
|
在 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); }); });
|