与测试框架(Jest/Mocha)
测试框架(Jest/Mocha)在TypeScript中的使用
TypeScript项目中选择合适的测试框架对代码质量至关重要。Jest和Mocha是两个主流选择,它们各有特点但都能很好地与TypeScript集成。
Jest的基本配置与使用
Jest是Facebook开发的测试框架,内置断言库和mock功能。在TypeScript项目中使用需要安装相关依赖:
npm install --save-dev jest ts-jest @types/jest
配置jest.config.js
:
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/*.test.ts'],
moduleFileExtensions: ['ts', 'js', 'json'],
};
编写测试示例:
// math.test.ts
import { add } from './math';
describe('math operations', () => {
it('adds two numbers correctly', () => {
expect(add(1, 2)).toBe(3);
});
it('handles decimal numbers', () => {
expect(add(0.1, 0.2)).toBeCloseTo(0.3);
});
});
Mocha的基本配置与使用
Mocha是更灵活的测试框架,需要配合其他库使用。安装依赖:
npm install --save-dev mocha chai @types/mocha @types/chai ts-node
配置.mocharc.json
:
{
"extension": ["ts"],
"spec": "src/**/*.test.ts",
"require": "ts-node/register"
}
测试示例:
// math.test.ts
import { expect } from 'chai';
import { multiply } from './math';
describe('math operations', () => {
it('multiplies two numbers', () => {
expect(multiply(2, 3)).to.equal(6);
});
it('returns NaN with invalid inputs', () => {
expect(multiply('a' as any, 2)).to.be.NaN;
});
});
异步测试比较
Jest处理异步测试更简洁:
// jest异步测试
test('fetches user data', async () => {
const user = await fetchUser(1);
expect(user).toHaveProperty('id', 1);
});
Mocha需要明确返回Promise或使用done回调:
// mocha异步测试
it('fetches user data', async () => {
const user = await fetchUser(1);
expect(user).to.have.property('id', 1);
});
// 或使用done回调
it('fetches user data', (done) => {
fetchUser(1).then(user => {
expect(user).to.have.property('id', 1);
done();
});
});
Mock功能的差异
Jest内置强大的mock系统:
// jest mock示例
jest.mock('./api');
import { fetchData } from './api';
test('mocks API call', async () => {
(fetchData as jest.Mock).mockResolvedValue({ data: 'mock' });
const result = await callApi();
expect(result).toEqual({ data: 'mock' });
});
Mocha需要额外库如sinon:
// mocha + sinon mock示例
import sinon from 'sinon';
import { fetchData } from './api';
describe('API tests', () => {
let fetchStub: sinon.SinonStub;
beforeEach(() => {
fetchStub = sinon.stub(fetchData).resolves({ data: 'mock' });
});
it('mocks API call', async () => {
const result = await callApi();
expect(result).to.deep.equal({ data: 'mock' });
expect(fetchStub.calledOnce).to.be.true;
});
});
测试覆盖率报告
Jest内置覆盖率工具,配置简单:
// jest.config.js
module.exports = {
collectCoverage: true,
coverageDirectory: 'coverage',
coveragePathIgnorePatterns: ['/node_modules/'],
};
Mocha需要nyc生成覆盖率:
npm install --save-dev nyc
配置package.json
:
{
"scripts": {
"test:coverage": "nyc mocha"
},
"nyc": {
"extension": [".ts"],
"include": ["src/**/*.ts"],
"reporter": ["text", "html"]
}
}
TypeScript特定测试场景
测试泛型函数:
// 测试泛型函数
function identity<T>(arg: T): T {
return arg;
}
describe('identity function', () => {
it('returns same value for number', () => {
expect(identity(42)).toBe(42);
});
it('returns same value for string', () => {
expect(identity('test')).toBe('test');
});
});
测试类方法:
class Calculator {
private value: number;
constructor(initialValue = 0) {
this.value = initialValue;
}
add(num: number): this {
this.value += num;
return this;
}
getResult(): number {
return this.value;
}
}
describe('Calculator', () => {
let calc: Calculator;
beforeEach(() => {
calc = new Calculator();
});
it('chains methods correctly', () => {
const result = calc.add(5).add(3).getResult();
expect(result).toBe(8);
});
});
测试React组件
Jest配合@testing-library/react测试React组件:
import React from 'react';
import { render, screen } from '@testing-library/react';
import Button from './Button';
describe('Button component', () => {
it('renders with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('handles click events', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click</Button>);
fireEvent.click(screen.getByText('Click'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
性能测试对比
Jest性能测试示例:
test('performance test', () => {
const start = performance.now();
heavyCalculation();
const end = performance.now();
expect(end - start).toBeLessThan(100); // 应在100ms内完成
});
Mocha性能测试通常使用benchmark.js:
import Benchmark from 'benchmark';
describe('Performance', () => {
it('measures function speed', function() {
this.timeout(10000); // 延长超时时间
const suite = new Benchmark.Suite;
suite.add('heavyCalculation', () => heavyCalculation())
.on('cycle', (event: Benchmark.Event) => {
console.log(String(event.target));
})
.run();
});
});
自定义断言扩展
Jest添加自定义匹配器:
expect.extend({
toBeWithinRange(received, floor, ceiling) {
const pass = received >= floor && received <= ceiling;
return {
message: () => `expected ${received} ${pass ? 'not ' : ''}to be within ${floor}-${ceiling}`,
pass,
};
},
});
test('custom matcher', () => {
expect(10).toBeWithinRange(5, 15);
});
Mocha使用chai插件:
import chai from 'chai';
chai.use(function(chai, utils) {
const Assertion = chai.Assertion;
Assertion.addMethod('within', function(floor, ceiling) {
this.assert(
this._obj >= floor && this._obj <= ceiling,
`expected #{this} to be within ${floor} and ${ceiling}`,
`expected #{this} not to be within ${floor} and ${ceiling}`
);
});
});
it('custom assertion', () => {
expect(10).to.be.within(5, 15);
});
测试环境清理
Jest的清理钩子:
let db: Database;
beforeAll(async () => {
db = await connectToTestDB();
});
afterAll(async () => {
await db.close();
});
afterEach(async () => {
await db.clear();
});
Mocha的清理方式:
let db: Database;
before(async () => {
db = await connectToTestDB();
});
after(async () => {
await db.close();
});
afterEach(async () => {
await db.clear();
});
测试HTTP接口
Jest测试Express接口:
import request from 'supertest';
import app from '../app';
describe('GET /users', () => {
it('responds with json', async () => {
const response = await request(app)
.get('/users')
.expect('Content-Type', /json/)
.expect(200);
expect(response.body).toHaveLength(3);
});
});
Mocha测试Koa接口:
import request from 'supertest';
import app from '../app';
describe('GET /posts', () => {
it('returns posts array', async () => {
const res = await request(app.callback())
.get('/posts')
.expect(200);
expect(res.body).to.be.an('array');
});
});
测试错误处理
测试抛出错误:
function divide(a: number, b: number): number {
if (b === 0) throw new Error('Cannot divide by zero');
return a / b;
}
// Jest方式
test('throws error when dividing by zero', () => {
expect(() => divide(1, 0)).toThrow('Cannot divide by zero');
});
// Mocha方式
it('throws error when dividing by zero', () => {
expect(() => divide(1, 0)).to.throw('Cannot divide by zero');
});
测试配置管理
Jest环境变量管理:
// jest.config.js
module.exports = {
setupFiles: ['<rootDir>/tests/setupEnv.js'],
};
// tests/setupEnv.js
process.env.NODE_ENV = 'test';
process.env.API_URL = 'http://test.api';
Mocha环境配置:
// tests/setup.ts
import dotenv from 'dotenv';
dotenv.config({ path: '.env.test' });
// .mocharc.json
{
"require": ["ts-node/register", "./tests/setup.ts"]
}
测试数据库操作
使用内存数据库测试:
import { User } from '../models';
import { initTestDB, closeTestDB } from '../test-utils';
describe('User model', () => {
beforeAll(async () => {
await initTestDB();
});
afterAll(async () => {
await closeTestDB();
});
it('creates a user', async () => {
const user = await User.create({ name: 'Test' });
expect(user.id).toBeDefined();
});
});
测试时间相关代码
Jest模拟定时器:
jest.useFakeTimers();
test('calls callback after delay', () => {
const callback = jest.fn();
delayedCallback(callback, 1000);
jest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalled();
});
Mocha使用sinon模拟时间:
import sinon from 'sinon';
describe('timed operations', () => {
let clock: sinon.SinonFakeTimers;
beforeEach(() => {
clock = sinon.useFakeTimers();
});
afterEach(() => {
clock.restore();
});
it('calls callback after delay', () => {
const callback = sinon.spy();
delayedCallback(callback, 1000);
clock.tick(1000);
expect(callback.calledOnce).to.be.true;
});
});
测试文件系统操作
Jest模拟文件系统:
jest.mock('fs');
import fs from 'fs';
import { readConfig } from '../config';
(fs.readFileSync as jest.Mock).mockReturnValue('{"env":"test"}');
test('reads config file', () => {
const config = readConfig();
expect(config).toEqual({ env: 'test' });
});
Mocha使用mock-fs:
import mock from 'mock-fs';
import { readConfig } from '../config';
describe('config reader', () => {
beforeEach(() => {
mock({
'config.json': '{"env":"test"}'
});
});
afterEach(() => {
mock.restore();
});
it('reads config file', () => {
const config = readConfig();
expect(config).to.deep.equal({ env: 'test' });
});
});
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:与数据库ORM