为什么需要测试

随着Web应用越来越复杂,它出错的可能性就会加大。为了保证应用不会出问题,我们需要一遍又一遍手动去点击UI界面检测我们代码是否正常。
这种手动方式耗时又不可靠。通过编写测试代码,可以提高我们编码质量,降低出错可能性。

测试类型

单元测试 (Unit testing)

单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。

单元测试是对应用中独立单位函数或类进行正确性校验。
单元测试的最基本原则: 输入条件相同则输出必定一样。
如果一个单位函数依赖于其他方法或数据,在测试时我们可以Mock依赖的方法或数据。
单元测试实例步骤:

  1. 准备测试条件
  2. 触发测试函数
  3. 验证结果是否正确

单元测试对被测单元要求:

  1. 被测单元内不存在可变因素,输入条件相同,输出结果必须相同
  2. 被测单元功能单一
  3. 被测单元对外依赖性低
    单元测试通常采取断言(assertion)的形式,也就是测试某个功能的返回结果,是否与预期结果一致。如果与预期不一致,就表示测试失败。
    单元测试可以保证代码的正确性,将出错可能性降到最低。

测试框架(实例+断言+运行时):

端到端测试(End to End Test)

模拟真实环境对应用进行全链路全流程测试被称作端到端测试。
端到端测试范围是整个应用:

1
从用户界面触发 --> 前端代码处理 --> 前端发起网络请求 --> 后端代码接受请求处理 --> 数据库处理 --> 后端代码返回请求处理 --> 前端接受网络反馈 --> 前端对反馈处理 --> 界面更新展示

端对端测试框架:WebDriver

集成测试(Integration test)

集成测试是单元测试的逻辑扩展。它的最简单的形式是:两个已经测试过的单元组合成一个组件,并且测试它们之间的接口。

测试模式

TDD (Test-Driven Development)

测试驱动的开发,是指先写好测试,再根据测试完成开发。
TDD模式一般会有很高的代码覆盖率。

BDD(Behavior-Driven Development)

行为驱动开发,用通用语言写用例描述软件行为的过程。
BDD接口提供下面六个方法:

  1. describe(name,fn) // 描述一组测试用例
  2. it(name,fn)/test(name,fn) //具体的测试用例
  3. before(fn) // 测试用例执行前的动作
  4. after(fn) // 测试用例执行后的动作
  5. beforeEach(fn) // 每个用例执行前的动作
  6. afterEach(fn) // 每个用例执行后的动作

BDD术语

套件 Describe

1
2
3
describe("A suite", function() {
// ...
});

测试套件(Test suit),一组针对软件某个功能点测试用例。
测试套件一般 describe 函数,第一个参数是字符串,描述测试套件。第二个参数是函数,实现测试套件。

用例 It

1
2
3
4
5
describe("A suite", function() {
it("contains spec with an expectation", function() {
// ...
});
});

测试用例(Test case),软件单位的测试实例。它是软件测试中最小测试单位。
测试用例一般 ittest 函数,第一参数是字符串,描述测试用例。第二个参数是函数,实现测试用例。

断言 Expect

1
2
3
4
5
describe("A suite", function() {
it("contains spec with an expectation", function() {
expect(true).toBe(true);
});
});

断言(assert)对被测单元行为描述和结果预期。一个测试用例可以包含一个或多个断言。
断言有三种风格:assert、expect、should

1
2
3
4
5
6
7
8
// assert风格
assert.equal(event.detail.item, '(item)');

// expect风格
expect(event.detail.item).to.equal('(item)');

// should风格
event.detail.item.should.equal('(item)');

一般expect风格更容易理解些。将被测试单元放入expect方法,后面用链式模式匹配预期的结果.to.equal()。

例子

