配置 angular2 的单元测试与CI持续集成

2018-06

目录

  • 单元测试简介
  • 单元测试涉及的框架
  • 针对 angular2+ 的单元测试配置与编写
  • 持续集成:Jenkins 构建
  • 持续集成:GITLAB-CI
  • FAQ

1 单元测试的基本概念

1.1 测试套件(Test Suite)

表示一组测试用例集合。一般将同一个功能模块的实现方法的测试用例,写为一个集合。在测试代码书写上,则表现为使用 describe 包装描述,并且可以嵌套书写。示例:

describe('test suite name', () => {
    describe('test suite name1', () => {});
    describe('test suite name2', () => {});
});

1.2 测试用例(Specs)

用于单个功能方法的测试。在编码书写上表现为使用 it 包装,并放置于 describe 中。示例:

describe('test suite name', () => {
    const isTrue = true;
    it('should be true', () => {
        expect(isTrue).toBe(true);
        // 断言失败,测试不通过
        expect(isTrue).toBe(false);
    });
});

1.3 断言(Expectations)

在测试用例中,用于断定指定的结果的期望值。如果与期望值相同,则通过,否则则认为失败。如上例中的 expect 即为一个断言方法。

具体到断言的实现,不同的断言库风格各异,常见的有这几种:

const result = [1,2,3].indexOf(4);
// expect风格
expect(result).toBe(-1);
// assert 风格
assert.equal(result, -1);
// should 风格(BDD)
result.should.be.exactly(-1);
should(result).be.exactly(-1);
result.should.equal(-1);

1.4 单元测试覆盖率

通过覆盖率报告,我们甚至可以直观的了解哪些代码测试到了,哪些代码未测试到。

通过覆盖率报告,可以了解被测试代码的覆盖情况,以避免漏测的发生,但没法去确定各种不同场景是否都有涉及,这只能依靠测试代码编写者去把控。

2 单元测试涉及的框架

  • 测试框架。负责整体的测试执行逻辑。如:eva、Jasmine、mocha
  • 断言库。提供断言方法。如:chai(expect/assert/should三种风格都支持)、should.js、assert.js、better-assert 等。Jasmine 自带断言库
  • 覆盖率测试工具。如:istanbul(伊斯坦布尔)
  • 测试过程管理工具(Test Runner)。管控测试执行过程,以方便多样化的测试执行需求,如多环境自动化测试、CI集成等。如:karma

3 针对 angular2+ 的单元测试配置

3.1 测试工具简介

3.1.1 Karma [ˈkɑ:mə]

Karma 是一个基于 Node.js 的 JavaScript 测试执行过程管理工具(Test Runner)。该工具可用于测试所有主流 Web 浏览器,也可集成到 CI(Continuous integration)工具,也可和其他代码编辑器一起使用。

为什么会有 Karma

On the AngularJS team, we rely on testing and we always seek better tools to make our life easier. That's why we created Karma - a test runner that fits all our needs.

Karma 通过读取配置文件 karma.conf.js 决定如何执行单元测试。

karma 的安装

在项目根目录执行如下命令:

# 添加 Karma
yarn add -D karma
# 安装相关依赖插件,根据需求不同,你可能需要安装更多的插件
yarn add -D karma-jasmine karma-chrome-launcher jasmine-core

为了方便执行 Karma 命令,可以全局安装 karma-cli

npm install -g karma-cli

karma 的使用

# 使用向导方式创建配置文件
karma init
# 启动 karma
karma start
# or
./node_modules/karma/bin/karma start
# 指定参数运行 karma
karma start my.conf.js --log-level debug --single-run

执行 karma -h 可了解更多功能参数的含义。

3.1.2 Jasmine

Jasmine 是一种行为驱动开发(BDD)的单元测试框架。它提供了各种 API 辅助测试的环境模拟和执行,并且自带断言库(mocha 需要第三方断言库支持)。

3.1.3 Angular 测试工具API

用于辅助测试,如组件的创建、组件DOM的获取、依赖注入的模拟、http 请求的模拟等。 主要有:TestBed类、async、fakeAsync、tick、inject、spy(Jasmine)等。

3.1.4 Istanbul

istanbul 是一个单元测试代码覆盖率检查工具,可以很直观地告诉我们,单元测试对代码的控制程度。

