从零开始打造 RandBox:一个强大的 JavaScript 随机数据生成库(4/5)- 测试体系与质量保证
前言
在前三篇文章中,我们深入探讨了 RandBox 的规划、架构和功能实现。今天,我将分享如何为 RandBox 构建完善的测试体系,确保库的质量和可靠性。对于一个随机数据生成库而言,测试面临着独特的挑战:如何测试”随机”的结果?如何保证跨平台的一致性?这些问题都需要巧妙的解决方案。
测试策略设计
测试金字塔原则
RandBox 的测试体系遵循经典的测试金字塔原则:
/\
/ \
/ E2E \ (少量端到端测试)
/______\
/ \
/ 集成测试 \ (中等数量)
/____________\
/ \
/ 单元测试 \ (大量基础测试)
/________________\
核心测试挑战
1. 随机性测试的悖论
随机数据生成的结果本质上是不可预测的,但测试需要可预测的断言。
解决方案:种子控制
// 使用固定种子确保测试结果可重现
const randBox = new RandBox('test-seed-123');
assert(randBox.integer(1, 10) === 7); // 在相同种子下总是返回7
2. 统计性质验证
需要验证生成数据的统计特性是否符合预期。
解决方案:大数定律
// 生成大量数据验证分布特性
test('integer distribution should be uniform', () => {
const results = {};
const iterations = 10000;
for (let i = 0; i < iterations; i++) {
const value = randBox.integer(1, 10);
results[value] = (results[value] || 0) + 1;
}
// 验证每个数字出现频率接近期望值
Object.values(results).forEach(count => {
const expected = iterations / 10;
const tolerance = expected * 0.1; // 10% 容差
assert(Math.abs(count - expected) < tolerance);
});
});
单元测试实现
1. 基础功能测试框架
选择 AVA 作为测试框架,原因如下:
- 简洁的 API 设计
- 内置并行执行
- 强大的断言库
- 优秀的错误报告
// test/basics.test.js
import test from 'ava';
import RandBox from '../src/index.js';
test.beforeEach(t => {
// 每个测试使用相同的种子确保可重现性
t.context.randBox = new RandBox('test-seed');
});
test('integer() should return number within range', t => {
const { randBox } = t.context;
for (let i = 0; i < 100; i++) {
const result = randBox.integer(1, 10);
t.true(Number.isInteger(result));
t.true(result >= 1 && result <= 10);
}
});
test('integer() with exclude option', t => {
const { randBox } = t.context;
const exclude = [5, 7, 9];
for (let i = 0; i < 100; i++) {
const result = randBox.integer({ min: 1, max: 10, exclude });
t.false(exclude.includes(result));
}
});
2. 参数验证测试
test('integer() parameter validation', t => {
const { randBox } = t.context;
// 测试无效参数
t.throws(() => randBox.integer('invalid'), {
instanceOf: TypeError,
message: /Expected number/
});
// 测试范围错误
t.throws(() => randBox.integer(10, 5), {
instanceOf: RangeError,
message: /Min cannot be greater than max/
});
// 测试边界值
t.notThrows(() => randBox.integer(0, 0)); // 应该总是返回0
t.is(randBox.integer(0, 0), 0);
});
3. 复杂功能测试
// 测试姓名生成
test('name() should generate valid names', t => {
const { randBox } = t.context;
// 中文姓名测试
const chineseName = randBox.name({ nationality: 'zh' });
t.true(typeof chineseName === 'string');
t.true(chineseName.length >= 2 && chineseName.length <= 4);
// 英文姓名测试
const englishName = randBox.name({ nationality: 'en' });
t.true(englishName.includes(' ')); // 英文名包含空格
// 带前缀的姓名
const nameWithPrefix = randBox.name({
nationality: 'en',
prefix: true,
gender: 'male'
});
t.true(nameWithPrefix.startsWith('Mr.') || nameWithPrefix.startsWith('Dr.'));
});
// 测试邮箱生成
test('email() should generate valid email addresses', t => {
const { randBox } = t.context;
const email = randBox.email();
// 基本格式验证
t.regex(email, /^[^\s@]+@[^\s@]+\.[^\s@]+$/);
// 自定义域名测试
const customEmail = randBox.email({ domain: 'example.com' });
t.true(customEmail.endsWith('@example.com'));
});
性能测试与基准测试
1. 性能测试框架搭建
// test/performance.test.js
import { performance } from 'perf_hooks';
import test from 'ava';
import RandBox from '../src/index.js';
function benchmark(name, fn, iterations = 10000) {
const start = performance.now();
for (let i = 0; i < iterations; i++) {
fn();
}
const end = performance.now();
const duration = end - start;
const opsPerSec = (iterations / duration) * 1000;
console.log(`${name}: ${duration.toFixed(2)}ms (${opsPerSec.toFixed(0)} ops/sec)`);
return { duration, opsPerSec };
}
test('performance benchmarks', t => {
const randBox = new RandBox();
// 基础功能性能测试
const integerPerf = benchmark('integer()', () => randBox.integer());
const stringPerf = benchmark('string()', () => randBox.string());
const namePerf = benchmark('name()', () => randBox.name());
// 性能要求:每秒至少处理10000次操作
t.true(integerPerf.opsPerSec > 10000);
t.true(stringPerf.opsPerSec > 5000);
t.true(namePerf.opsPerSec > 1000);
});
2. 内存使用测试
test('memory usage should be reasonable', t => {
const initialMemory = process.memoryUsage().heapUsed;
// 创建多个实例
const instances = [];
for (let i = 0; i < 1000; i++) {
instances.push(new RandBox());
}
// 生成大量数据
instances.forEach(randBox => {
for (let j = 0; j < 100; j++) {
randBox.name();
randBox.email();
randBox.address();
}
});
const finalMemory = process.memoryUsage().heapUsed;
const memoryIncrease = finalMemory - initialMemory;
// 内存增长应该在合理范围内(小于50MB)
t.true(memoryIncrease < 50 * 1024 * 1024);
});
统计特性验证
1. 随机分布测试
// test/statistics.test.js
import test from 'ava';
import RandBox from '../src/index.js';
function chiSquareTest(observed, expected, significance = 0.05) {
let chiSquare = 0;
for (let i = 0; i < observed.length; i++) {
const diff = observed[i] - expected[i];
chiSquare += (diff * diff) / expected[i];
}
// 简化的卡方检验(实际应用中需要查表)
const degreesOfFreedom = observed.length - 1;
const critical = degreesOfFreedom * 2; // 简化阈值
return chiSquare < critical;
}
test('integer distribution should pass chi-square test', t => {
const randBox = new RandBox();
const buckets = new Array(10).fill(0);
const iterations = 10000;
for (let i = 0; i < iterations; i++) {
const value = randBox.integer(0, 9);
buckets[value]++;
}
const expected = new Array(10).fill(iterations / 10);
const isUniform = chiSquareTest(buckets, expected);
t.true(isUniform, 'Distribution should be approximately uniform');
});
test('floating point distribution', t => {
const randBox = new RandBox();
const samples = [];
for (let i = 0; i < 1000; i++) {
samples.push(randBox.floating(0, 1));
}
// 验证范围
samples.forEach(sample => {
t.true(sample >= 0 && sample <= 1);
});
// 验证均值接近0.5
const mean = samples.reduce((sum, val) => sum + val, 0) / samples.length;
t.true(Math.abs(mean - 0.5) < 0.05);
});
2. 正态分布测试
test('normal distribution should have correct properties', t => {
const randBox = new RandBox();
const samples = [];
const mean = 100;
const stdDev = 15;
for (let i = 0; i < 1000; i++) {
samples.push(randBox.normal(mean, stdDev));
}
// 计算样本均值和标准差
const sampleMean = samples.reduce((sum, val) => sum + val, 0) / samples.length;
const variance = samples.reduce((sum, val) => sum + Math.pow(val - sampleMean, 2), 0) / samples.length;
const sampleStdDev = Math.sqrt(variance);
// 验证均值和标准差在合理范围内
t.true(Math.abs(sampleMean - mean) < 2);
t.true(Math.abs(sampleStdDev - stdDev) < 2);
});
兼容性测试
1. 多环境测试
// test/compatibility.test.js
test('should work in different JavaScript environments', t => {
// Node.js 环境测试
if (typeof window === 'undefined') {
t.true(true, 'Running in Node.js environment');
}
// 测试 ES6 模块导入
t.truthy(RandBox);
t.true(typeof RandBox === 'function');
// 测试基本功能
const randBox = new RandBox();
t.true(typeof randBox.random === 'function');
});
// 浏览器环境测试(使用 Puppeteer)
test('browser compatibility', async t => {
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.addScriptTag({
path: './dist/index.umd.js'
});
const result = await page.evaluate(() => {
const randBox = new window.RandBox();
return {
integer: randBox.integer(1, 10),
string: randBox.string(),
name: randBox.name()
};
});
t.true(typeof result.integer === 'number');
t.true(typeof result.string === 'string');
t.true(typeof result.name === 'string');
await browser.close();
});
2. TypeScript 类型测试
// test/types.test.ts
import test from 'ava';
import RandBox from '../src/index';
test('TypeScript type definitions', t => {
const randBox = new RandBox();
// 编译时类型检查
const num: number = randBox.integer();
const str: string = randBox.string();
const bool: boolean = randBox.bool();
// 可选参数类型检查
const customInt: number = randBox.integer({ min: 1, max: 10, exclude: [5] });
const customStr: string = randBox.string({ length: 20, alpha: true });
t.true(typeof num === 'number');
t.true(typeof str === 'string');
t.true(typeof bool === 'boolean');
});
持续集成配置
1. GitHub Actions 工作流
# .github/workflows/test.yml
name: Test Suite
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [16.x, 18.x, 20.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 linting
run: npm run lint
- name: Run type checking
run: npm run typecheck
- name: Run tests
run: npm test
- name: Run coverage
run: npm run coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
browser-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Build for browser
run: npm run build
- name: Run browser tests
run: npm run test:browser
2. 测试覆盖率配置
// nyc.config.js
module.exports = {
include: ['src/**/*.ts'],
exclude: [
'src/**/*.test.ts',
'src/**/*.spec.ts',
'src/types.ts'
],
reporter: ['text', 'html', 'lcov'],
'report-dir': './coverage',
'temp-dir': './coverage/.nyc_output',
checkCoverage: true,
lines: 90,
statements: 90,
functions: 90,
branches: 85
};
质量保证流程
1. 代码质量检查
// .eslintrc.js
module.exports = {
extends: [
'@typescript-eslint/recommended',
'plugin:jest/recommended'
],
rules: {
// 自定义规则
'prefer-const': 'error',
'no-var': 'error',
'@typescript-eslint/explicit-function-return-type': 'warn',
'@typescript-eslint/no-explicit-any': 'error'
}
};
2. 自动化发布流程
{
"scripts": {
"preversion": "npm run test && npm run build",
"version": "npm run docs:build",
"postversion": "git push && git push --tags",
"prepublishOnly": "npm run test && npm run build",
"release": "semantic-release"
}
}
3. 变更日志自动生成
// release.config.js
module.exports = {
branches: ['main'],
plugins: [
'@semantic-release/commit-analyzer',
'@semantic-release/release-notes-generator',
'@semantic-release/changelog',
'@semantic-release/npm',
'@semantic-release/github'
]
};
测试数据管理
1. 测试数据生成器
// test/helpers/test-data-generator.js
export class TestDataGenerator {
static generateTestCases(count = 100) {
const cases = [];
for (let i = 0; i < count; i++) {
cases.push({
seed: `test-${i}`,
expected: {
integer: this.expectedInteger(`test-${i}`),
string: this.expectedString(`test-${i}`),
name: this.expectedName(`test-${i}`)
}
});
}
return cases;
}
static expectedInteger(seed) {
// 基于种子预计算期望值
const randBox = new RandBox(seed);
return randBox.integer(1, 100);
}
}
2. 快照测试
test('output consistency snapshot test', t => {
const randBox = new RandBox('snapshot-seed');
const output = {
integers: Array(10).fill().map(() => randBox.integer(1, 100)),
strings: Array(5).fill().map(() => randBox.string({ length: 10 })),
names: Array(5).fill().map(() => randBox.name()),
emails: Array(5).fill().map(() => randBox.email())
};
t.snapshot(output, 'Random output should be consistent');
});
下期预告
在最后一篇文章中,我将分享 RandBox 的文档建设、社区运营和生态发展,包括:
- 现代化文档网站的构建
- API 文档的自动生成
- 社区贡献指南的制定
- 插件生态的规划与实现
敬请期待《从零开始打造 RandBox(5/5)- 文档建设与生态发展》!
关于 RandBox
🏠 官网: https://randbox.top 📦 GitHub: https://github.com/027xiguapi/randbox 📚 文档: https://randbox.top/zh/docs
如果这个项目对你有帮助,欢迎给我们一个 ⭐ Star!
最后更新于: