前端测试(上)
在前端项目中,前端测试并没有被重视,原因有很多,比如 学习/研发成本高,团队不够重视,或者项目不合适等,在这里我们不去追究是什么原因导致这种现象,但是有一点我很确定,造成这种原因,还有一个更重要的原因,就是 “意识不到位”,即使有很多同学了解过单元测试,但是也不知道如何应用到 “项目” 中,针对这种现象,我们从一个简单却很常见的小项目,来打开测试工程化
冰山一角
在刷题的过程中,我们经常会使用一个项目用于练习写笔试题,比如排序,查找之类的算法题目
石器时代
新建一个工程,目录如下
├── index.js
├── index.html
└── src
└── search.js
- index.html - 基于浏览器运行,至少早期的笔试题目,基于浏览器即可;此例,引入
search.js
和index.js
即可 - index.js - 用于写测试代码,比如
console.log(binarySearch([1],1)===0)
- search.js - 用于写查找相关的算法代码,比如顺序查找、插值查找等,在这里我只写
二分法查找
和顺序查找
函数
/**
* 二分法
* @param {Array} arr
*/
function binarySearch(arr, expected) {
let start = 0
let end = arr.length - 1
while (start <= end) {
let mid = parseInt(start + (end - start) / 2)
let value = arr[mid]
if (value === expected) {
return mid
} else if (value > expected) {
end = mid
} else {
start = mid
}
}
return -1
}
/**
* 顺序查找
* @param {*} arr
* @param {*} expected
*/
function sequentialSearch(arr, expected) {
let i = 0
while (i > arr.length) {
let value = arr[i]
if (value === expected) {
return i
}
i++
}
return -1
}
OK,大功告成,把页面拖到浏览器中直接运行,连服务器都省了~~!
当我准备为我这个完美的项目鼓掌的时候,眼角瞟到我的座右铭,成为一个专业的大前端,此时此刻 专业这个词格外刺眼,作为新世纪好青年,我怎么可能让别人质疑我的专业,于是我要继续装(入)逼(坑)
青铜时代
- 使用npm创建工程,并且对模块进行管理
- 使用git对项目仓库以及迭代进行管理
- 载体nodejs环境代替浏览器环境,这样可以测试文件读取等IO操作
.
├── node_modules
└── test
└── test.js
└── src
└── search.js
├── package.json
├── .gitignore
├── index.js
在 package.json 配置
{
....
"scripts":{
"test":"node test/test.js"
}
}
对应 js的模块 要改成commonjs
规范
search.js 调整
function binarySearch(){
//todo
}
function sequentialSearch(){
//todo
}
module.exports = {
binarySearch,
sequentialSearch
}
index.js 调整
const { binarySearch,sequentialSearch } = require('./src/search')
module.exports = {
binarySearch,
sequentialSearch
}
test.js 调整,为了让提示更加明显点,我们尝试让描述更加丰富点
const { binarySearch,sequentialSearch } = require('../index')
console.log(‘二分查找: [1]的1在数组0位置上’,binarySearch([1],1)===0)
console.log(‘二分查找:[1,2,3]的1在数组0位置上’,binarySearch([1,2,3],1)===0)
console.log(‘二分查找:[1,2,3]的2在数组1位置上’,binarySearch([1,2,3],2)===0)
console.log(‘顺序查找:[1]的1在数组0位置上’,sequentialSearch([1],1)===0)
console.log(‘顺序查找:[1,2,3]的1在数组0位置上’,sequentialSearch([1,2,3],1)===0)
console.log(‘顺序查找:[1,2,3]的2在数组1位置上’,sequentialSearch([1,2,3],2)===0)
一顿操作猛如虎之后,感觉完美的一笔~~
我迫不及待,运行 npm run test
二分查找:[1]的1在数组0位置上 true
二分查找:[1,2,3]的1在数组0位置上 true
二分查找:[1,2,3]的2在数组1位置上 false
顺序查找:[1]的1在数组0位置上 false
顺序查找:[1,2,3]的1在数组0位置上 false
顺序查找:[1,2,3]的2在数组1位置上 false
我们发现 有几点不足:
- 当测试用例增加时,测试代码变的难以管理,所有测试输出揉在一起,没有分组的管理
- 不管成功或者失败,没有高亮显示,也没有颜色上的区分
- 即使错误,也没有把错误详细打印出来
- 没有相关输出报表
黄金时代
为了解决 青铜时代 遗留下不少体验问题,我们不得不封装一些方法,强化console的输出,文档输出,可视化等输出,然而我们所做的一切强化,都是新概念 测试框架的雏形,不过在正式介绍 测试框架前,我们先了解下 断言
“我×,测试框架?断言?这尼玛又是什么?”
断言是单元测试中用来保证最小单元是否正常的检测方法,用于判断逻辑执行是否达到开发者预期的表达式,断言在运行的过程中,若断言不为真,程序会中止运行
“常用的断言库有哪些?”
- assert - nodejs的内置核心模块,node环境可以直接使用
- shouldjs - 基于assert模块进行封装扩展
- expectjs - 基本是 shouldjs 的缩水版
- chai - 目前比较流行的断言库,支持 TDD(assert),BDD(expect、should)两种风格
我们先简单学习 assert, 作为Nodejs内置核心模块,无需引用,最为 断言 入门库最为合适
## assert
var assert=require('assert')
assert.equal(Math.max(1,100),100)
一旦 assert.equal()不满足期望,将会抛出AssertionError
异常,整个程序将会停止运行
常用的检测方法
- ok(actual) - 判断结果是否为真
- strictEqual(actual,expected,[,message]) - 判断实际值和期望值是否严格相等
- deepStrictEqual(actual, expected[, message]) -判断实际值和期望值是否深度严格相等
- doesNotReject(asyncFn, error) - 判断代码块是否返回reslove
- rejects(block, error)- 判断结果返回reject
- throws(block, error)- 判断结果是否抛出异常
- ifError()- 判断实际值是否为一个假值(null,undefined,0,'',false);如果为真值,就会抛出异常
- fail([message]) - 直接报错
“感觉脑壳疼,能不能通俗点?”
先来一个例子,压压惊,我们把青铜时代的代码优化下
console.log(‘顺序查找:[1]的1在数组0位置上’,sequentialSearch([1],1)===0)
//为了通用性,我们把sequentialSearch([1],1)===0 提炼出来
function equal(actual,expected,message){
return actual===expected?message:`${actual}!==${expected}`
}
console.log(‘顺序查找:[1]的1在数组0位置上’,equal(sequentialSearch([1],1),0,'成功'))
通俗的说 就是 equal
这个方法就是断言
“我迷迷糊糊的貌似明白了一点,那我运行一下尝尝鲜吧”
test/index.js
const assert = chai.assert
const { binarySearch } = require('../index')
assert.equal(binarySearch([1], 1), 0)//成功
assert.equal(binarySearch([1], 1), 1)//失败
assert.equal(binarySearch([1,2], 2), 1)//成功
assert.equal(binarySearch([1,2], 1), 0)//失败
运行 node test/index.js
//失败输出
AssertionError: expected 0 to equal 1
at Object. (F:\learn\test\index.js:19:8)
“呃....我觉得这体验,也青铜时代差不多”
我们可以看到,在第二个测试用例执行时,发现代码执行失败后,直接退出程序,同时提示你 期望值
和实际运行值
,以及对于错误代码相关提示
等等。错误提示方面比封装equire
方法强大不少;但是,依旧不能让我愿意使用它。
- 成功时,没有任何提示
- 虽然有错误提示,但是运行到第一个错误的时候,就程序退出;开发者无法看到 自己的测试用例 错误多少个
- 依旧没有高亮,可视化方面依旧苍白
没错,断言拿到非常重要错误信息;但是他没有解决体验问题;如果说 断言是里子,那测试框架 就是面子
“测试框架是什么?”
测试框架 通俗的说就是专门 服务于代码块测试 的解决方案,他主要有以下功能
- 管理测试用例
- 生成测试报告
“常用的测试框架有哪些?”
- [jasmine]() -自带断言(assert),mock 功能
- [mocha]() -框架不带断言和mock功能,需要结合其他工具
通俗的说,测试框架 就是 管理/执行断言,他和断言一起使用将会更加强大
Mocha
mocha 是一款强大的测试框架,能够运行在nodejs和浏览器中,能够高效的管理测试用例,支持多种测试报告格式
- 支持多种断言库:chai/shouldjs/expectjs/assert
-
支持两种测试风格:TDD/BDD
- TDD:基于测试用例 进行测试
- BDD:基于产品本身功能 进行测试
常用方法
- describe(string,callback) -主要用于对测试用例的分组,层级描述。
TDD
使用suite
- it(string [,callback])-测试用例,callback 包含一个或者多个断言;当callback不存在时,表示这个测试用例需要写,但目前还未写入,状态使用
pending
表示 -
hook-用于协助
describe
中测试用例的准备·安装·卸载和回收等工作,Hook
一般用于describe
内,但也可以describe
外,作为顶级Hook
- before/after([ string ,]callback) - 分别在进入或者退出
describe
时触发执行 - beforeEach/afterEach([ string ,]callback) - 分别在
describe
中每个测试用例执行前和执行后触发执行
- before/after([ string ,]callback) - 分别在进入或者退出
Full example
test/index.js
describe('hooks', function() {
before(function() {
console.log('before')
});
after(function() {
console.log('after')
});
beforeEach(function() {
console.log('beforeEach')
});
afterEach(function() {
console.log('afterEach')
});
it('Test1',()=>{
console.log('test1')
})
it('Test2',()=>{
console.log('test2')
})
// test cases
});
运行 npm run test
{
"script":{
" test":"mocha"
}
}
hooks
before
beforeEach
test1
√ Test1
afterEach
beforeEach
test2
1) Test2
afterEach
after
1 passing (15ms)
1 failing
1) hooks
Test2:
AssertionError: expected 0 to equal 1
+ expected - actual
-0
+1
at Context.it (test\index.js:93:12)
我们可以看到 基于mocha后的断言,他的提示体验大大的提升
- 成功后,有相关提示
- 遇到失败时,依旧可以执行下去,展示所有失败用例信息
- 统计测试用例成功数和失败数
4. **异步处理**
- done
it('should save without error', (done)=> {
var user = new User('Luna');
user.save((err)=> {
if (err) done(err);
else done();
});
//user.save(done);
});
```
- promise
```ts
it('respond with matching records', ()=> {
return db.find({type: 'User'}).should.eventually.have.length(3);
});
```
- async/await
```ts
it('responds with matching records', async function() {
const users = await db.find({type: 'User'});
users.should.have.length(3);
});
```
-
Only-屏蔽其他测试单元/测试用例,只执行标识为Only的测试单元/用例。一般用于 当你的单元测试越写越多时,只想测试新写的单元测试是否正确,这个属性就可以帮你在执行时,帮你过滤掉其他测试,加快执行速度
describe.only('something', function() { // 只会跑包在里面的测试 })
或者
it.only('do do', () => { // 只会跑这一个测试 })
-
skip-表示执行时,跳过标识的测试单元/测试用例,可以作用于
describe
和it
it.skip('should return -1 unless present', function() { // 代码不存被执行 }); it('should return the index when present', function() { // 代码会执行 });
可以
this.skip()
在测试用例执行的时候,根据运行时过滤当前测试案例describe('outer', function() { before(function() { this.skip(); }); after(function() { // will be executed }); describe('inner', function() { before(function() { // will be skipped }); after(function() { // will be skipped }); }); });
- 其他
- timeout - 超时