一直知道前端也是有测试的,但理解很肤浅,今天下定决心摸索一遍到底什么是前端自动化测试…

本例子通过vue-cli生成的unite2e来探讨…

基础名词

一些前端测试的名词解释:

  • karma是一个基于Node.jsJavaScript测试执行过程管理工具,其在测试中的作用相当于开发构建中使用的webpack

  • karma-webpack连接karmawebpack的桥梁。不经过webpack编译命令是文件是无法独立运行的,karma需要了解你的webpack配置,决定如何处理你的测试文件。

  • karma-phantomjs-launcherphantomjskarma中的启动器,由此引出了PhantomJS,一个基于 webkit 内核的无头浏览器,即没有 UI 界面,即它就是一个浏览器,只是其内的点击、翻页等人为相关操作需要程序设计实现。在查找相关资料时,也发现了其他的常规浏览器launcher,比如:Chrome、Firefox、Safari、IE 等,以应对不同浏览器或多浏览器的测试需求。见Browsers

  • karma-sourcemap-loader一个Karma插件,生成文件的sourcemap

  • karma-mocha让你在karma中使用Mocha一款功能丰富的javascript单元测试框架,它既可以运行在nodejs环境中,也可以运行在浏览器环境中

  • karma-sinon-chai让你在karma中使用sinon-chai断言库的插件, 提供丰富的断言方法,前置依赖有sinon-chaisinonchai

  • karma-spec-reporter用于将测试结果显示到控制台。

  • karma-coverage用来生成代码覆盖率。

  • Nightwatch是一套基于 Node.js 的测试框架,使用 Selenium WebDriver API 以将 Web 应用测试自动化。它提供了简单的语法,支持使用 JavaScript 和 CSS 选择器,来编写运行在 Selenium 服务器上的端到端测试。

相关配置

unit目录结构,主要测试单元是一个个函数、方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
└── unit
├── coverage 代码覆盖率报告,src下面的index.html可以直接用浏览器打开
│ ├── lcov-report
│ │ ├── base.css
│ │ ├── index.html
│ │ ├── prettify.css
│ │ ├── prettify.js
│ │ ├── sort-arrow-sprite.png
│ │ ├── sorter.js
│ │ └── src
│ │ ├── App.vue.html
│ │ ├── components
│ │ │ ├── Hello.vue.html
│ │ │ └── index.html
│ │ └── index.html
│ └── lcov.info
├── index.js 运行测试用例前先加载的文件,方便统计代码覆盖率
├── karma.conf.js karma的配置文件
└── specs 所有的测试用例都放在这里
└── Hello.spec.js

karma.conf.js内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
module.exports = function (config) {
config.set({
// 要启动的测试浏览器
browsers: [ 'Chrome'],
// 测试框架
frameworks: ['mocha', 'sinon-chai'],
// 测试报告处理
reporters: ['spec', 'coverage'],
// 要测试的目标文件
files: ['./index.js'],
// 忽略的文件
exclude: [],
// 预处理文件
preprocessors: {
'./index.js': ['webpack', 'sourcemap']
},
// webpack
webpack: webpackConfig,
webpackMiddleware: {
noInfo: true
},
// Coverage options
coverageReporter: {
dir: './coverage',
reporters: [
{ type: 'lcov', subdir: '.' },
{ type: 'text-summary' }
]
},
// true: 自动运行测试并退出
// false: 监控文件持续测试
singleRun: true,
// 以下是 vue-cli 没有生成的一些配置
// 文件匹配的起始路径
// basePath: '',
// 服务器端口
// port: 9876,
// 输出着色
// colors: true,
// 日志级别
// LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
// logLevel: config.LOG_INFO,
// 监控文件更改
// autoWatch: true,
// 超时处理,6s内没有捕获浏览器将终止进程
// captureTimeout: 6000
})
}

index.js入口文件

1
2
3
4
5
6
7
8
9
10
// 加载所有的测试用例、 testsContext.keys().forEach(testsContext)这种写法是webpack中的加载目录下所有文件的写法

// 匹配的是specs目录,里面是存放的是测试用例
const testsContext = require.context('./specs', true, /\.spec$/)
testsContext.keys().forEach(testsContext)

