描述在 grunt 中实现复数规则单元测试
解读
面试官真正想考察的是:
- 你是否理解“复数规则”在前端国际化(i18n)场景中的含义——即根据数量动态返回不同文案(0 个、1 个、2 个、…、n 个)。
- 你是否能把“规则”拆成可单元测试的纯函数,并用 Grunt 把测试任务无缝集成到构建流水线。
- 你是否熟悉国内团队常用的 Grunt + Mocha + Chai + PhantomJS/headless Chrome 组合,以及如何用 grunt-contrib-watch 做“保存即测试”。
- 你是否具备“配置即代码”的严谨思维:测试文件、源码、Gruntfile 三者职责分离,且能在 CI 环境一次性跑通。
知识点
- Grunt 任务阶段:initConfig → registerTask → loadNpmTasks
- grunt-mocha-test:在 Node 端跑 Mocha,避免浏览器环境差异;国内镜像源可提速安装
- PluralRule 设计模式:函数签名推荐
(count, locale) => string,内部用 Intl.PluralRules(ECMA402)或自定义 locale 表 - 测试用例分层:正常分支(0、1、few、many)、边界分支(-1、NaN、Infinity)、异常分支(null、undefined、非法 locale)
- 覆盖率卡点:grunt-istanbul 配合 nyc,在 CI 中强制 ≥90% 分支覆盖率
- watch 优化:用 grunt-contrib-watch 的 spawn:false + livereload 实现 1 秒内二次测试
- 国内 CI 适配:GitLab-CI / Jenkins 中缓存 node_modules,避免重复安装 phantomjs-prebuilt
答案
以下示例基于 Grunt 1.5.x + Mocha 9.x + Node 16,目录结构为国内团队惯用的“src/test/build”三分离:
project
├─ src
│ └─ plural.js
├─ test
│ └─ plural.test.js
├─ Gruntfile.js
└─ package.json
- 安装依赖(淘宝源加速)
npm i -D grunt grunt-mocha-test mocha chai grunt-contrib-watch grunt-istanbul
- 复数规则源码
src/plural.js
// 只导出一个纯函数,方便单元测试
exports.choose = function (count, locale = 'zh-CN') {
const rules = new Intl.PluralRules(locale);
const key = rules.select(count);
const map = {
'zh-CN': { zero: '零个', one: '一个', other: '多个' },
'en-US': { zero: 'zero items', one: 'one item', other: 'many items' }
};
return map[locale][key] || map['zh-CN'][key];
};
- 单元测试
test/plural.test.js
const { expect } = require('chai');
const { choose } = require('../src/plural');
describe('PluralRule 单元测试', () => {
it('中文 0 个', () => expect(choose(0, 'zh-CN')).to.equal('零个'));
it('中文 1 个', () => expect(choose(1, 'zh-CN')).to.equal('一个'));
it('中文 5 个', () => expect(choose(5, 'zh-CN')).to.equal('多个'));
it('英文 1 item', () => expect(choose(1, 'en-US')).to.equal('one item'));
it('非法 locale 降级 zh-CN', () => expect(choose(99, 'xx-YY')).to.equal('多个'));
it('边界 NaN 走 other 分支', () => expect(choose(NaN, 'zh-CN')).to.equal('多个'));
});
- Gruntfile.js 配置
module.exports = function (grunt) {
grunt.initConfig({
mochaTest: {
test: {
options: { reporter: 'spec', require: function(){ /* 可放 babel-register */ } },
src: ['test/**/*.test.js']
}
},
watch: {
test: {
files: ['src/**/*.js', 'test/**/*.js'],
tasks: ['mochaTest'],
options: { spawn: false, livereload: 35729 }
}
},
istanbul: {
coverage: {
src: 'test',
options: {
coverageFolder: 'build/coverage',
check: {
lines: 90,
branches: 90,
functions: 90
}
}
}
}
});
grunt.loadNpmTasks('grunt-mocha-test');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.loadNpmTasks('grunt-istanbul');
// 默认任务:一次性测试 + 覆盖率
grunt.registerTask('default', ['mochaTest', 'istanbul']);
// 本地开发:保存即测试
grunt.registerTask('dev', ['mochaTest', 'watch']);
};
- 运行
# 本地开发
npx grunt dev
# CI 集成
npx grunt
若覆盖率未达 90%,CI 直接非零退出,阻断合并。
拓展思考
- 多 locale 动态加载:把 map 拆成独立 JSON,用 grunt-contrib-copy 按语言包维度打入 dist,减少主包体积。
- 浏览器端复用:同一套 plural.js 通过 grunt-browserify + babelify 转译,并在 Karma 中跑真机测试,覆盖 Safari 双位数版本。
- 快照回归:使用 grunt-mocha-snapshot 把文案输出保存为快照,防止产品经理“悄悄”改文案。
- 性能基准:在 grunt 任务链里增加 grunt-benchmark,对比 Intl.PluralRules 与手写 if-else 的耗时,向面试官展示你对 ECMA402 性能 的关注。
- Monorepo 场景:若 plural 模块被多个子包引用,用 grunt-lerna 统一测试,避免重复跑 4000+ 用例浪费 CI 时长。