一个计算器案例

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
<section id="screen">0</section>
<section id="keyboard">
<div class="keyboard-level">
<button onclick="clearFunction()" class="keyboard-key">CE</button>
<button onclick="cleanFunction()" class="keyboard-key">C</button>
<button onclick="cutFunction()" class="keyboard-key"><<</button>
<button onclick="signalFunction('%')" class="keyboard-key">%</button>
</div>
<div class="keyboard-level">
<button onclick="numFunction('7')" class="keyboard-key">7</button>
<button onclick="numFunction('8')" class="keyboard-key">8</button>
<button onclick="numFunction('9')" class="keyboard-key">9</button>
<button onclick="signalFunction('x')" class="keyboard-key">X</button>
</div>
<div class="keyboard-level">
<button onclick="numFunction('4')" class="keyboard-key">4</button>
<button onclick="numFunction('5')" class="keyboard-key">5</button>
<button onclick="numFunction('6')" class="keyboard-key">6</button>
<button onclick="signalFunction('-')" class="keyboard-key">-</button>
</div>
<div class="keyboard-level">
<button onclick="numFunction('1')" class="keyboard-key">1</button>
<button onclick="numFunction('2')" class="keyboard-key">2</button>
<button onclick="numFunction('3')" class="keyboard-key">3</button>
<button onclick="signalFunction('+')" class="keyboard-key">+</button>
</div>
<div class="keyboard-level">
<button onclick="stateFunction()" class="keyboard-key">+/-</button>
<button onclick="numFunction('0')" class="keyboard-key">0</button>
<button onclick="pointFunction()" class="keyboard-key">.</button>
<button onclick="resultFunction()" class="keyboard-key">=</button>
</div>
</section>
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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
var ValueDom = document.getElementById('screen'); // 界面DOM
var Value = []; //值
var State = ""; // 状态
var Signal = ""; // 符号
// 减少
function clearFunction() {
ValueDom.innerHTML = '0';
State = '';
}
// 清空
function cleanFunction () {
ValueDom.innerHTML = '0';
Value = [];
Singal = '';
State = '';
}
// 减位
function cutFunction () {
let value = ValueDom.innerHTML;
if(value == '0') {
return
} else if (value.length == 1) {
ValueDom.innerHTML = '0';
} else {
ValueDom.innerHTML = value.slice(0,-1);
}
}
// 符号操作
function signalFunction (optionType) {
if(ValueDom.innerHTML == '0') return;
Value[0]=(ValueDom.innerHTML);
ValueDom.innerHTML = '0';
Signal = optionType;
State = '';
}
// 数字操作
function numFunction (num) {
let newValue;
if(ValueDom.innerHTML == '0') {
newValue = num;
} else if (ValueDom.innerHTML != '0') {
newValue = ValueDom.innerHTML + num;
}
ValueDom.innerHTML = newValue;
}
// 修改数字正负值
function stateFunction () {
if(State == '') {
ValueDom.innerHTML = '-'+ValueDom.innerHTML;
State = '-';
} else if (State == '-') {
ValueDom.innerHTML = ValueDom.innerHTML.slice(1);
State = '';
}
}
// 小数
function pointFunction () {
ValueDom.innerHTML = ValueDom.innerHTML + '.';
}
// 结果
function resultFunction () {
if(Value.length == 0 || Signal =='' ||ValueDom.innerHTML == '0') return;
let result = calc(parseFloat(Value[0]),parseFloat(ValueDom.innerHTML),Signal);
if(result < 0) State = '-';
ValueDom.innerHTML = result;
}
// 计算
function calc (value1,value2,optionType) {
let result;
switch (optionType) {
case '+':
result = parseFloat((value1*1000 + value2*1000)/1000);
break;
case '-':
result = parseFloat((value1*1000 - value2*1000)/1000);
break;
case '%':
result = value1/value2;
break;
case 'x':
result = value1*value2;
break;
}
return result;
}

业务代码测试

单元测试框架:Mocha+Chai

1
2
$ yarn add mocha chai -D
$ mocha init test

Mocha 会在 test 文件夹下创建几个文件

|— index.html // 执行测试和结果展示
|— mocha.css
|— mocha.js
|— index.test.js //测试代码

