表示一组测试用例集合。一般将同一个功能模块的实现方法的测试用例,写为一个集合。在测试代码书写上,则表现为使用 describe
包装描述,并且可以嵌套书写。示例:
describe('test suite name', () => {
describe('test suite name1', () => {});
describe('test suite name2', () => {});
});
用于单个功能方法的测试。在编码书写上表现为使用 it
包装,并放置于 describe
中。示例:
describe('test suite name', () => {
const isTrue = true;
it('should be true', () => {
expect(isTrue).toBe(true);
// 断言失败,测试不通过
expect(isTrue).toBe(false);
});
});
在测试用例中,用于断定指定的结果的期望值。如果与期望值相同,则通过,否则则认为失败。如上例中的 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);
衡量单元测试对功能代码的测试情况,通过统计单元测试中对功能代码中行、分支、类等模拟场景数量,来量化说明测试的充分度。
通过覆盖率报告,我们甚至可以直观的了解哪些代码测试到了,哪些代码未测试到。
通过覆盖率报告,可以了解被测试代码的覆盖情况,以避免漏测的发生,但没法去确定各种不同场景是否都有涉及,这只能依靠测试代码编写者去把控。
我们的 angular2 项目中使用了 pug
模板引擎和 less
预处理器。我们采用 webpack3
进行项目编译,测试工具则主要有:
Karma 是一个基于 Node.js 的 JavaScript 测试执行过程管理工具(Test Runner)。该工具可用于测试所有主流 Web 浏览器,也可集成到 CI(Continuous integration)工具,也可和其他代码编辑器一起使用。
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
yarn add -D karma
# 安装相关依赖插件,根据需求不同,你可能需要安装更多的插件
yarn add -D karma-jasmine karma-chrome-launcher jasmine-core
为了方便执行 Karma 命令,可以全局安装 karma-cli
:
npm install -g karma-cli
# 使用向导方式创建配置文件
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
可了解更多功能参数的含义。
Jasmine 是一种行为驱动开发(BDD)的单元测试框架。它提供了各种 API 辅助测试的环境模拟和执行,并且自带断言库(mocha 需要第三方断言库支持)。
用于辅助测试,如组件的创建、组件DOM的获取、依赖注入的模拟、http 请求的模拟等。 主要有:TestBed类、async、fakeAsync、tick、inject、spy(Jasmine)等。
istanbul 是一个单元测试代码覆盖率检查工具,可以很直观地告诉我们,单元测试对代码的控制程度。
Istanbul 是 JavaScript 程序的代码覆盖率工具,以土耳其最大城市伊斯坦布尔命名。Istanbul会对代码进行转换,生成语法树,然后在相应位置注入统计代码,执行之后根据注入的全局变量的值,统计代码执行的次数;在对代码的转换完成之后,Istanbul 会调用 test runner 执行转换之后的代码的测试,生成测试报告。
添加 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"
}
karma.conf.js
配置process.env.NODE_ENV = 'test';
const testWebpackConfig = require('./webpack.test');
module.exports = function(config) {
const configuration = {
basePath: '',
frameworks: ['jasmine'],
files: [
{ pattern: './config/spec-bundle.js', watched: false },
{ pattern: './src/{css,img}/**/*', watched: false, included: false, served: true, nocache: false }
],
proxies: { '/assets/': '/base/src/assets/'},
exclude: ['./src/lib/**', './src/config/**.js'],
preprocessors: { './test/spec-bundle.js': ['coverage', 'webpack', 'sourcemap'] },
port: 9876, colors: true, logLevel: config.LOG_INFO,
browsers: ['ChromeHeadless'],
browserConsoleLogOptions: { terminal: true, level: 'log' },
concurrency: Infinity,
reporters: ['progress', 'coverage', 'remap-coverage'],
remapCoverageReporter: {
'text-summary': null, json: './coverage/coverage.json',
html: './coverage/html', cobertura: './coverage/cobertura.xml'
},
coverageReporter: { type: 'in-memory' },
webpack: testWebpackConfig,
webpackServer: { noInfo: true },
webpackMiddleware: { logLevel: 'warn', stats: { chunks: false } },
plugins: [ 'karma-*'],
client: { captureConsole: false, clearContext: true },
autoWatch: false,
singleRun: true
};
// 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);
};
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 }
}
入口文件加载项目主要依赖库,并加载所有 .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);
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 module = {
rules: [{
test: /\.pug$/,
use: [ 'raw-loader', { loader: 'pug-html-loader', options: { doctype: 'html' } } ]
},
// 参考: https://github.com/kevindqc/angular2-moduleid-loader
{ test: /\.component\.ts$/, loader:'./ng2-moduleid-loader' },
{
test: /\.ts$/,
use: [{ loader: 'awesome-typescript-loader', options: { configFileName: 'tsconfig.spec.json' } },
'angular2-template-loader'
],
exclude: [/\.e2e\.ts$/]
},
{
test: /\.css$/,
use: ['to-string-loader', { loader: 'css-loader', options: { url: false } }]
},
{
test: /\.less$/, use: ['raw-loader', { loader: 'less-loader' }]
},
{ test: /\.html$/, loader: 'raw-loader' },
// Instruments JS files with Istanbul
{
test: /\.ts$/, loader: 'istanbul-instrumenter-loader',
enforce: 'post',
include: helpers.root('src'),
exclude: [/\.(e2e|spec)\.ts$/, /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'
};
angular2+ 单元测试编写应参考官方指南官方测试指南。
应当注意:
.spec.ts
结尾命令配置参考(配置到 package.json
的 scripts
中):
"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
提示:由于实践中使用了 Gitlab-CI(见后文),Jenkins 的配置过程并未实际操作,本小节内容为搜集整理而来,仅供参考。
yarn add -D karma-sonarqube-unit-reporter
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');
}
https://npm.taobao.org/mirrors/node/latest-v10.x/
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
安装 Sonar TypeScript 插件 Sonar Typescript plugin
全局安装 sonar-scanner
npm i -g sonar-scanner
构建环境
中,选中 Prepare SonarQube Scanner environment
构建-Execute shell
中添加相关的执行命令:# 执行测试,会生成兼容 sonar 的测试结果到指定目录
npm run test:sonar
# 将测试执行的结果推送到 SonarCube
sonar-scanner
如构建成功,则可在 sonar 项目面板中看到相关执行报告:
通过配置 gitlab-ci,可以实现有新代码提交时立即执行单元测试,以尽早的获知错误的存在,而不是推迟到项目构建时才能发现。
在 windows 系统下,下载 docker-toolbox
,执行安装即可。
http://mirrors.aliyun.com/docker-toolbox/windows/docker-toolbox/
由于单元测试需要 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
配置 .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
karma.conf.js 增加对 gitlab-ci
执行环境的支持:
// for gitlab-ci
if (process.env.GITLAB_CI) {
configuration.browsers = ['Chromium_no_sandbox'];
configuration.customLaunchers = {
Chromium_no_sandbox: {
base: 'ChromiumHeadless',
flags: ['--no-sandbox']
}
};
}
覆盖率配置:
打开 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)
以上操作完成,提交一下代码到 gitlab,然后到 gitlab 项目的 pipelines
即可看到单元测试的执行。
端到端测试本质上算是黑盒式的 UI 功能测试。通过模拟浏览器上用户的行为操作,测试指定的操作行为得到的结果是否与预期相同。由于 e2e 编写涉及交互逻辑,过于复杂,耗时较多,而测试组本省有 UI 测试,我们对此不做要求。