Istanbul 是 JavaScript 程序的代码覆盖率工具,以土耳其最大城市伊斯坦布尔命名。Istanbul会对代码进行转换,生成语法树,然后在相应位置注入统计代码,执行之后根据注入的全局变量的值,统计代码执行的次数;在对代码的转换完成之后,Istanbul 会调用 test runner 执行转换之后的代码的测试,生成测试报告。

3.2 配置测试到 angular2+ 的项目中

3.2.1 添加依赖插件

添加 Jasmine、 karma 测试工具及 typescript、pug、less 编译相关的 webpack 依赖插件。配置如下依赖到 package.json 文件的 devDependencies 中:

devDependencies: {
    "@types/core-js": "^0.9.46",
    "@types/jasmine": "^2.8.7",
    "@types/jasminewd2": "^2.0.3",
    "@types/webpack": "~2.2.14",
    "angular2-template-loader": "~0.6.2",
    "awesome-typescript-loader": "~3.2.3",
    "cross-env": "^5.1.6",
    "istanbul-instrumenter-loader": "^3.0.1",
    "jasmine-core": "^3.1.0",
    "karma": "^2.0.2",
    "karma-chrome-launcher": "^2.2.0",
    "karma-coverage": "^1.1.1",
    "karma-coverage-istanbul-reporter": "^2.0.1",
    "karma-jasmine": "^1.1.2",
    "karma-jasmine-html-reporter": "^1.1.0",
    "karma-mocha-reporter": "^2.2.5",
    "karma-remap-coverage": "^0.1.5",
    "karma-sourcemap-loader": "~0.3.7",
    "karma-webpack": "^3.0.0",
    "less": "^3.0.4",
    "less-loader": "^4.1.0",
    "pug": "^2.0.3",
    "pug-html-loader": "^1.1.5",
    "raw-loader": "~0.5.1",
    "to-string-loader": "^1.1.5",
    "webpack": "~3.6.0"
}

3.2.2 karma.conf.js 配置

// for dev watch
  if (process.env.NODE_TEST_ENV === 'debug') {
    Object.assign(configuration, {
      browsers: ['Chrome'],
      singleRun: false, autoWatch: true,
      reporters: ['progress', 'kjhtml'],
      client: { clearContext: false }
    });
  }
// Optional Sonar Qube Reporter
  if (process.env.SONAR_QUBE) {
    configuration.sonarQubeUnitReporter = {
      sonarQubeVersion: '5.x',
      outputFile: 'reports/ut_report.xml',
      overrideTestDescription: true,
      testPath: 'src', testFilePattern: '.spec.ts',
      useBrowserName: false
    };
    configuration.remapCoverageReporter.lcovonly = './coverage/coverage.lcov';
    configuration.reporters.push('sonarqubeUnit');
  }
  // for TRAVIS CI
  if (process.env.TRAVIS) {
    configuration.browsers = ['ChromeTravisCi'];
  }

  config.set(configuration);
};

3.2.3 tsconfig.spec.json 配置

配置单元测试执行时 typeScript 的编译方式。参考:

{
  "compileOnSave": false,
  "buildOnSave": false,
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "sourceMap": true,
    "noImplicitAny": false,
    "noEmit": true,
    "noEmitHelpers": true,
    "importHelpers": true,
    "skipLibCheck": true,
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "baseUrl": "./src",
    "paths": { "@angular/*": ["node_modules/@angular/*"] },
    "typeRoots": ["node_modules/@types"],
    "types": [ "jasmine", "node", "core-js", "webpack", "jquery" ],
    "lib": ["es2017", "dom"]
  },
  "angularCompilerOptions": {"debug": true},
  "exclude": ["node_modules", "dist"],
  "awesomeTypescriptLoaderOptions": {"forkChecker": true, "useWebpackText": true }
}

3.2.4 新建单元测试入口文件

入口文件加载项目主要依赖库,并加载所有 .sepc.ts 测试文件及其依赖。

文件名如 test/spec-bundle.js,内容为:

