让 TypeScript 开源项目支持单元测试覆盖率

孙博 技术分享
TypeScript 单元测试 覆盖率 Github Mocha NYC Coveralls

很多小伙伴在访问 Github 的开源项目时,总能看到一些与项目有关的徽章,比如代码测试覆盖率、代码质量、下载次数等等。前段时间有朋友问我该如何设置,我想干脆就写一篇小短文来分享一下吧,万一恰好还有朋友需要呢。

徽章其实并不是什么稀罕的东西。在打开 Github 项目时,默认会将 README.md 文件的内容渲染成网页,而徽章就是通过 Markdown 语法来实现的。说白了,徽章仅仅只是个图片而已。你可以使用 Markdown 语法来插入任何你想要的图片,并不仅仅局限于徽章。但是如果是想要插入与自己项目有关的、动态的数据 —— 如代码测试覆盖率、代码质量、下载次数等等,那又该怎么办呢?总不可能每次上传代码时,再去修改图片或图片地址吧。

本文将以单元测试覆盖率作为示例,来和大家一起探讨如何为 TypeScript 项目实施单元测试,并添加单元测试覆盖率徽章。

提到单元测试覆盖率,首先要先从单元测试说起。

Mocha

mocha 是一个非常优秀的单元测试框架,它能较好的支持 TypeScript,并且非常容易使用。但与一般的 Nodejs 项目一样,它默认只能运行 JavaScript 代码,所以我们还需要采取一些其他的手段来让其支持 TypeScript

我们以我项目中正在使用的 10.1.0 版本为例,你只需要安装 ts-node,并在 .mocharc.json 文件中配置 require 节点的值为 ts-node/register,便可以使用 TypeScript 来编写单元测试了。

单元测试的代码通常都放在 test 目录下,所有单元测试的代码均以 .spec.ts 为后缀。mocha 会自动查找 test 目录下的所有 .spec.ts 文件,并执行其中的测试代码。

测试代码由 describeit 两个函数组成,分别表示测试用例的组和测试用例。一个普通的测试代码就像下面这样:

/* tslint:disable */
import { expect } from "chai";
import DEFAULT, { Enumerable, List, Dictionary, ReadOnlyCollection, ReadOnlyList } from "../src/index";

describe("./index.ts", function () {
  it("存在 默认导出", function () {
    expect(DEFAULT).not.null;
    expect(DEFAULT).not.undefined;
    expect(typeof DEFAULT).to.equal("function");
  });

  // ............
  // 其他测试用例,此处略。
  // ............
});

它在运行后的结果类似下图:

mocha report

在上述代码中,可以看到引入的一个库叫做 chai,它是一个非常优秀的断言库,可以让我们在测试代码中方便的编写断言。其他常见的断言库还有 shouldassert 等等,它们各有特点,但都是为了编写断言,这里就不做特别介绍了。

对很多人来说,正确配置 mocha 以让它支持 TypeScript 是一个很麻烦的事情。所以我将在下面贴出我的 .mocharc.jsontsconfig.json 文件供大家参考:

.mocharc.json

{
  "bail": true,
  "timeout": 0,
  "recursive": true,
  "require": "ts-node/register",
  "extension": "ts"
}

tsconfig.json

{
  "include": ["src/**/*"],
  "compilerOptions": {
    "esModuleInterop": true,
    "module": "commonjs",
    "target": "es5",
    "sourceMap": true,
    "declaration": true,
    "downlevelIteration": true,
    "lib": ["es5", "es2015", "dom"],
    "outDir": "dist"
  }
}

当你在项目代码中写入了大量的单元测试代码后,mocha 会依次执行。但只有这些并不够,我们只能保证我们写的用例能不能正常执行,但还不能表明已经走完了项目中的全部分支。

只有测试用例覆盖了全部的项目代码、经过了全部分支条件,才能保证代码的质量。单纯使用 mocha 是没有办法做到这一点的,所以我们需要引入 nyc

NYC

nyc 的前身是 istanbul,它是非常知名的代码覆盖率工具。它不仅可以支持 JavaScript,还能通过 mapping 的方式统计出 TypeScript 项目的单元测试覆盖率。

它最让人喜欢的特性之一是 —— 它不仅仅能够统计出单元测试覆盖率的结果,更能以生成格式精美的 HTML 报告,方便我们查漏补缺,就像下图一样。

html report

追求更高的代码覆盖率(甚至是 100%)是每个项目的理想目标,但完善所有代码路径的覆盖是一件颇具挑战性的工作,尤其是在大型项目中。所以,我们通常只需要保证项目中的代码覆盖率不低于一定值即可。我一般会将代码覆盖率统一设置为 95%,对于特别难走到,或者是确定不需要测试的代码,可以追加 /* istanbul ignore next *//* istanbul ignore else */ 这样的注释来忽略部分代码。

* 更多的 istanbul 注释请参考官方文档。

正如同执行 mocha 的命令仅需要 mocha 非常的简单,执行 nyc 也并不复杂。在配置好 .nycrc 规则文件后,只需要执行 nyc mocha 即可。

