🎬 为什么需要自动化测试?
凌晨2点56分,我部署了一个新 Skill,结果导致整个 Agent 崩溃——因为我忘了测试边界情况。
就像周星驰电影《破坏之王》里的"输了就要变女人"——没测试就上线,后果很严重。自动化测试就是你的"必胜客",让你稳稳地赢。
🔍 测试金字塔
/\
/ \ E2E测试 (少量,关键路径)
/____\
/ \ 集成测试 (中等,API接口)
/__________\ 单元测试 (大量,函数级别)
/____________\
- 单元测试:测试单个函数/模块(快速、隔离)
- 集成测试:测试模块间交互(API、数据库)
- E2E测试:测试完整用户流程(慢速、真实)
🧪 单元测试(Unit Test)
配置 Jest(Node.js)
# 安装依赖
npm install --save-dev jest @types/jest
# package.json 配置
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
},
"jest": {
"testEnvironment": "node",
"coveragePathIgnorePatterns": ["/node_modules/", "/test/"]
}
}
编写单元测试
// math.js - 被测试模块
function add(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('Both arguments must be numbers');
}
return a + b;
}
module.exports = { add };
// test/math.test.js - 测试文件
const { add } = require('../math.js');
describe('add function', () => {
test('adds two numbers correctly', () => {
expect(add(2, 3)).toBe(5);
expect(add(-1, 1)).toBe(0);
expect(add(0.1, 0.2)).toBeCloseTo(0.3);
});
test('throws error for non-numbers', () => {
expect(() => add('2', 3)).toThrow('Both arguments must be numbers');
expect(() => add(2, '3')).toThrow('Both arguments must be numbers');
expect(() => add(null, undefined)).toThrow();
});
});
// 运行测试
npm test
// 输出:
// PASS test/math.test.js
// ✓ adds two numbers correctly (5 ms)
// ✓ throws error for non-numbers (3 ms)
// Test Suites: 1 passed, 1 total
// Tests: 2 passed, 2 total
💡 最佳实践: 单元测试要"快、独立、可重复"。每个测试只测一件事,不依赖外部状态。
🔗 集成测试(Integration Test)
测试 API 接口
// test/api.test.js
const request = require('supertest');
const app = require('../app.js'); // Express app
describe('API Integration Tests', () => {
test('GET /api/skills returns list', async () => {
const response = await request(app)
.get('/api/skills')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toBeInstanceOf(Array);
expect(response.body.length).toBeGreaterThan(0);
});
test('POST /api/skills creates new skill', async () => {
const newSkill = {
name: 'test-skill',
description: 'A test skill'
};
const response = await request(app)
.post('/api/skills')
.send(newSkill)
.expect(201);
expect(response.body.name).toBe('test-skill');
});
});
测试数据库交互
// test/db.test.js
const db = require('../db.js');
describe('Database Integration', () => {
beforeAll(async () => {
await db.connect(); // 连接测试数据库
});
afterAll(async () => {
await db.disconnect();
});
test('inserts and retrieves record', async () => {
const record = { key: 'test', value: '123' };
await db.insert('test_table', record);
const result = await db.query('SELECT * FROM test_table WHERE key = ?', ['test']);
expect(result[0].value).toBe('123');
});
});
🌐 E2E 测试(End-to-End)
使用 Puppeteer 测试 Web UI
# 安装 Puppeteer
npm install --save-dev puppeteer
// test/e2e.spec.js
const puppeteer = require('puppeteer');
describe('E2E: OpenClaw Web UI', () => {
let browser, page;
beforeAll(async () => {
browser = await puppeteer.launch({ headless: true });
page = await browser.newPage();
});
afterAll(async () => {
await browser.close();
});
test('user can generate image', async () => {
await page.goto('https://miaoquai.com/tools/openclaw-skills-...');
// 输入 prompt
await page.type('#prompt-input', 'a cat on moon');
// 点击生成按钮
await page.click('#generate-btn');
// 等待结果
await page.waitForSelector('#result-image', { timeout: 30000 });
// 验证结果
const imageSrc = await page.$eval('#result-image', img => img.src);
expect(imageSrc).toMatch(/^https?:\/\//);
}, 60000); // 60秒超时
});
🚀 CI/CD 配置(GitHub Actions)
# .github/workflows/ci.yml
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x, 22.x]
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linter
run: npm run lint
- name: Run unit tests
run: npm run test:coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/coverage-final.json
- name: Run integration tests
run: npm run test:integration
env:
DATABASE_URL: ${{ secrets.TEST_DB_URL }}
build:
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Build
run: |
npm ci
npm run build
- name: Deploy to production
uses: easingthemes/ssh-deploy@main
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
remote-host: ${{ secrets.DEPLOY_HOST }}
source: "dist/"
target: "/var/www/miaoquai/"
🎯 CI/CD 流程说明
- 代码提交 → 触发 GitHub Actions
- 多版本测试 → Node 18/20/22 都跑一遍
- 代码检查 → ESLint 检查代码质量
- 单元测试 → Jest 跑测试 + 覆盖率
- 集成测试 → 测试数据库、API 等
- 构建部署 → 测试通过后自动部署
📊 测试覆盖率目标
# 查看覆盖率报告
npm run test:coverage
# 输出示例:
# ----------|---------|----------|---------|---------
# File | % Stmts | % Branch | % Funcs | % Lines
# ----------|---------|----------|---------|---------
# math.js | 100 | 80 | 100 | 100
# api.js | 95 | 85 | 90 | 95
# db.js | 88 | 75 | 85 | 88
# ----------|---------|----------|---------|---------
# All files | 94 | 80 | 92 | 94
# 配置覆盖率门槛(package.json)
{
"jest": {
"coverageThreshold": {
"global": {
"statements": 80,
"branches": 70,
"functions": 80,
"lines": 80
}
}
}
}
💡 覆盖率建议: 单元测试 80%+,核心模块 90%+,集成测试覆盖关键流程。
🌟 总结
凌晨3点37分,我看着 CI/CD 绿灯亮起,心里踏实了——代码再也不会"裸奔"上线。
你已经学会了:
- ✅ 单元测试编写(Jest)
- ✅ 集成测试(API、数据库)
- ✅ E2E 测试(Puppeteer)
- ✅ CI/CD 配置(GitHub Actions)
- ✅ 测试覆盖率管理
记住:测试不是"额外工作",是代码质量的"保险丝"。