Error.stackTraceLimit = Infinity;
require('core-js/es6');
require('core-js/es7/reflect');
require('zone.js/dist/zone');
require('zone.js/dist/long-stack-trace-zone');
require('zone.js/dist/proxy');
require('zone.js/dist/sync-test');
require('zone.js/dist/jasmine-patch');
require('zone.js/dist/async-test');
require('zone.js/dist/fake-async-test');
require('rxjs/Rx');
const testing = require('@angular/core/testing');
const browser = require('@angular/platform-browser-dynamic/testing');
// 针对 NBOP 项目的各种兼容
window.$ = window.jQuery = require('jquery');
window['NODE_ENV'] = 'dev'; // 'test'
window.__uri = uri => uri;
window.nodeRequire = () => {};

testing.TestBed.initTestEnvironment(
  browser.BrowserDynamicTestingModule,
  browser.platformBrowserDynamicTesting()
);

const testContext = require.context('../src', true, /\.spec\.ts/);
testContext.keys().map(testContext);

3.2.5 webpack 配置

webpack 主要用于 less、pug、css、typeScript 等的编译和模块化加载。另外还包含单元测试覆盖率报告生成的配置。

文件名为 webpack.test.js,配置内容参考:

const path = require('path');
const webpack = require('webpack');
const ENV = (process.env.ENV = process.env.NODE_ENV = 'test');
const resolve = {
  extensions: ['.ts', '.js', '.json'],
  modules: [helpers.root('src'), 'node_modules']
},
const plugins = [
  new webpack.ContextReplacementPlugin(
    /angular(\\|\/)core(\\|\/)@angular/,
    './src'
  ),
  new webpack.LoaderOptionsPlugin({
    debug: false,
  })
];

module.exports = {
  resolve,
  module,
  plugins,
  performance: {
    hints: false
  },
  devtool: 'inline-source-map'
};

3.3 基于 Jasmine 的 Angular 单元测试编写

angular2+ 单元测试编写应参考官方指南官方测试指南

应当注意:

  • 单元测试文件应与测试对象同名、同目录,以 .spec.ts 结尾
  • 公共工具类方法,必须写完整严格的单元测试
  • 公共组件,必须写单元测试,功能应当可全部覆盖
  • 业务组件,视情况选择性书写单元测试
  • 组件使用的工具类方法、服务,应当书写单元测试
  • 管道、服务类,应当书写单元测试

3.4 单元测试的执行

命令配置参考(配置到 package.jsonscripts 中):

"scripts": {
    "test": "cross-env NODE_TEST_ENV=debug karma start",
    "test:ci": "karma start --single-run",
    "test:sonar": "cross-env SONAR_QUBE=1 karma start --single-run"
}

执行测试:

# 监听方式
npm run test
# 只执行一次,会生成覆盖率报告到 coverage 目录中
npm run test:ci

4 持续集成:Jenkins 构建

提示:由于实践中使用了 Gitlab-CI(见后文),Jenkins 的配置过程并未实际操作,本小节内容为搜集整理而来,仅供参考。

4.1 前端项目配置

  • 配置 sonar-project.properties,内容参考:
sonar.projectKey=angular:nbop-front
sonar.projectName=nbop-front
sonar.projectVersion=6.0.0
sonar.sourceEncoding=UTF-8
sonar.sources=src
sonar.exclusions=**/node_modules/**,**/*.spec.ts
sonar.tests=src/app
sonar.test.inclusions=**/*.spec.ts

# tslint
# https://github.com/Pablissimo/SonarTsPlugin#installation
# sonar.ts.tslint.outputPath=reports/lint_issues.json
# sonar.ts.tslint.configPath=tslint.json
sonar.ts.coverage.lcovReportPath=coverage/coverage.lcov

sonar.genericcoverage.unitTestReportPaths=reports/ut_report.xml
# sonar.host.url=http://localhost:9000
  • karma.conf.js 添加针对 sonar 的配置:
if (process.env.SONAR_QUBE) {
    configuration.sonarQubeUnitReporter = {
      sonarQubeVersion: '5.x', // 这里应对应修改为实际的sonarqube主版本
      outputFile: 'reports/ut_report.xml',
      overrideTestDescription: true,
      testPath: 'src',
      testFilePattern: '.spec.ts',
      useBrowserName: false
    };
    configuration.remapCoverageReporter.lcovonly = './coverage/coverage.lcov';
    configuration.reporters.push('sonarqubeUnit');
  }

4.2 Jenkins server 环境要求

4.2.1 依赖插件安装

  • Chrome 安装:
cd ~
mkdir chrome && cd chrome
wget https://npm.taobao.org/mirrors/chromium-browser-snapshots/Linux_x64/563942/chrome-linux.zip
unzip chrome_linux64.zip
vi ~/.bashrc #添加环境变量
#在最后一行添加如下一行,然后保存退出
export PATH=/home/username/chrome:$PATH
source ~/.bashrc #立即生效

# 如执行时有依赖报错,尝试安装一下依赖
# https://segmentfault.com/a/1190000011382062
#依赖库
yum install pango.x86_64 libXcomposite.x86_64 libXcursor.x86_64
libXdamage.x86_64 libXext.x86_64 libXi.x86_64 libXtst.x86_64
 cups-libs.x86_64 libXScrnSaver.x86_64 libXrandr.x86_64 GConf2.x86_64
 alsa-lib.x86_64 atk.x86_64 gtk3.x86_64 -y
#字体
yum install ipa-gothic-fonts xorg-x11-fonts-100dpi xorg-x11-fonts-75dpi
xorg-x11-utils xorg-x11-fonts-cyrillic xorg-x11-fonts-Type1
xorg-x11-fonts-misc -y

4.2.2 Jenkins 构建配置

  • 构建环境 中,选中 Prepare SonarQube Scanner environment
  • 构建-Execute shell 中添加相关的执行命令:
# 执行测试,会生成兼容 sonar 的测试结果到指定目录
npm run test:sonar
# 将测试执行的结果推送到 SonarCube
sonar-scanner

5 持续集成:GITLAB-CI

通过配置 gitlab-ci,可以实现有新代码提交时立即执行单元测试,以尽早的获知错误的存在,而不是推迟到项目构建时才能发现。

5.1 安装 docker 环境

5.2 自定义 docker 镜像

由于单元测试需要 nodejs、chrome 浏览器等环境,内部 gitlab 又因安全原因与外网隔离,无法在镜像初始化时执行一些软件的安装操作, 我们首先需要定制满足基本环境要求的 docker 镜像。

这里我们选取 weboaks/node-karma-protractor-chrome 为基础镜像,并添加本地的 node_modules 目录到镜像中,进行定制并发布到内部 docker 仓库中。

Dockerfile 文件内容:

FROM weboaks/node-karma-protractor-chrome
ADD ./node_modules /usr/local/node_modules

执行自定义镜像的构建与发布:

docker build -t docker.gf.com.cn/gtp/gtpnode:unittest .
docker push docker.gf.com.cn/gtp/gtpnode:unittest

5.3 项目中增加对 gitlab-ci 的配置支持

配置 .gitlab-ci.yml 文件,内容参考如下:

image: docker.gf.com.cn/gtp/gtpnode:unittest

cache:
  paths:
  - node_modules/

stages:
 - test

test_async:
  stage: test
  script:
   - npm config set registry http://registry_npm.gf.com.cn
   - cp -R /usr/local/node_modules ./
   - GITLAB_CI=1 karma start --single-run

覆盖率配置:

打开 gitlab 项目页面,依次操作: Settings -> pipelines -> Test coverage parsing,输入框中输入覆盖率匹配正则:

Statements\s*:\s*(\d+\.\d+\%) \(

README.md 增加 pipelines 执行状态图标:

[![pipeline status]
(http://gitlab.gf.com.cn/gmts/front/badges/master/pipeline.svg)]
(http://gitlab.gf.com.cn/gmts/front/pipelines)

[![coverage report]
(http://gitlab.gf.com.cn/gmts/front/badges/master/coverage.svg)]
(http://gitlab.gf.com.cn/gmts/front/-/jobs)

http://gitlab.gf.com.cn/gmts/front/-/jobs

以上操作完成,提交一下代码到 gitlab,然后到 gitlab 项目的 pipelines 即可看到单元测试的执行。

e2e 端到端测试

端到端测试本质上算是黑盒式的 UI 功能测试。通过模拟浏览器上用户的行为操作,测试指定的操作行为得到的结果是否与预期相同。由于 e2e 编写涉及交互逻辑,过于复杂,耗时较多,而测试组本省有 UI 测试,我们对此不做要求。

更多参考

FAQ