描述在 grunt 中实现复数规则单元测试

解读

面试官真正想考察的是:

  1. 你是否理解“复数规则”在前端国际化(i18n)场景中的含义——即根据数量动态返回不同文案(0 个、1 个、2 个、…、n 个)。
  2. 你是否能把“规则”拆成可单元测试的纯函数,并用 Grunt 把测试任务无缝集成到构建流水线。
  3. 你是否熟悉国内团队常用的 Grunt + Mocha + Chai + PhantomJS/headless Chrome 组合,以及如何用 grunt-contrib-watch 做“保存即测试”。
  4. 你是否具备“配置即代码”的严谨思维:测试文件、源码、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
  1. 安装依赖(淘宝源加速)
npm i -D grunt grunt-mocha-test mocha chai grunt-contrib-watch grunt-istanbul
  1. 复数规则源码 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];
};
  1. 单元测试 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('多个'));
});
  1. 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']);
};
  1. 运行
# 本地开发
npx grunt dev
# CI 集成
npx grunt

若覆盖率未达 90%,CI 直接非零退出,阻断合并。

拓展思考

  1. 多 locale 动态加载:把 map 拆成独立 JSON,用 grunt-contrib-copy 按语言包维度打入 dist,减少主包体积。
  2. 浏览器端复用:同一套 plural.js 通过 grunt-browserify + babelify 转译,并在 Karma 中跑真机测试,覆盖 Safari 双位数版本。
  3. 快照回归:使用 grunt-mocha-snapshot 把文案输出保存为快照,防止产品经理“悄悄”改文案。
  4. 性能基准:在 grunt 任务链里增加 grunt-benchmark,对比 Intl.PluralRules 与手写 if-else 的耗时,向面试官展示你对 ECMA402 性能 的关注。
  5. Monorepo 场景:若 plural 模块被多个子包引用,用 grunt-lerna 统一测试,避免重复跑 4000+ 用例浪费 CI 时长。