我也将我的 .nycrc 文件贴出来供大家参考:

.nycrc

{
  "check-coverage": true,
  "cache": false,
  "lines": 95,
  "statements": 95,
  "functions": 95,
  "branches": 95,
  "extension": [".ts"],
  "reporter": ["html", "lcov", "text", "text-summary"],
  "include": ["src"]
}

按照我的设置,除了上面截图中的 html 报告之外,在控制台中还会以文字的形式打印出测试覆盖率的结果,就像下面这样:

text report

text-summary report

现在本地已经能正确的生成单元测试覆盖率报告了,甚至连 lcov 格式的报告文件已经有了,我们便可以将其上传至 coveralls.io 进行外显了。

Coveralls

Coveralls 是一个知名的代码覆盖率站点,它支持多种语言,包括 C、C++、C#、Java、JavaScript、Objective-C、PHP、Python、Ruby、Scala 等等。我们只需要在项目中进行单元测试,并将测试结果以特定的格式上传,即可在站点中看到详实的代码覆盖率数据页面,就像下图一样:

coveralls

使用 Coveralls 非常简单,只需要打开 coveralls.io,使用 GITHUB SIGN IN 登录 (或其他你更喜欢的注册登录方式),然后在左边栏中点击 + 号,选择 Add Repos,即可通过切换 SWITCH 按钮的方式添加你的开源项目。

* 私有项目也是可以添加的,但为了防止无意间泄漏代码,我没有开启这个能力。

添加完成后,便可以通过 Detail 按钮跳转到项目的代码测试覆盖率页面,但新配置的项目肯定是不会显示出任何数据的,下面让我介绍下如何进行上传。

很显然,我们不可能选择手动上传,因为这太麻烦了。不过好在 Coveralls 提供了多种自动化的上传方式,我们可以使用脚本的方式完成上传动作。曾经最简单的方式是使用 Travis CI 来执行脚本,但它在前两年改变了自己的策略,当你的额度用完后便无法免费使用了,所以现在更推荐使用的是 Github Actions

Github Actions

Github ActionsGithub 提供的自动化构建、测试、部署、发布等能力的工具,它通过你在项目中配置的 .github/workflows 文件来执行相应的动作。它的配置有点像 Jenkins 的感觉,但与 Jenkins 还是有不少不同之处。

我在我的示例项目中配置了两个 workflow,分别是 ci-test.yml 用来执行自动化测试,和 npm-publish.yml 用来将项目构建后发布到 npm。这里将以 ci-test.yml 为例,介绍如何配置自动化测试及将测试结果上传至 Coveralls

其实 Coveralls 官方文档中已经写了标准方案,在 https://docs.coveralls.io/integrations#official-integrations 中有 Coveralls Github Action 的集成说明,点开后则是 Github Actions 的插件说明:https://github.com/marketplace/actions/coveralls-github-action。在最傻瓜的情况下,你只需要把它文档中 Standard Example 的代码复制到你项目中即可。

我的项目是很多年前的,在写这篇文章时,我发现插件都已经有了更新的版本。但为了能帮助大家进行对比,我也会把我的配置文件贴出来供大家参考,你可以可以直接使用官方示例中的最新版本。

name: CI-TEST
on: [push, pull_request]
jobs:
  ci-test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [14.x, 16.x, 18.x, 20.x, 22.x]
    steps:
      - uses: actions/checkout@v2
      - name: Set Nodejs Version
        uses: actions/setup-node@v2
        with:
          node-version: ${{ matrix.node-version }}
      - name: Install Dependencies
        run: npm i
      - name: Unit Tests
        run: npm test
      - name: Coveralls
        uses: coverallsapp/github-action@master
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}

我的这个配置允许我在将代码推送到 Github 仓库、或创建 PR 后,自动分别在 NodeJS 14.x ~ 22.x 不同版本的环境下,分别独立的执行 npm test 命令,然后将测试结果上传至 Coveralls

能看到这里的肯定都是程序员,我想我不需要针对配置文件进行过多的解释了。
CI-TEST 成功运行结束后,你便可以在 Coveralls 中看到本次提交的测试结果了。

Badges

完成这一切之后,我们可以在 Coveralls 中拷贝出 Badges 的代码,然后插入到 README.md 文件中,让我们的项目看起来更加专业。徽章代码如下:

[![Coverage Status](https://coveralls.io/repos/github/LuckyStarry/luckystarry-collections/badge.svg?branch=master)](https://coveralls.io/github/LuckyStarry/luckystarry-collections?branch=master)

被渲染之后的结果就像下面图中一样。

github badges


全文完

如果你想完整的参考我的项目,可以访问我的 Github 仓库 https://github.com/LuckyStarry/luckystarry-collections

这是一个为 NodeJS 代码的集合对象(Array)赋予像 .NET 那样的 LINQ 查询能力的库,由于没有依赖特定的环境,所以在浏览器端或服务器端都可以使用,感兴趣的小伙伴也欢迎使用。