阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 与测试框架(Jest/Mocha)

与测试框架(Jest/Mocha)

作者:陈川 阅读数:23092人阅读 分类: TypeScript

测试框架(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

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