1. Egg.js 单元测试与部署实战指南
作为一名长期使用Egg.js开发企业级应用的工程师,我深知单元测试和应用部署是项目落地过程中最容易被忽视却又至关重要的环节。本文将结合我在多个生产项目中的实践经验,详细拆解Egg.js的测试体系与部署方案,带你避开那些新手常踩的坑。
2. 单元测试体系构建
2.1 测试环境搭建与规范
在Egg.js项目中,测试不是可选项而是必选项。我们团队强制要求测试覆盖率必须达到80%以上,这是保证代码质量的基本防线。测试目录结构遵循官方约定:
plaintext复制test/
├── controller/ # 控制器测试
├── service/ # 业务逻辑测试
├── extend/ # 扩展功能测试
└── middleware/ # 中间件测试(建议补充)
测试文件命名采用${filename}.test.js格式,例如user.service.test.js。这种命名方式能快速建立文件关联,当你在IDE中看到测试文件时能立即知道它对应哪个功能模块。
重要提示:永远不要把测试文件散落在项目各处,集中管理能显著降低维护成本。我曾接手过一个测试文件分散的项目,光是定位测试用例就浪费了大量时间。
Egg-bin作为官方测试工具链,已经集成了我们需要的所有工具:
- Mocha:测试框架
- power-assert:断言库
- nyc:覆盖率统计
- egg-mock:模拟工具
配置方式极其简单,只需在package.json中添加:
json复制{
"scripts": {
"test": "egg-bin test",
"cov": "egg-bin cov" // 覆盖率测试
}
}
2.2 测试准备与最佳实践
2.2.1 应用实例管理
测试中最关键的是处理好应用生命周期。egg-mock提供了两种创建方式:
javascript复制// 传统方式(适合复杂场景)
const mock = require('egg-mock');
describe('test', () => {
let app;
before(() => {
app = mock.app({
cache: false // 禁用缓存确保测试隔离
});
return app.ready();
});
after(() => app.close());
});
// 推荐方式(90%场景适用)
const { app, mock, assert } = require('egg-mock/bootstrap');
我曾遇到过一个典型问题:测试用例间相互污染。原因是开发者直接在describe块中执行初始化逻辑:
javascript复制// 错误示例!
describe('user', () => {
initDatabase(); // 这个操作会在所有用例执行前运行一次
it('case1', () => {});
it('case2', () => {});
});
正确做法是使用钩子函数控制执行时机:
javascript复制describe('user', () => {
before(() => initDatabase()); // 当前describe块开始前执行
beforeEach(() => clearData()); // 每个it执行前运行
afterEach(() => resetMock()); // 每个it执行后清理
after(() => dropDatabase()); // 当前describe结束后执行
it('case1', () => {});
});
2.2.2 异步测试方案
Egg.js应用中异步操作无处不在,测试时需要特别注意:
javascript复制// 1. Promise链式调用(适合简单场景)
it('should work', () => {
return app.httpRequest()
.get('/api/users')
.expect(200);
});
// 2. 回调函数(不推荐,容易陷入回调地狱)
it('should work', (done) => {
app.httpRequest()
.get('/api/users')
.expect(200, done);
});
// 3. async/await(最佳实践)
it('should work', async () => {
const res = await app.httpRequest()
.get('/api/users')
.expect(200);
assert(res.body.data.length > 0);
});
在实际项目中,我们统一采用async/await写法,代码可读性最好。特别是测试Service层时,await能完美处理异步数据操作:
javascript复制it('should create user', async () => {
const ctx = app.mockContext();
const user = await ctx.service.user.create({
name: 'test',
password: '123456'
});
assert(user.id);
assert(user.createdAt);
});
2.3 分层测试策略
2.3.1 Controller测试要点
Controller测试的核心是验证HTTP接口行为:
javascript复制describe('POST /api/users', () => {
it('should create user', async () => {
app.mockCsrf(); // 必须模拟CSRF
await app.httpRequest()
.post('/api/users')
.send({
name: 'test',
password: '123456'
})
.expect(201)
.expect(res => {
assert(res.body.id);
assert(res.body.name === 'test');
});
});
it('should reject duplicate name', async () => {
app.mockCsrf();
await app.httpRequest()
.post('/api/users')
.send({ name: 'admin' }) // 假设admin已存在
.expect(422);
});
});
经验之谈:Controller测试应该聚焦于输入输出验证,不要过度测试业务逻辑,那是Service层的职责。我曾见过有人把各种边界条件都放在Controller测试,导致测试用例臃肿且难以维护。
2.3.2 Service测试技巧
Service是业务逻辑的核心载体,测试时要特别注意:
javascript复制describe('user service', () => {
let ctx;
beforeEach(() => {
ctx = app.mockContext({
// 模拟登录用户
user: { id: 1, role: 'admin' }
});
});
it('should find user by id', async () => {
const user = await ctx.service.user.find(1);
assert(user);
assert(user.id === 1);
});
it('should throw when not found', async () => {
try {
await ctx.service.user.find(999);
assert.fail('should throw error');
} catch (err) {
assert(err.message === 'user not found');
}
});
});
2.3.3 扩展功能测试
| 扩展类型 | 测试方法 | 示例 |
|---|---|---|
| Application | 直接访问app实例 | app.myExtendMethod() |
| Context | 通过mockContext创建 | ctx.myExtendProp |
| Helper | 通过ctx.helper访问 | ctx.helper.formatDate() |
| Request/Response | 通过ctx获取 | ctx.request.isMobile() |
javascript复制describe('application extend', () => {
it('should have cache property', () => {
assert(app.cache);
});
});
describe('helper', () => {
it('should format date', () => {
const ctx = app.mockContext();
const str = ctx.helper.formatDate(new Date('2023-01-01'));
assert(str === '2023-01-01');
});
});
2.4 Mock高级技巧
2.4.1 常用Mock场景
javascript复制// 模拟Session
app.mockSession({
userId: 1,
role: 'admin'
});
// 模拟Service返回
app.mockService('user', 'find', () => ({
id: 1,
name: 'mock user'
}));
// 模拟Service异常
app.mockServiceError('user', 'find', new Error('mock error'));
// 模拟HTTP请求
app.mockHttpclient('https://api.example.com', {
status: 200,
data: { foo: 'bar' }
});
2.4.2 Mock的陷阱与规避
-
过度Mock问题:不要Mock所有依赖,否则测试将失去意义。我们团队遵循"只Mock外部依赖"原则:
- Mock第三方API调用
- Mock数据库等I/O操作
- 不Mock项目内部模块
-
Mock泄漏问题:确保每个测试用例执行后清理Mock状态:
javascript复制afterEach(mock.restore); // 手动清理
// 或者使用egg-mock/bootstrap自动清理
- Mock时效问题:当被Mock的模块发生变化时,记得更新Mock数据。我们会在CI流程中加入Mock校验步骤。
3. 应用部署方案
3.1 构建与打包
生产环境部署前需要先构建:
bash复制# 安装生产依赖(不安装devDependencies)
npm install --production
# 打包(排除测试文件和开发配置)
tar --exclude=test \
--exclude=.vscode \
--exclude=.eslintrc \
-zcvf ../release.tgz .
避坑指南:千万不要把.env等敏感文件打包进去!我们曾经因此泄露过数据库密码。建议使用
--exclude明确排除敏感文件。
3.2 进程管理
Egg-scripts是官方推荐的进程管理工具,配置方式:
bash复制npm install egg-scripts --save
package.json配置:
json复制{
"scripts": {
"start": "egg-scripts start --daemon --title=egg-server-app",
"stop": "egg-scripts stop --title=egg-server-app",
"restart": "egg-scripts restart --daemon --title=egg-server-app"
}
}
关键参数说明:
--daemon:后台运行模式(Docker环境不要使用)--title:进程标识,便于管理--workers=4:指定Worker数量(默认CPU核数)--port=7001:指定端口(覆盖config配置)
3.3 监控与诊断
3.3.1 基础监控
bash复制# 安装alinode运行时
npm install nodeinstall -g
nodeinstall --install-alinode ^3
# 安装插件
npm install egg-alinode --save
配置插件:
javascript复制// config/plugin.js
exports.alinode = {
enable: true,
package: 'egg-alinode'
};
// config/config.default.js
exports.alinode = {
appid: '你的APPID',
secret: '你的SECRET',
logdir: '/path/to/log', // 自定义日志目录
error_log: [
'/path/to/app/stderr.log',
'/path/to/egg-stderr.log'
],
packages: [
'/path/to/package.json'
]
};
3.3.2 高级诊断
- 性能分析:
bash复制# 生成CPU profile
kill -USR1 <master_pid>
# 生成Heap snapshot
kill -USR2 <master_pid>
- 内存泄漏排查:
javascript复制// 定期记录内存使用
setInterval(() => {
const usage = process.memoryUsage();
console.log(`Memory: ${usage.rss / 1024 / 1024} MB`);
}, 10000);
- 慢查询监控:
javascript复制// config/config.default.js
exports.sequelize = {
dialectOptions: {
benchmark: true, // 记录执行时间
logging: (sql, timing) => {
if (timing > 1000) { // 超过1秒的查询
logger.warn(`Slow query(${timing}ms): ${sql}`);
}
}
}
};
3.4 部署实战经验
3.4.1 多环境配置
我们采用以下环境划分:
- 开发环境(development)
- 测试环境(unittest)
- 预发布环境(preview)
- 生产环境(production)
配置方式:
javascript复制// config/config.${env}.js
module.exports = appInfo => ({
// 环境专属配置
cluster: {
listen: {
port: process.env.PORT || 7001
}
}
});
启动时指定环境:
bash复制EGG_SERVER_ENV=production npm start
3.4.2 容器化部署
Dockerfile示例:
dockerfile复制FROM node:14-alpine
WORKDIR /app
# 先安装依赖(利用层缓存)
COPY package.json .
RUN npm install --production
# 复制应用代码
COPY . .
# 设置环境变量
ENV EGG_SERVER_ENV=production
ENV NODE_ENV=production
EXPOSE 7001
CMD ["npm", "start"]
关键优化点:
- 使用Alpine基础镜像减小体积
- 多阶段构建进一步优化
- 非root用户运行增强安全
3.4.3 灰度发布方案
我们采用的灰度策略:
- 通过Nginx流量切分
- 基于Cookie的版本标记
- 逐步扩大新版本流量比例
Nginx配置示例:
nginx复制upstream egg_app {
server 127.0.0.1:7001;
server 127.0.0.1:7002 backup;
}
split_clients "${remote_addr}AAA" $variant {
50% "v2";
50% "v1";
}
server {
location / {
if ($cookie_version = "v2") {
proxy_pass http://127.0.0.1:7002;
}
if ($variant = "v2") {
add_header Set-Cookie "version=v2;Path=/;Max-Age=3600";
proxy_pass http://127.0.0.1:7002;
}
proxy_pass http://egg_app;
}
}
4. 常见问题排查
4.1 测试相关问题
问题1:测试时报"app not ready"
- 原因:未正确等待app.ready()
- 解决:
javascript复制before(async () => {
app = mock.app();
await app.ready(); // 必须等待
});
问题2:Mock不生效
- 原因:可能被其他测试用例清理
- 解决:检查afterEach钩子,确保没有提前调用mock.restore()
问题3:测试超时
- 原因:默认超时时间(5000ms)不足
- 解决:
javascript复制describe('slow test', function() {
this.timeout(30000); // 设置30秒超时
it('should work', async () => {
// 慢测试逻辑
});
});
4.2 部署相关问题
问题1:启动后立即退出
- 检查:
npm start是否添加了--daemon参数(Docker中不能使用) - 检查:端口是否被占用
- 检查:Node版本是否符合要求
问题2:Worker频繁重启
- 可能原因:内存泄漏
- 排查:
bash复制# 查看内存增长情况 alinode top # 生成堆快照分析 alinode heapdump
问题3:性能突然下降
- 排查步骤:
- 检查系统负载:
top/htop - 检查Node进程CPU:
alinode top - 检查数据库慢查询
- 检查外部API响应时间
- 检查系统负载:
4.3 监控指标解读
| 关键监控指标 | 健康范围 | 异常处理 |
|---|---|---|
| CPU使用率 | <70% | 检查热点函数 |
| 内存使用 | 无持续增长 | 排查内存泄漏 |
| 事件循环延迟 | <100ms | 优化同步阻塞代码 |
| HTTP QPS | 符合预期 | 检查流量异常 |
| 错误率 | <0.5% | 分析错误日志 |
5. 性能优化建议
5.1 测试优化
- 并行测试:
bash复制# 使用mocha-parallel加速测试
npm install mocha-parallel --save-dev
"scripts": {
"test": "mocha-parallel -t 5000 test/**/*.test.js"
}
- 测试分组:
bash复制# 只运行修改文件的测试
npm test -- --grep "controller/user"
# 排除慢测试
npm test -- --invert --grep "slow"
5.2 部署优化
- 启动加速:
javascript复制// config/config.default.js
exports.workerStartTimeout = 60000; // 延长Worker启动超时
- 内存优化:
bash复制# 调整Node内存限制
NODE_OPTIONS="--max-old-space-size=4096" npm start
- 日志优化:
javascript复制exports.logger = {
level: 'INFO', // 生产环境用INFO
consoleLevel: 'WARN' // 控制台只输出警告以上
};
5.3 监控增强
- 自定义指标:
javascript复制// app.js
class AppBootHook {
async didReady() {
const metrics = app.metrics;
setInterval(() => {
metrics.set('memory', process.memoryUsage().rss);
}, 5000);
}
}
- 告警配置:
javascript复制exports.alinode = {
// ...
alarm: {
enable: true,
rules: [{
type: 'cpu',
threshold: 85,
duration: 5
}]
}
};
在实际项目中,完善的测试体系和稳健的部署方案能节省大量后期维护成本。我建议在项目初期就建立这些规范,而不是等到出现问题后再补救。记住:好的工程实践不是负担,而是提高开发效率的利器。