作为一名长期奋战在一线的Node.js开发者,我深知Egg.js作为企业级框架在生产环境中的重要性。今天要分享的是我在实际项目中积累的Egg.js两大核心技能:单元测试和应用部署。这两个环节直接关系到代码质量和线上稳定性,是每个Egg.js开发者必须掌握的硬核技能。
在过去的三年里,我主导过7个基于Egg.js的中大型项目,踩过无数坑后总结出这套实战经验。不同于官方文档的理论说明,本文将聚焦真实项目场景,手把手带你解决以下问题:
在电商项目"双十一"大促前,我们通过单元测试发现了3个可能导致订单丢失的严重Bug。这让我深刻认识到,完善的测试体系是线上稳定的第一道防线。
推荐使用以下黄金组合:
bash复制# package.json关键配置
{
"scripts": {
"test": "egg-bin test",
"cov": "egg-bin cov"
},
"devDependencies": {
"egg-mock": "^3.21.0",
"power-assert": "^1.6.1"
}
}
特别注意:
egg-bin而非原生mocha命令,它能自动加载Egg环境power-assert的断言错误信息更直观,适合复杂对象校验test/目录,命名规范为*.test.js在金融项目中我们要求核心模块覆盖率达到90%,这是通过以下策略实现的:
分层覆盖策略:
覆盖率检查命令:
bash复制# 生成lcov报告
npm run cov
# 检查特定目录覆盖率
COV_EXCLUDES="app/utils/*" npm run cov
经验:不要盲目追求100%覆盖率,重点保证核心业务路径和异常场景。我们曾因过度追求覆盖率导致测试代码比业务代码还多,维护成本激增。
在用户中心项目中,我们总结出接口测试四要素:
javascript复制describe('POST /api/users', () => {
it('创建用户应返回201状态码', async () => {
const app = mock.app();
await app.ready();
const res = await app.httpRequest()
.post('/api/users')
.send({
username: 'test001',
password: 'Abcd1234'
})
.expect(201);
assert(res.body.code === 0);
assert(res.body.data.userId);
});
it('重复用户名应返回400错误', async () => {
// 先创建测试用户
await app.httpRequest().post('/api/users').send({...});
// 测试重复创建
const res = await app.httpRequest()
.post('/api/users')
.send({...})
.expect(400);
assert(res.body.message.includes('已存在'));
});
});
支付服务的单元测试曾帮我们提前发现金额计算错误,避免百万损失。关键要点:
app.mockContext()模拟请求上下文javascript复制describe('PaymentService', () => {
let ctx;
let service;
before(() => {
ctx = app.mockContext({
// 模拟登录用户
user: { id: 1001 }
});
service = ctx.service.payment;
});
it('跨境支付应自动计算汇率', async () => {
// Mock汇率接口
app.mockHttpclient('https://api.exchange.com', {
data: { rate: 6.5 }
});
const result = await service.createOrder({
amount: 100,
currency: 'USD'
});
assert(result.cnyAmount === 650);
});
});
在对接第三方服务时,我们开发了分层Mock策略:
javascript复制app.mockHttpclient('https://api.sms.com', {
status: 200,
data: { success: true }
});
javascript复制app.mockClassFunction('aliyunSdk', 'sendSms', () => {
return { Code: 'OK' };
});
javascript复制app.mockService('sms', 'sendVerification', () => {
return { status: 'sent' };
});
我们内部开发了egg-mock-mongoose插件,支持:
javascript复制describe('OrderService', () => {
before(async () => {
// 加载测试数据
await app.mockMongoose.load('orders');
});
it('创建订单应扣减库存', async () => {
const before = await app.mongoose.model('Stock').findOne();
await service.createOrder(...);
const after = await app.mongoose.model('Stock').findOne();
assert(before.count - after.count === 1);
});
});
在容器化部署实践中,我们形成了"三环境五步骤"标准流程:
code复制构建机(Build) -> 测试环境(Test) -> 预发环境(Stage) -> 生产环境(Prod)
bash复制#!/bin/bash
# build.sh
# 1. 安装生产依赖
npm install --production
# 2. 如果是TypeScript项目需要编译
npm run build
# 3. 生成版本指纹
git rev-parse HEAD > .version
# 4. 打包(排除开发配置文件)
tar --exclude='*.dev.*' \
--exclude='.env.local' \
-zcvf ../release.tgz .
通过压力测试我们发现,Worker数量不是越多越好。最佳实践是:
code复制Worker数 = CPU核心数 * (1 + I/O等待系数)
其中I/O等待系数根据API特性调整:
javascript复制// config/config.prod.js
exports.cluster = {
workers: process.env.WORKERS || 4, // 默认4个worker
listen: {
port: 7001,
hostname: '127.0.0.1' // 通过Nginx暴露外网
}
};
在百万级QPS的直播项目中,我们实现了平滑重启:
bash复制# 1. 启动时预加载
egg-scripts start --daemon --preload
# 2. 灰度发布脚本
#!/bin/bash
# 逐个worker重启
for i in {1..4}; do
kill -USR2 `cat /run/egg.pid`
sleep 5 # 间隔5秒
done
除了Alinode的基础监控,我们还添加了业务指标:
javascript复制// app.js
class AppBootHook {
async didReady() {
app.monitor = new Monitor();
// 采集QPS
setInterval(() => {
const qps = app.counter.reset();
app.monitor.report('api_qps', qps);
}, 60000);
}
}
当API错误率超过阈值时自动降级:
javascript复制// app/middleware/circuit_breaker.js
module.exports = (options) => {
return async (ctx, next) => {
if (ctx.app.circuitBreaker.isOpen()) {
return ctx.service.fallback.get();
}
try {
await next();
} catch (err) {
ctx.app.circuitBreaker.recordError();
throw err;
}
};
};
通过以下步骤定位过线上内存泄漏:
heapdump生成内存快照解决方案:
javascript复制// config/config.prod.js
exports.redis = {
client: {
maxAge: 3600000 // 1小时缓存
}
};
某次大促前接口响应从200ms优化到50ms的关键措施:
async_hooks定位慢查询优化前后对比:
markdown复制| 优化措施 | QPS提升 | 平均耗时下降 |
|-------------------|---------|--------------|
| 并行IO | 40% | 35% |
| 缓存命中率提升 | 25% | 50% |
| SQL优化 | 15% | 20% |
我们制定的服务稳定性SLA:
实现方式:
使用factory-girl创建测试数据:
javascript复制// test/factories/user.js
factory.define('user', app.model.User, {
username: factory.sequence('User.username', n => `user_${n}`),
status: 'active'
});
// 在测试中使用
const user = await factory.create('user', {
role: 'admin'
});
使用pactum做契约测试确保接口兼容性:
javascript复制const { spec } = require('pactum');
it('获取用户列表应符合契约', async () => {
await spec()
.get('/api/users')
.expectStatus(200)
.expectJsonSchema({
type: 'object',
properties: {
data: {
type: 'array',
items: {
properties: {
id: { type: 'number' },
name: { type: 'string' }
}
}
}
}
});
});
使用chaos-mesh模拟线上故障:
yaml复制# chaos-experiment.yaml
apiVersion: chaos-mesh.org/v1alpha1
kind: NetworkChaos
metadata:
name: delay-request
spec:
action: delay
mode: one
selector:
namespaces: [ "production" ]
delay:
latency: "500ms"
correlation: "100"
jitter: "100ms"
经过这些年的实践,我认为Egg.js最强大的不是框架本身,而是这套完整的工程化体系。从代码编写到线上运维形成闭环,这才是企业级开发的精髓所在。