index.html 执行测试和结果展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html>
<head>
<title>Mocha</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="mocha.css" />
</head>
<body>
<div id="mocha"></div>
<script src="mocha.js"></script>
<script>mocha.setup('bdd');</script>
<script src="../index.js"></script> //被测试代码
<script src="chai.js"></script> // chai文件
<script src="index.test.js"></script> // 测试代码
<script>
mocha.run(); // 启动测试
</script>
</body>
</html>
1
2
3
4
5
6
var expect = chai.expect;
describe('demo', function () {
it('1+2',function () {
expect(calc(1,2,'+')).to.equal(3); // 测试index.js 文件中 calc 方法
})
});

执行 index.js 文件中的 calc方法 expect 会捕获到放回结果,然后用 to.equal方法进行值比对。
chai Api
.ok — 判断目标值是否为正确

1
2
3
4
expect('everything').to.be.ok;
expect(1).to.be.ok;
expect(false).to.not.be.ok;
expect(undefined).to.not.be.ok;

.true — 判断目标是否为true

1
2
expect(true).to.be.true;
expect(1).to.not.be.true;

.change(function)
@param { String } object
@param { String } property name
@param { String } message _optional_
目标函数是否改变对象某个属性值

1
2
3
4
5
var obj = { val: 10 };
var fn = function() { obj.val += 3 };
var noChangeFn = function() { return 'foo' + 'bar'; }
expect(fn).to.change(obj, 'val');
expect(noChangeFn).to.not.change(obj, 'val')

界面测试

selenium-webdriver一个自动化测试库。 以一种更底层、更灵活的方式来操作浏览器,支持的框架、弹出窗口、页面导航、下拉菜单、基于AJAX的UI元素等控件的操作。它支持多种语言开发:java、ruby、php、javasrcript

yarn add selenium-webdriver -D

1
2
3
4
5
6
7
8
9
10
11
12
13
	
const webdriver = require('selenium-webdriver');
const Capabilities = require('selenium-webdriver/lib/capabilities').Capabilities;
By = webdriver.By,
until = webdriver.until;
var capabilities = Capabilities.firefox();
capabilities.set('marionette', true);

var driver = new webdriver.Builder().withCapabilities(capabilities).build();
driver.get('http://localhost:9000/');
driver.findElement(By.xpath('//*[@id="keyboard"]/div[2]/button[1]')).click();
var screenValue = driver.findElement(By.id('screen'));
driver.wait(until.elementTextIs(screenValue,'7'),10);

使用firebox浏览器进行模拟测试。
selenium-webdriver 提供了丰富的Api (元素定位(By),点击操作(click),比较元素值(until.elementTextIs)),可以真实的模拟用户操作。

selenium-webdriver 常用API:

webdriver
控制浏览器行为的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
this.get(url) // 打开某个链接 返回一个promise对象  
this.getTitle() // 获取站点title 返回一个promise对象

var web = driver.get("http://www.google.com");
web.then(()=>{
let title = driver.getTitle();
title.then(s=>console.log(s));
});
this.quit() // 结束当前会话
this.wait(condition,time,message) // 延迟执行

var button = driver.wait(until.elementLocated(By.id('foo')), 10000);
button.click();
this.findElement(locator) // 根据locator获取元素在页面位置,返回WebElementPromise
driver.findElements(By.id('hplogo'))
.then(found => console.log('Element found? %s', !!found.length));

By
通过不同条件,获取元素在页面中的定位。

1
2
3
4
By.className(name) // 根据元素class 名称定位,返回 new locater  
By.id(id) // 根据元素id定位,返回new locater
By.name(name) // 根据元素name定位,返回new locater
By.xpath(xpath) // 根据元素xpath定位,返回 new locater

until
提供验证结果的方法,方法返回的值可以做为WebDriver wait的条件,从而验证方法是否正确调用。

1
2
until.titleIs(title) // 验证站点title,返回一个条件。    
until.elementTextIs(element, text) // 验证元素text,返回一个条件。