// 加载所有代码文件,方便统计代码覆盖率
// 匹配的是src目录,除了main.js以外的所有文件。
const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/)
srcContext.keys().forEach(srcContext)

e2e目录结构,测试的单元是一个个预期的行为表现,打开游览器,模拟测试

1
2
3
4
5
6
7
8
9
10
├── e2e
│ ├── custom-assertions
│ │ └── elementCount.js 自定义的断言方法
│ ├── nightwatch.conf.js nightwatch的配置文件
│ ├── reports
│ │ ├── CHROME_60.0.3112.101_Mac\ OS\ X_test.xml
│ │ └── CHROME_60.0.3112.113_Mac\ OS\ X_test.xml
│ ├── runner.js bootstrap文件,起我们的页面servernightwatch文件
│ └── specs
│ └── test.js 测试用例

nightwatch.conf.js内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
src_folders: ['test/e2e/specs'],
output_folder: 'test/e2e/reports',
custom_assertions_path: ['test/e2e/custom-assertions'],
// 对selenium的配置
selenium: {
start_process: true,
server_path: require('selenium-server').path,
host: '127.0.0.1',
port: 4444,
cli_args: {
'webdriver.chrome.driver': require('chromedriver').path
}
},
// 测试环境的配置
test_settings: {
default: {
selenium_port: 4444,
selenium_host: 'localhost',
silent: true,
globals: {
devServerURL: 'http://localhost:' + (process.env.PORT || config.dev.port)
}
},

chrome: {
desiredCapabilities: {
browserName: 'chrome',
javascriptEnabled: true,
acceptSslCerts: true
}
},

firefox: {
desiredCapabilities: {
browserName: 'firefox',
javascriptEnabled: true,
acceptSslCerts: true
}
}
}

runner.js入口文件内容,先起一个我们的网页服务然后再起 nightWatch 服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var server = require('../../build/dev-server.js')

server.ready.then(() => {
// 2. run the nightwatch test suite against it
// to run in additional browsers:
// 1. add an entry in test/e2e/nightwatch.conf.json under "test_settings"
// 2. add it to the --env flag below
// or override the environment flag, for example: `npm run e2e -- --env chrome,firefox`
// For more information on Nightwatch's config file, see
// http://nightwatchjs.org/guide#settings-file
var opts = process.argv.slice(2)
console.log(opts);
if (opts.indexOf('--config') === -1) {
opts = opts.concat(['--config', 'test/e2e/nightwatch.conf.js'])
}
if (opts.indexOf('--env') === -1) {
opts = opts.concat(['--env', 'chrome,firefox'])
}

var spawn = require('cross-spawn')
var runner = spawn('./node_modules/.bin/nightwatch', opts, { stdio: 'inherit' })

runner.on('exit', function (code) {
server.close()
process.exit(code)
})

runner.on('error', function (err) {
server.close()
throw err
})
})

工具详解

chai

定义几个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
const math = {
add: (...args) => args.reduce((num, value) => num + value),
mul: (...args) => args.reduce((num, value) => num * value),
cover: (a, b) => {
if (a > b) {
return a - b
} else if (a == b) {
return a + b
} else {
return -1
}
}
}

node自带的断言测试

1
2
3
const assert = require('assert')
const {add, mul} = require('./math')
assert.equal(add(2, 3), 5)

引入chai库测试,3 个方法作用一样,断言分格不同而已

