Supertest 教程
1. 核心知识点讲解
1.1 Supertest 简介
Supertest 是一个功能强大的 HTTP 测试库,专门用于测试 Node.js HTTP 服务器。它建立在 Superagent 之上,提供了简洁、直观的 API 来测试 HTTP 请求和响应,支持各种 HTTP 方法和断言。
1.2 安装和配置
安装 Supertest 和测试框架:
npm install --save-dev supertest mocha chai基本使用:
const request = require('supertest');
const app = require('../app'); // Express/Koa 应用
describe('API 测试', function() {
it('应该返回 200 OK', function(done) {
request(app)
.get('/')
.expect(200, done);
});
});1.3 基本测试结构
测试 HTTP GET 请求:
const request = require('supertest');
const app = require('../app');
describe('GET /users', function() {
it('应该返回用户列表', function(done) {
request(app)
.get('/users')
.expect('Content-Type', /json/)
.expect(200)
.end(function(err, res) {
if (err) return done(err);
// 额外的断言
expect(res.body).to.be.an('array');
done();
});
});
});测试 HTTP POST 请求:
const request = require('supertest');
const app = require('../app');
describe('POST /users', function() {
it('应该创建新用户', function(done) {
request(app)
.post('/users')
.send({ name: '张三', email: 'zhangsan@example.com' })
.expect('Content-Type', /json/)
.expect(201)
.end(function(err, res) {
if (err) return done(err);
expect(res.body).to.have.property('id');
expect(res.body.name).to.equal('张三');
done();
});
});
});1.4 支持的 HTTP 方法
Supertest 支持所有 HTTP 方法:
get(path)- GET 请求post(path)- POST 请求put(path)- PUT 请求patch(path)- PATCH 请求delete(path)- DELETE 请求head(path)- HEAD 请求options(path)- OPTIONS 请求
示例:
// PUT 请求
request(app)
.put('/users/1')
.send({ name: '李四' })
.expect(200)
.end(done);
// DELETE 请求
request(app)
.delete('/users/1')
.expect(204)
.end(done);1.5 请求配置
设置请求头:
request(app)
.get('/protected')
.set('Authorization', 'Bearer token123')
.set('Content-Type', 'application/json')
.expect(200)
.end(done);设置查询参数:
request(app)
.get('/users')
.query({ page: 1, limit: 10 })
.expect(200)
.end(done);设置请求体:
// JSON 格式
request(app)
.post('/users')
.send({ name: '张三', email: 'zhangsan@example.com' })
.expect(201)
.end(done);
// 表单格式
request(app)
.post('/login')
.type('form')
.send({ username: 'admin', password: 'password' })
.expect(200)
.end(done);1.6 响应断言
状态码断言:
request(app)
.get('/not-found')
.expect(404)
.end(done);响应头断言:
request(app)
.get('/')
.expect('Content-Type', /json/)
.expect('X-Powered-By', 'Express')
.end(done);响应体断言:
request(app)
.get('/users/1')
.expect(200)
.expect(function(res) {
res.body.id.should.equal(1);
res.body.name.should.equal('张三');
})
.end(done);1.7 异步测试支持
使用 async/await:
const request = require('supertest');
const app = require('../app');
const { expect } = require('chai');
describe('异步 API 测试', function() {
it('应该返回用户列表', async function() {
const res = await request(app)
.get('/users')
.expect('Content-Type', /json/)
.expect(200);
expect(res.body).to.be.an('array');
});
it('应该创建新用户', async function() {
const res = await request(app)
.post('/users')
.send({ name: '张三', email: 'zhangsan@example.com' })
.expect('Content-Type', /json/)
.expect(201);
expect(res.body).to.have.property('id');
expect(res.body.name).to.equal('张三');
});
});1.8 测试文件上传
测试文件上传:
const request = require('supertest');
const app = require('../app');
const path = require('path');
describe('文件上传测试', function() {
it('应该上传文件', function(done) {
request(app)
.post('/upload')
.attach('file', path.resolve(__dirname, '../test-file.txt'))
.expect(200)
.end(function(err, res) {
if (err) return done(err);
expect(res.body).to.have.property('filename');
done();
});
});
});2. 实用案例分析
2.1 基本 API 测试案例
场景: 测试一个简单的 Express 应用的用户 API
实现:
// app.js
const express = require('express');
const app = express();
app.use(express.json());
let users = [
{ id: 1, name: '张三', email: 'zhangsan@example.com' },
{ id: 2, name: '李四', email: 'lisi@example.com' }
];
// 获取用户列表
app.get('/users', (req, res) => {
res.json(users);
});
// 获取单个用户
app.get('/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) return res.status(404).json({ error: '用户不存在' });
res.json(user);
});
// 创建用户
app.post('/users', (req, res) => {
const newUser = {
id: users.length + 1,
name: req.body.name,
email: req.body.email
};
users.push(newUser);
res.status(201).json(newUser);
});
// 更新用户
app.put('/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) return res.status(404).json({ error: '用户不存在' });
user.name = req.body.name || user.name;
user.email = req.body.email || user.email;
res.json(user);
});
// 删除用户
app.delete('/users/:id', (req, res) => {
const userIndex = users.findIndex(u => u.id === parseInt(req.params.id));
if (userIndex === -1) return res.status(404).json({ error: '用户不存在' });
users.splice(userIndex, 1);
res.status(204).send();
});
module.exports = app;
// test/api.test.js
const request = require('supertest');
const app = require('../app');
const { expect } = require('chai');
describe('用户 API 测试', function() {
describe('GET /users', function() {
it('应该返回用户列表', async function() {
const res = await request(app)
.get('/users')
.expect('Content-Type', /json/)
.expect(200);
expect(res.body).to.be.an('array');
expect(res.body.length).to.be.at.least(2);
});
});
describe('GET /users/:id', function() {
it('应该返回指定用户', async function() {
const res = await request(app)
.get('/users/1')
.expect('Content-Type', /json/)
.expect(200);
expect(res.body).to.have.property('id', 1);
expect(res.body).to.have.property('name', '张三');
});
it('应该返回 404 当用户不存在', async function() {
const res = await request(app)
.get('/users/999')
.expect('Content-Type', /json/)
.expect(404);
expect(res.body).to.have.property('error', '用户不存在');
});
});
describe('POST /users', function() {
it('应该创建新用户', async function() {
const res = await request(app)
.post('/users')
.send({ name: '王五', email: 'wangwu@example.com' })
.expect('Content-Type', /json/)
.expect(201);
expect(res.body).to.have.property('id');
expect(res.body).to.have.property('name', '王五');
expect(res.body).to.have.property('email', 'wangwu@example.com');
});
});
describe('PUT /users/:id', function() {
it('应该更新指定用户', async function() {
const res = await request(app)
.put('/users/1')
.send({ name: '张三更新', email: 'zhangsan-updated@example.com' })
.expect('Content-Type', /json/)
.expect(200);
expect(res.body).to.have.property('id', 1);
expect(res.body).to.have.property('name', '张三更新');
expect(res.body).to.have.property('email', 'zhangsan-updated@example.com');
});
it('应该返回 404 当用户不存在', async function() {
const res = await request(app)
.put('/users/999')
.send({ name: '测试' })
.expect('Content-Type', /json/)
.expect(404);
expect(res.body).to.have.property('error', '用户不存在');
});
});
describe('DELETE /users/:id', function() {
it('应该删除指定用户', async function() {
await request(app)
.delete('/users/2')
.expect(204);
// 验证用户已删除
const res = await request(app)
.get('/users/2')
.expect(404);
expect(res.body).to.have.property('error', '用户不存在');
});
it('应该返回 404 当用户不存在', async function() {
const res = await request(app)
.delete('/users/999')
.expect('Content-Type', /json/)
.expect(404);
expect(res.body).to.have.property('error', '用户不存在');
});
});
});2.2 认证 API 测试案例
场景: 测试一个需要认证的 API
实现:
// app.js
const express = require('express');
const app = express();
app.use(express.json());
// 模拟认证中间件
function authenticate(req, res, next) {
const token = req.headers.authorization;
if (!token || !token.startsWith('Bearer ')) {
return res.status(401).json({ error: '未授权' });
}
const tokenValue = token.split(' ')[1];
if (tokenValue !== 'valid-token') {
return res.status(401).json({ error: '无效的令牌' });
}
req.user = { id: 1, name: '管理员' };
next();
}
// 公开路由
app.get('/public', (req, res) => {
res.json({ message: '公开路由' });
});
// 需要认证的路由
app.get('/protected', authenticate, (req, res) => {
res.json({ message: '受保护的路由', user: req.user });
});
module.exports = app;
// test/auth.test.js
const request = require('supertest');
const app = require('../app');
const { expect } = require('chai');
describe('认证 API 测试', function() {
describe('GET /public', function() {
it('应该可以访问公开路由', async function() {
const res = await request(app)
.get('/public')
.expect('Content-Type', /json/)
.expect(200);
expect(res.body).to.have.property('message', '公开路由');
});
});
describe('GET /protected', function() {
it('应该返回 401 当没有提供令牌', async function() {
const res = await request(app)
.get('/protected')
.expect('Content-Type', /json/)
.expect(401);
expect(res.body).to.have.property('error', '未授权');
});
it('应该返回 401 当提供无效令牌', async function() {
const res = await request(app)
.get('/protected')
.set('Authorization', 'Bearer invalid-token')
.expect('Content-Type', /json/)
.expect(401);
expect(res.body).to.have.property('error', '无效的令牌');
});
it('应该可以访问受保护路由当提供有效令牌', async function() {
const res = await request(app)
.get('/protected')
.set('Authorization', 'Bearer valid-token')
.expect('Content-Type', /json/)
.expect(200);
expect(res.body).to.have.property('message', '受保护的路由');
expect(res.body).to.have.property('user');
expect(res.body.user).to.have.property('id', 1);
expect(res.body.user).to.have.property('name', '管理员');
});
});
});2.3 错误处理测试案例
场景: 测试 API 的错误处理
实现:
// app.js
const express = require('express');
const app = express();
app.use(express.json());
// 模拟会抛出错误的路由
app.get('/error', (req, res) => {
throw new Error('服务器内部错误');
});
// 404 处理
app.use((req, res) => {
res.status(404).json({ error: '路由不存在' });
});
// 错误处理中间件
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: '服务器内部错误' });
});
module.exports = app;
// test/error.test.js
const request = require('supertest');
const app = require('../app');
const { expect } = require('chai');
describe('错误处理测试', function() {
describe('GET /error', function() {
it('应该返回 500 当服务器内部错误', async function() {
const res = await request(app)
.get('/error')
.expect('Content-Type', /json/)
.expect(500);
expect(res.body).to.have.property('error', '服务器内部错误');
});
});
describe('GET /non-existent', function() {
it('应该返回 404 当路由不存在', async function() {
const res = await request(app)
.get('/non-existent')
.expect('Content-Type', /json/)
.expect(404);
expect(res.body).to.have.property('error', '路由不存在');
});
});
describe('POST /users', function() {
it('应该返回 404 当路由不存在', async function() {
const res = await request(app)
.post('/users')
.send({ name: '测试' })
.expect('Content-Type', /json/)
.expect(404);
expect(res.body).to.have.property('error', '路由不存在');
});
});
});3. 代码示例
3.1 基本测试示例
测试 Express 应用:
const request = require('supertest');
const express = require('express');
const app = express();
app.get('/', function(req, res) {
res.status(200).json({ message: 'Hello, World!' });
});
app.get('/users', function(req, res) {
res.status(200).json([
{ id: 1, name: '张三' },
{ id: 2, name: '李四' }
]);
});
app.post('/users', function(req, res) {
res.status(201).json({ id: 3, name: req.body.name });
});
describe('Express 应用测试', function() {
it('GET / 应该返回 200 OK', function(done) {
request(app)
.get('/')
.expect('Content-Type', /json/)
.expect(200)
.expect({ message: 'Hello, World!' })
.end(done);
});
it('GET /users 应该返回用户列表', function(done) {
request(app)
.get('/users')
.expect('Content-Type', /json/)
.expect(200)
.end(function(err, res) {
if (err) return done(err);
if (res.body.length !== 2) {
return done(new Error(`期望 2 个用户,但得到 ${res.body.length} 个`));
}
done();
});
});
it('POST /users 应该创建新用户', function(done) {
request(app)
.post('/users')
.send({ name: '王五' })
.expect('Content-Type', /json/)
.expect(201)
.expect({ id: 3, name: '王五' })
.end(done);
});
});3.2 高级测试示例
测试文件上传:
const request = require('supertest');
const express = require('express');
const multer = require('multer');
const path = require('path');
const app = express();
// 配置 multer
const storage = multer.diskStorage({
destination: function(req, file, cb) {
cb(null, 'uploads/');
},
filename: function(req, file, cb) {
cb(null, Date.now() + '-' + file.originalname);
}
});
const upload = multer({ storage: storage });
// 文件上传路由
app.post('/upload', upload.single('file'), function(req, res) {
if (!req.file) {
return res.status(400).json({ error: '没有文件被上传' });
}
res.status(200).json({ filename: req.file.filename });
});
// 测试文件上传
const fs = require('fs');
const { expect } = require('chai');
describe('文件上传测试', function() {
// 创建测试文件
before(function(done) {
fs.writeFile('test-file.txt', '测试文件内容', done);
});
// 清理测试文件
after(function(done) {
fs.unlink('test-file.txt', done);
});
it('应该上传文件成功', function(done) {
request(app)
.post('/upload')
.attach('file', path.resolve(__dirname, 'test-file.txt'))
.expect('Content-Type', /json/)
.expect(200)
.end(function(err, res) {
if (err) return done(err);
expect(res.body).to.have.property('filename');
// 清理上传的文件
fs.unlink(path.resolve(__dirname, 'uploads', res.body.filename), done);
});
});
it('应该返回 400 当没有上传文件', function(done) {
request(app)
.post('/upload')
.expect('Content-Type', /json/)
.expect(400)
.end(function(err, res) {
if (err) return done(err);
expect(res.body).to.have.property('error', '没有文件被上传');
done();
});
});
});3.3 与测试框架集成
与 Mocha 和 Chai 集成:
const request = require('supertest');
const express = require('express');
const { expect } = require('chai');
const app = express();
app.use(express.json());
let books = [
{ id: 1, title: 'JavaScript 权威指南', author: 'David Flanagan' },
{ id: 2, title: 'Node.js 实战', author: 'Mike Cantelon' }
];
app.get('/books', function(req, res) {
res.json(books);
});
app.get('/books/:id', function(req, res) {
const book = books.find(b => b.id === parseInt(req.params.id));
if (!book) return res.status(404).json({ error: '书籍不存在' });
res.json(book);
});
app.post('/books', function(req, res) {
const newBook = {
id: books.length + 1,
title: req.body.title,
author: req.body.author
};
books.push(newBook);
res.status(201).json(newBook);
});
describe('书籍 API 测试', function() {
describe('GET /books', function() {
it('应该返回书籍列表', async function() {
const res = await request(app)
.get('/books')
.expect('Content-Type', /json/)
.expect(200);
expect(res.body).to.be.an('array');
expect(res.body.length).to.equal(2);
expect(res.body[0]).to.have.property('title', 'JavaScript 权威指南');
});
});
describe('GET /books/:id', function() {
it('应该返回指定书籍', async function() {
const res = await request(app)
.get('/books/1')
.expect('Content-Type', /json/)
.expect(200);
expect(res.body).to.have.property('id', 1);
expect(res.body).to.have.property('title', 'JavaScript 权威指南');
});
it('应该返回 404 当书籍不存在', async function() {
const res = await request(app)
.get('/books/999')
.expect('Content-Type', /json/)
.expect(404);
expect(res.body).to.have.property('error', '书籍不存在');
});
});
describe('POST /books', function() {
it('应该创建新书籍', async function() {
const res = await request(app)
.post('/books')
.send({ title: 'React 实战', author: 'Alex Banks' })
.expect('Content-Type', /json/)
.expect(201);
expect(res.body).to.have.property('id', 3);
expect(res.body).to.have.property('title', 'React 实战');
expect(res.body).to.have.property('author', 'Alex Banks');
});
});
});3.4 测试 Koa 应用
测试 Koa 应用:
const request = require('supertest');
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const app = new Koa();
app.use(bodyParser());
// 中间件
app.use(async (ctx) => {
if (ctx.path === '/' && ctx.method === 'GET') {
ctx.status = 200;
ctx.body = { message: 'Hello, Koa!' };
} else if (ctx.path === '/users' && ctx.method === 'GET') {
ctx.status = 200;
ctx.body = [{ id: 1, name: '张三' }];
} else if (ctx.path === '/users' && ctx.method === 'POST') {
ctx.status = 201;
ctx.body = { id: 2, name: ctx.request.body.name };
} else {
ctx.status = 404;
ctx.body = { error: '路由不存在' };
}
});
// 转换 Koa 应用为 Node.js HTTP 服务器
const server = app.listen();
describe('Koa 应用测试', function() {
after(function() {
server.close();
});
it('GET / 应该返回 200 OK', function(done) {
request(server)
.get('/')
.expect('Content-Type', /json/)
.expect(200)
.expect({ message: 'Hello, Koa!' })
.end(done);
});
it('GET /users 应该返回用户列表', function(done) {
request(server)
.get('/users')
.expect('Content-Type', /json/)
.expect(200)
.end(function(err, res) {
if (err) return done(err);
if (res.body.length !== 1) {
return done(new Error(`期望 1 个用户,但得到 ${res.body.length} 个`));
}
done();
});
});
it('POST /users 应该创建新用户', function(done) {
request(server)
.post('/users')
.send({ name: '李四' })
.expect('Content-Type', /json/)
.expect(201)
.expect({ id: 2, name: '李四' })
.end(done);
});
it('GET /non-existent 应该返回 404', function(done) {
request(server)
.get('/non-existent')
.expect('Content-Type', /json/)
.expect(404)
.expect({ error: '路由不存在' })
.end(done);
});
});4. 总结
Supertest 是一个功能强大的 HTTP 测试库,专门用于测试 Node.js HTTP 服务器。本教程介绍了 Supertest 的核心概念,包括:
- 安装和配置
- 基本测试结构
- HTTP 方法测试
- 请求配置
- 响应断言
- 异步测试支持
- 文件上传测试
- 错误处理测试
通过学习这些概念,你可以开始在项目中使用 Supertest 编写 HTTP API 测试,确保 API 的正确性和可靠性。Supertest 的简洁、直观的 API 使得测试编写变得简单而高效,无论是测试 Express、Koa 还是其他 Node.js HTTP 服务器。
5. 进一步学习资源
希望本教程对你学习 Supertest 有所帮助!