什么是单元测试?
- 对软件中最小可测单元进行测试(如:一个方法function)
引入单元测试的原因?(一般是大的项目时)
- 分模块开发,方便的定位到哪个单元出了问题
- 保证代码质量
- 驱动开发(测试驱动开发)
- TDD:
- 测试驱动开发,从需求角度看,即我需要结果是什么,如果不是就是错误的。(需求分析-》编写单元测试-》编写代码使单元测试通过-》重构)
- BDD:
- 行为驱动开发,从具体功能角度出发看,即结果应该是什么,如果不是什么就出错。(从业务角度定义目标-》找到实现目标的方法-》编写单元测试-》实现行为-》检查产品)
- 先写测试用例,再开发满足测试用例
测试的原则
- 及时修改和维护
- Code review:代码审查
- 偶尔代码审核
- 同步
- 异步(借助工具)
- 只测单一的点
- 尽量贴近真实
- 避免测试中逻辑过于复杂
单元测试详解
单元测试的核心内容
- 测试框架
- 测试运行的基础
- 框架
Jest
:基于jasmine,对react友好jasmine
:自带assert,mockMocha
:全面适合node和浏览器两个端(适合自定义高的)Qunit
出自是Query
- 断言库
- 自己对自己的断言,判断结果
- 工具:
Chai
-支持所有风格【常用】,Assert
-node环境直接使用,Should,expect
-用得少
- Mock库
- 用于屏蔽其他方法的数据影响
- 工具:
sinon
- Test runner
- 跑测试的环境(跑前三个的环境)
- 工具:
karma
- 覆盖率工具
- 提供测试代码的覆盖率工具(单元测试覆盖了多少内容多少行代码)
- 检测工具:
istanbul
- 示例(测最小单元,对于复杂表单则需要拆分):
/*
* descript:新建一个测试
it:一个测试用例
expect: 期待结果
toequal:等于
*/
// 1. 对于有返回值的方法
descript('this is a test for xxx',()=>{
it('test function a',()=>{
expect(a()).toequal(2);
})
})
// 2. 对于没返回值的方法
descript('this is a test for xxx',()=>{
b(); // 将div.innerHTML设置为123
it('test function b',()=>{
expect(div.innerHTML).toequal('123');
})
})
Vue-Cli创建的项目:Mocha+Karma进行单元测试
Vue-cli 创建项目,选择mocha+karma进行unit test后创建项目
- 最终创建的项目,test目录为测试文件目录
|-- test
|-- karma.conf.js //karma配置文件
|-- unit
|-- coverage
|-- specs
|-- HelloWorld.spec.js //一个测试示例
|-- .eslintrc
|-- index.js // 引入测试目标文件(spec,coverage)
|--
karma.conf.js配置文件
- 一般来说不需要去修改这个配置文件,这里对配置文件中的一些属性名进行说明
module.exports = function karmaConfig (config) {
config.set({
// 1. 模拟环境,PhantomJS-无头浏览器,一个JS环境。
browsers: ['PhantomJS'],
// 2. 与karma搭配所用到的库, mocha-测试库,sinon-chai-断言和Mock库,phantomjs:无头浏览器用到的模拟环境
frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'],
// 3. 用到的文件夹, spec-单元测试,coverage:存覆盖率的
reporters: ['spec', 'coverage'],
// 4. 跑单元测试的起点
files: ['./index.js'],
// 5. 对代码的预处理(如下:对Index.js先用webpack)
preprocessors: {
'./index.js': ['webpack', 'sourcemap']
},
// 5.1 对webpack的额外配置
webpack: webpackConfig,
webpackMiddleware: {
noInfo: true
},
// 测试报告输出的配置
coverageReporter: {
dir: './coverage',
reporters: [
{ type: 'lcov', subdir: '.' },
{ type: 'text-summary' }
]
}
})
}
- PhantomJS:是一个基于webkit内核的简易浏览器包
第一个测试用例
-
npm run test
:跑测试(初始vue-cli的初始配置)- 这里出了个小插曲,首次run test时,报错
Cannot load browser "PhantomJS": it is not registered! Perhaps you are missing some plugin?
。原因是本机环境没有装过PhantomJS。 解决方案:
http://phantomjs.org/download.html这个网址下先去下载好 phantomjs-2.1.1-windows.zip这个包- 解压后,把包放到C盘里,C:\Users\Administrator\AppData\Local\Temp\phantomjs,
- 最后,在项目中重装依赖,再
npm run test
- 这里出了个小插曲,首次run test时,报错
-
第一个测试用例
// HelloWorld.spec.js
import Vue from 'vue'
import HelloWorld from '@/components/HelloWorld'
import axios from 'axios' //测试异步请求时用到
import Promise from 'es6-promise'
Promise.polyfill();
describe('HelloWorld.vue', () => {
const Constructor = Vue.extend(HelloWorld)
const vm = new Constructor().$mount()
it('should render correct contents', () => {
expect(vm.$el.querySelector('.hello h1').textContent)
.to.equal('Welcome to Your Vue.js App')
})
// 1. 带返回值的测试用例
it(`is m1 add two arguments`, () => {
// m1对应组件helloworld组件中的methods方法m1
const m1 = vm.m1
expect(m1(1, 2)).to.equal(3)
})
// 2. 一个简单异步方法的测试用例
it(`async m2 should return 4`, () => {
// m2对应组件helloworld组件中的methods方法m2
const m2 = vm.m2
m2(2, 2, (data) => {
expect(data).to.equal(4)
})
})
// 3. 一个接口请求的测试用例
it('接口被请求', () => {
// 借助工具来屏蔽不用的方法 spy,stub,MOCK 【sinon:一个mock库】
// spy:可以获取到对应函数的调用 信息,但是不能屏蔽,用于获取参数信息
// stub :会拦截掉这个方法的作用
let callback = sinon.stub(axios, 'get')
let axiosspy = sinon.spy(() => {
return 4;
})
// 获取HelloWorld组件的异步请求方法 getmes
const getmes = vm.getmes
expect(getmes(axiosspy)).to.equal(4);
callback.restore(); //使用sinon之后都要用resotre进行重置
console.log(axiosspy.callCount); //用于获取包装的get方法在getmes方法中被调用 了几次
})
})
- describe:描述测试
- it():新建一个测试用例
mocha的钩子函数
before
: 在执行所有用例之前after
: 在每个用例执行之前beforeEach
: 在用例执行完之后it.only()
: 只执行这一个用例(使用场景:当其他方法测试完了时,只需要测试新出的这个方法时,可以测这一个)
使用Jest库来测试
同样使用vue-cli创建一个初始项目,在选择unit test时,选择jest
npm run test
直接首次运行时会报错:Option "mapCoverage" has been removed, as it's no longer necessary....
- 解决办法:修改配置项
jest.conf.js
- 注释掉
mapCoverage: true,
- 新增两行
verbose: true,testURL:'http://localhost/',
- 注释掉
// jest.conf.js
const path = require('path')
module.exports = {
rootDir: path.resolve(__dirname, '../../'),
verbose: true,
testURL:'http://localhost/',
moduleFileExtensions: [
'js',
'json',
'vue'
],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
transform: {
'^.+\\.js$': '<rootDir>/node_modules/babel-jest',
'.*\\.(vue)$': '<rootDir>/node_modules/vue-jest'
},
snapshotSerializers: ['<rootDir>/node_modules/jest-serializer-vue'],
setupFiles: ['<rootDir>/test/unit/setup'],
//mapCoverage: true,
coverageDirectory: '<rootDir>/test/unit/coverage',
collectCoverageFrom: [
'src/**/*.{js,vue}',
'!src/main.js',
'!**/node_modules/**'
]
}
jest的一个简单测试用例
- jest测试用例的写法与mocha类似
- describe() : 新建一个测试
- it(): 一个测试用例
- expect: 断言
- toEqual(): 理想结果
import Vue from 'vue'
import HelloWorld from '@/components/HelloWorld'
describe('HelloWorld.vue', () => {
const Constructor = Vue.extend(HelloWorld)
const vm = new Constructor().$mount()
it('should render correct contents', () => {
expect(vm.$el.querySelector('.hello h1').textContent)
.toEqual('Welcome to Your Vue.js App')
})
it('just a test', () => {
const m1 = vm.m1;
expect(m1(1,2)).toEqual(3)
})
})