1
2
3
4
5
6
7
8
9
10
const chai = require('chai')
// should
chai.should()
add(2, 3).should.equal(5)
// expect
consr expect = chai.expect
expect(add(2, 3).to.be(5)
// assert
consr assert = chai.assert
assert.equal(add(2, 3), 5)

expect断言的优点是很接近自然语言,下面是一些例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 相等或不相等
expect(4 + 5).to.be.equal(9);
expect(4 + 5).to.be.not.equal(10);
expect(foo).to.be.deep.equal({ bar: 'baz' });

// 布尔值为true
expect('everthing').to.be.ok;
expect(false).to.not.be.ok;

// typeof
expect('test').to.be.a('string');
expect({ foo: 'bar' }).to.be.an('object');
expect(foo).to.be.an.instanceof(Foo);

// include
expect([1,2,3]).to.include(2);
expect('foobar').to.contain('foo');
expect({ foo: 'bar', hello: 'universe' }).to.include.keys('foo');

// empty
expect([]).to.be.empty;
expect('').to.be.empty;
expect({}).to.be.empty;

// match
expect('foobar').to.match(/^foo/);

基本上,expect断言的写法都是一样的。头部是expect方法,尾部是断言方法,比如equal、a/an、ok、match等。两者之间使用 to 或 to.be 连接

如果expect断言不成立,就会抛出一个错误。只要不抛出错误,测试用例就算通过

Mocha

Mocha的作用是运行测试脚本,首先必须学会写测试脚本。所谓”测试脚本”,就是用来测试源码的脚本
Mocha默认运行test子目录里面的测试脚本 添加--recursive参数可以运行test目录下所有层数用例基本用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
describe('#math', () => {
describe('add', () => {
it('should return 5 when 2 + 3', () => {
assert(add(2, 3), 5)
})
})
describe('mul', () => {
it('should return 6 when 2 * 3', () => {
assert(mul(2, 3), 6)
})
// 只执行此条
it.only('should return 6 when 2 * 3', () => {
assert(mul(2, 3), 6)
})
// 忽略此条
it.skip('should return 6 when 2 * 3', () => {
assert(mul(2, 3), 6)
})
})
describe('mul', () => {
it('should return -1 when 2 < 3', () => {
assert(cover(2, 3), -1)
})
it('should return 1 when 3 > 2', () => {
assert(cover(3, 2), 1)
})
it('should return 4 when 2 = 2', () => {
assert(cover(2, 2), 4)
})
})
})

异步例子:
it块执行的时候,传入一个done参数,当测试结束的时候,必须显式调用这个函数,告诉Mocha测试结束了需要用-t--timeout参数,改变默认的(2000)超时设置。

1
2
3
4
5
6
7
8
9
10
11
// $ mocha -t 5000 timeout.test.js

it('测试应该5000毫秒后结束', done => {
var x = true;
var f = function() {
x = false;
expect(x).to.be.not.ok;
done(); // 通知Mocha测试结束
};
setTimeout(f, 4000);
});

测试用例的钩子:
Mochadescribe块之中,提供测试用例的四个钩子:before()、after()、beforeEach()和afterEach()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
describe('hooks', function() {

before(function() {
// 在本区块的所有测试用例之前执行
});

after(function() {
// 在本区块的所有测试用例之后执行
});

beforeEach(function() {
// 在本区块的每个测试用例之前执行
});

afterEach(function() {
// 在本区块的每个测试用例之后执行
});

// test cases
});

benchmark

benchmark是一个测试函数性能的库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var suite = new Benchmark.Suite;
// add tests
suite.add('RegExp#test', function() {
/o/.test('Hello World!');
})
.add('String#indexOf', function() {
'Hello World!'.indexOf('o') > -1;
})
.add('String#match', function() {
!!'Hello World!'.match(/o/);
})
// add listeners
.on('cycle', function(event) {
console.log(String(event.target));
})
.on('complete', function() {
console.log('Fastest is ' + this.filter('fastest').map('name'));
})
// run async
.run({ 'async': true });

Puppeteer

Puppeteer是类似于Nightwatch的一个Chrome专用版,有更友好的 api,是用来测试游览器环境的一个工具也可用于爬虫,比如这个demo演示了爬取百度图片,相较于cheerio,它的爬虫更模拟真实环境,不易反爬虫

1
2
3
4
5
6
7
8
9
10
const puppeteer = require('puppeteer');

(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
await page.screenshot({path: 'example.png'});

await browser.close();
})();

Jest

哦,是不是这一大堆东西看晕了,用个karma还要集成一大堆各种插件配置,这一点上真是跟webpack一样了就像有人受不了webpack这一大堆配置所以有了前端构建集成工具Parcel
Jest就是这样一个前端测试集成工具
Jest的官方文档支持中文,这里就不详细说明了,有兴趣可以去官网查看相比于karma最大特点就是快和方便,缺点就是没有karma测试环境真实和自由具体抉择,仁者见仁智者见智啦~~