文章目录
测试策略
测试并不能直接减少bug,但可以间接减少代码反复修改出现的bug(每次修改极可能破坏原有系统功能)
测试有:
- 单元测试
- 组件测试(含快照测试Snapshot)
- 契约/接口测试(从api级别对代码进行测试)
- E2E测试。端对端测试,集成测试(从用户的级别进行测试)
单元测试:测试一个模块,对一个单元进行测试
集成测试:多个模块测试,同时对单元内的其他文件、依赖做统一做了测试
单元测试
步骤:
1)选单元测试框架
2)创建测试规则
3)制定测试覆盖率的最小值
一般都采用三段式风格:given-when-then
- 假设:一个上下文,指定完成测试所需要的条件
- 当:进行一系列操作,即执行操作,如单机某个按钮
- 那么:得到可观察的后果,即检测断言,如判单按钮是否被隐藏
正常情况下,应该测试大部分函数,但没有足够的时间,那就可以遵循测试的规则:
- 必须进行测试的式通用、公用的utils函数
- 复杂交互操作需要进行一定的测试
- 网络请求可以交给契约测试,或者不进行测试
单元测试通常只用于测试代码中的逻辑,而对于隐藏在模板中的逻辑来说,单元测试往往难以进行测试。因此,在编写业务逻辑的时候,尽可能把代码写在 JavaScript 或 TypeScript 代码中。可以尝试在项目中采用驱动测试开发,以测试来驱动业务逻辑。采用这种方式来开发,可以保证功能都得到测试
组件测试
组件测试时指对项目中编写的组件进行测试。对组件测试的两部分
- 组件功能测试,即单元测试
- 组件行为的测试,当点击组件的按钮,除了对应的模型、数据发送变化。对应的dom也发生改变,对dom的行为测试–快照测试。
快照常用于测试组件的配置文件,比如生成一个快照,你再增加新参数时,会提示你是否通过这个参数,如果通过就按w进入模式,按u更新成新的快照
就是运行测试的时候,把结果存一份,之后可以用来对比,对比不上就测试不通过
契约/接口测试
契约测试Contract Test,即对前后端约定的api进行测试
测试对象不同
E2E测试
其表现行为类似真实的用户行为:启动浏览器,模拟用户行为进行操作。
测试要花较长的时间编写,由于需要真实运行,运行时间也较长
测试场景:经常出错的、核心的功能
angular有自己的E2E测试框架Protractor
在选定E2E测试的时候,可以考虑是否采用BDD的方式进行:
BDD,英文全称 Behavior Driven Development,中文含义为行为驱动开发,它是一种敏捷软件开发的技术,它鼓励软件项目中的开发者、QA 和非技术人员或商业参与者之间的协作。
与一般的自动化测试(如单元测试、服务测试、UI 测试)不一样,BDD 是多方参与的测试开发方式。
如在使用 Protractor 写 Angular 的 E2E 测试的时候,所有的测试都是前端测试人员编写的。
BDD 最重要的一个特性是:由非开发人员编写测试用例,而这些测试用例是使用自然语言编写的 DSL(领域特定语言)
// BDD测试在代码底层的实现
/* 当我在网站的首页,输入用户名demo、密码123
结果:提交登录信息,用户跳转到欢迎页面
*/
defineSupportCode(function(Given, When, Then){
Given('当我在网站的首页', function(){
return this.driver.get('http://0.0.0.0:7272/')
})
When('输入用户名{string}', function(text){
return this.driver.findElement(By.id('username_field')).sendKeys(text)
})
Then('输入密码{string}', function(text){
return this.driver.findElement(By.id('password_field')).sendKeys(text)
})
})
expect 断言
vscode 工具jest
jest基础api -> 异步测试 -> mock技巧
快照 -> timer测试 -> Dom测试
vue:Vue-test-utils
react:Enzyme
tdd 单元测试 -> bdd集成测试
测试底层原理
底层原理:一段测试代码,通过这段代码去运行其他代码,预期他的结果,然后判断输出结果是否与预期相等
function expect(result){ // 实际结果
return {
toBe: function(actual){ // 预期
if(result !== actual){
throw new Error(`预期值和实际值不相等,预期为${actual},实际结果为${result}`)
}else{
console.log('通过测试')
}
}
}
}
function test(desc, fn){
try{
fn()
}catch(e){
console.log(`${desc} 没有通过测试`)
}
}
function add(a, b){
return a+b
}
test('测试加法 3+7', ()=>{
expect(add(3, 7)).toBe(8)
})
jest基础
npm i jest@24.8.0 -D // 当安装了jest其实就顺带安装了babel-jest
"test":"jest" // 运行时会去找xxx.test.js的后缀进行测试
"test":"jest --watchAll" // 监听所有文件,自动跑测试用例,不用每次修改去重新启动
// 初始化配置
npx jest --init
npx jest --coverage // 生成代码覆盖率报告
// 修改jest.config.js
coverageDirectory:"dellee" // 会把报告生成到dellee文件夹,意思就是把覆盖率报告生成到哪个文件夹下
npm @babel/core@7.4.5 @babel/preset-env@7.4.5 -D
// .babelrc
{
"presets":[
["@babel/preset-env",{ //第二个参数: 是@babel/preset-env的配置项
"targets":{
"node":"current" // 根据当前node环境下去做代码进行转换
}
}]
]
}
匹配器 matchers
- tobe 就是匹配器,类似Object.is
- toEqual 只会匹配内容,不会匹配引用
- toBeNull 判断是不是空内容 null(不能用来测试undefined)
- toBeUndefined 判断是不是undefined (空字符串和null都不通过)
- toBeDefined判断是否被定义过的(undefined|)
- toBeTruthy 是否为真(0,‘’ 等都不通过)
- toBeFalsy 是否为假
- not 取反的匹配器
- 数字
- toBeGreaterThan(a) 比某个数a大
- toBeLessThan(a) 比某个数a小
- toBeGreaterThanOrEqual(a) 大于等于a
- toBeCloseTo 用于浮点数 expect(0.1+0.2).toBeCloseTo(0.3)为true
- 字符串
- toMatch(a)包含某个字符串a
- Array、Set
- toContain(a)数组里是否包含某个对象a expect(arr).toBeCloseTo(‘a’)
- 异常
- toThrow 能否抛出异常
// 测试对象内容
test('测试10与10相匹配', ()=>{
const a={one:1}
expcet(a).toBe({one:1}) // 因为引用地址不同,所以为false
expcet(a).toEqual({one:1}) // true
})
test('测试对象内容相等', ()=>{
const a=nul
expcet(a).toBeNull()
const b=1
expect(b).not.toBeFalsy() // 判断是否不等于false,所以为true
})
// 能否抛出异常
const throwNerrorFunc=()=>{
throw new Error('this is a new error')
}
test('toThrow',()=>{
expect(throwNerrorFunc).toThrow() // true
expect(throwNerrorFunc).toThrow('this is a old error') // false
expect(throwNerrorFunc).not.toThrow() // false
})
模式
按w进入模式切换:
- f:不想把通过的测试用例再跑一遍
- o:只会测试当前改变的测试用例(需要使用git来管理代码,会记录哪个文件改变了)
- a:任何一个文件发生改变,都会把所有测试用例跑一遍
- t:根据表达式来过滤哪些测试用例需要执行
- q:退出对代码的监控
- enter:按回车重新运行测试
- p:会去找包含你输入名字的测试用例
// 初始git仓库
git init
// 存到仓库
git add .
// 提交
git commit -m 'version1'
npm run test > 点w进入o模式
"test":"jest --watchAll" // 测试所有用例
"test":"jest --watch" // 直接进入o模式,只会测试有变更的文件
git checkout master.test.js
异步代码测试
- 回调异步类型的用done参数
在参数里面写done,并调用,表示调用了done函数才执行完成
import axios from 'axios'
export const fetchData=(fn)=>{
axios.get('http://xxx').then((response)=>{
fn(response.data)
})
}
import {fetchData} from './fetchData'
test('fecthData 返回结果为 {success:true}',(done)=>{
// fetchData()能够正常执行,这个测试用例就结束了,不会等到回调函数执行完才结束
fecthData((data)=>{
expect(data).toEqual({
success: true
})
// 只有回调执行完毕了才结束
done()
})
})
- 必须走一次expect语法 expect.assertions(1)
import axios from 'axios'
export const fetchData=(fn)=>{
return axios.get('http://xxx')
}
// 测试正确
test('fecthData 返回结果为 {success:true}',(done)=>{
// fetchData()能够正常执行,这个测试用例就结束了,不会等到回调函数执行完才结束
return fecthData().then((response)=>{
expect(resopnse.data).toEqual({
success: true
})
})
})
// 测试错误
test('fecthData 返回结果为 404',()=>{
// 必须走一次expect语法
expect.assertions(1)
// 如果正确就不走catch
return fecthData().catch((e)=>{
expect(e.toString().indexOf('404')-1).toBe(true)
})
})
- 使用不同的匹配器去测试用例
// 测试成功情况
// toMatchObject返回的数据,只要包含这块内容就可以
test('fectchData 返回结果为{success: true}',()=>{
return expect(fetchData()).resolves.toMatchObject({
data:{
success: true
}
})
})
// 可以改成异步函数
test('fectchData 返回结果为{success: true}', async ()=>{
await expect(fetchData()).resolves.toMatchObject(...)
})
// 或者改成这种
test('fectchData 返回结果为{success: true}', async ()=>{
const response = fetchData()
expect(response.data).toEqual({ success: true })
})
// 测试失败情况
test('fectchData 返回结果为404',()=>{
// 取数据时,希望请求是失败的,失败就会抛出异常
return expect(fetchData()).rejects.toThrow()
})
// 捕获失败
test('fectchData 返回结果为404',async()=>{
expect.assertions(1) // 即使是正确的,也必须往catch走
try{
await fetchData()
}catch(e){
expect(e.toString()).toEqual('Error: Request failed...')
}
})
钩子函数
jest执行过程中的某些具体时刻,会被jest执行调用的函数
执行顺序:beforeAll > beforeEach > afterEach > afterAll
// counter.test.js
import Counter from './counter'
// const counter= new Counter 测试用例add的结果会影响到minus的结果,不推荐这种写法
let counter= new Counter
// 所有测试用例执行前调用beforeAll(),让他帮你去做准备,但是测试用例公用同一个counter,也会有影响
beforeAll(()=>{
// counter= new Counter()
})
// 每个测试用例执行之前都会调用
beforeEach(()=>{
counter= new Counter()
})
// 每个测试用例执行之后会被调用
afterEach(()=>{console.log('afterEach')})
// 等待所有的测试用例都结束后执行
afterAll(()=>{console.log('afterAll')})
test('测试add方法',()=>{
counter.add()
expect(counter.number).toBe(1)
})
test('测试minus方法',()=>{
counter.minus()
expect(counter.number).toBe(1)
})
- describe分组语法
describe('测试counetr代码', ()=>{
describe('测试增加代码', ()=>{
// ...
})
describe('测试减少代码', ()=>{
// ...
})
})
- 钩子函数作用域
- 一个describe下的钩子函数只对他自己有效
describe('测试counetr代码', ()=>{
beforeAll(()=>{console.log('beforeAll')})
beforeEach(()=>{console.log('beforeEach')})
afterEach(()=>{console.log('afterEach')})
afterAll(()=>{console.log('afterAll')})
describe('测试增加代码', ()=>{
// beforeEach只会对里面的test生效
beforeEach(()=>{console.log('beforeEach test add')})
beforeAll(()=>{console.log('beforeAll test add')})
})
describe('测试减少代码', ()=>{
// ...
})
})
// 执行顺序:每个钩子先执行外部的再执行内部,由外到内
beforeAll > beforeAll test add > beforeEach > beforeEach test add > 执行测试用例 > afterEach > afterAll
- test.only 只执行当前的,其他不会被执行
test.only ('测试add方法',()=>{...})
- 如果逻辑不写在钩子函数里或者test里,会被最先执行
所以要把准备的代码放到钩子函数里,避免遇到错误,定位困难
describe('测试counetr代码', ()=>{
console.log('describe 1')
beforeAll(()=>{console.log('beforeAll')})
describe('测试增加代码', ()=>{
console.log('describe 2')
})
describe('测试减少代码', ()=>{
console.log('describe 3')
})
})
// 执行顺序:describe 1 > describe 2 > describe 3 > beforeAll
mock
- mock函数
const func = jest.fn()
,捕获函数的调用和返回结果,以及this指向和调用顺序 - 设置返回结果
- 模拟每次返回的结果
func.mockReturnValue('abc')
- 模拟一次返回的结果
func.mockReturnValueOnce('abc')
,可链式调用
- 模拟每次返回的结果
- 改变函数的内部实现
- 用jest模拟axios,这样就不会真正请求数据
jest.mock('axios')
- 模拟每次返回的结果
axios.get.mockResolvedValue({data: 'hello'})
- 模拟一次返回的结果
axios.get.mockResolvedValueOnce({data: 'hello'})
,可链式调用
- 用jest模拟axios,这样就不会真正请求数据
// demo.js
// 接收callback然后去执行
export const runCallback = (callback) => {
callback() // 注意没有return返回
}
// demo.test.js
import {runCallback } from './demmo'
// 使用jest生成一个函数
test('测试 runCallback ',()=>{
/* const func = jest.fn()
runCallback(func) // func被调用了,就说明函数执行了
runCallback(func)
// expect(func).toBeCalled()
// 判断是否被调用两次
expect(func.mock.calls.length).toBe(2)
*/
const func = jest.fn()
/* const func = jest.fn(()=> '456')
底层写法,跟上面的写法一致
func.mockImplementation(()=>{
console.log('aaa')
return '456'
})
func.mockImplementationOnce(()=>{
// 跟mockReturnValueOnce的区别是,可以在里面写额外的逻辑
console.log('hhhh')
return '456'
})
func.mockImplementation(()=>this) 等价于 func.mockReturnThis
expect(func.mock.results[0].value).toBeUndefined()
*/
// func.mockReturnValueOnce('abc') // 第一次被执行时返回'abc',其他都是undefined
// func.mockReturnValueOnce('abc1').func.mockReturnValueOnce('abc2') // 可以链式调用
func.mockReturnValue('abc') // 每次都返回'abc'
runCallback(func)
runCallback(func)
runCallback(func)
expect(func.mock.results[0].value).toBe('abc')
// expect(func.mock.results[1].value).toBe('abc') // 第二次调用
console.log(func.mock) // func的this指向undfined
})
func.mock的返回结果instances
// demo.js
// 函数接收一个类,创建实例
export const createObject = classItem => new classItem()
// demo.test.js
test('测试 createObject ', ()=>{
const func = jest.fn()
createObject(func)
console.log(func.mock) // instances: [mockConstructor{}],每次func运行时的this指向,指向new 构造出的对象mockConstructor
})
改变函数内部的实现
// demo.js
import axios from 'axios'
export const getDate = () => axios.get('/api').then(res=>res.data)
// demo.test.js
// 不会真正发生ajax请求接口,只要确任这个异步请求发送了,后端返回什么内容不会测试(如果有1w个测试用例就调用1w次,不靠谱,所以不会真正请求)
import axios from 'axios'
jest.mock('axios') // 用jest模拟axios,这样就不会真正请求数据
test('测试 createObject ', ()=>{
// axios.get.mockResolvedValueOnce({data: 'hello'}) // 模拟一次的返回结果
axios.get.mockResolvedValue({data: 'hello'}) // 模拟成功请求的结果,把异步获取内容变成同步获取
await getData().then((data)=>{ // 这里的data就是hello
expect(data).toBe('hello')
})
await getData().then((data)=>{
expect(data).toBe('hello')
})
})
snapshot快照测试
- toMatchSnapshot() 快照匹配,会自动生成快照
- toMatchInlineSnapshot()行内快照,测试后,快照会被放到测试用例里
// demo.js
export const generateConfig = () =>{
return {
server: 'http://locahost',
port:8080,
domain:'locahost',
time:new Date()
}
}
// demo.test.js
import {generateConfig} from './demo'
test('测试 generateConfig 函数', ()=>{
/* expect(generateConfig()).toEqual({
server: 'http://locahost',
port:8080
})*/
// 会把generateConfig函数生成得结果存到快照里面,生成一个快照文件,只要demo.js被修改了,快照就会告诉你出错,可以按u键更新快照
// expect(generateConfig()).toMatchSnapshot()
expect(generateConfig()).toMatchSnapshot({
time: expect.any(Date)
})
})
// 要先安装prettier,不然没法使用
// npm i prettier -D
test('测试 generateConfig 函数', ()=>{
expect(generateConfig()).toMatchInlineSnapshot({ // 会把快照生成到test测试用例里面
time: expect.any(Date)
},
{
"domain": "localhost"
...
}
)
})
/* 匹配不上,
按u可更新快照,
按i模式一个个去确任,
按s可跳过
*/
mock深入
对异步请求进行mock
- 测试请求方法
// demo.js
import axios from 'axios'
export const fetchData=()=>{
return axios.get('/').then(res=>res.data)
}
// {data:"(function(){return '123'})()"}
// demo.test.js
import {fetchData} from './demo'
import axios from 'axios'
// 用jest模拟axios
jest.mock('axios')
test('fetchData 测试', ()=>{
Axios.get.mockResolvedValue({
data:"(function(){return '123'})()"
})
return fetchData().then(data=>{
expect(eval(data).toEqual('123'))
})
})
- 模拟fetchData方法
- 编写一个文件,用文件内容去替换掉真实的请求方法
jest.mock('./demo')
- 不从模拟的拿数据
jest.requireActual('./demo')
- 编写一个文件,用文件内容去替换掉真实的请求方法
// demo.js
import axios from 'axios'
export const fetchData=() => axios.get('/').then(res=>res.data)
export const getNumber = () => 123
// __mocks__/demo.js 用__mocks__文件夹里面得demo.js去替换外面的demo.js
export const fetchData=()=>{
return new Promise((resolved, reject)=>{
resolved("(function(){return '123'})()")
})
}
// demo.test.js
// 模拟demo文件的方法
jest.mock('./demo')
// 取消对demo文件的模拟,模拟不了就会用真正根目录下的demo文件
// jest.unmock('./demo')
import { fetchData } from './demo'
const {getNumber} = jest.requireActual('./demo') // 不是模拟的,拿真正的内容
test('fetchData 测试', ()=>{
return fetchData().then(data=>{
expect(eval(data)).toEqual('123')
})
})
test('getNumber 测试', ()=>{
expect(getNumber()).toEqual(123)
})
// 如果不用这些,可以开启jest.config.js里面的automock: true
对定时器进行mock
定时器是异步,可以用done,测试中定时器太长,不可能等很久,所以用mock模拟定时器,让它马上测试,避免等待时间
- 模拟定时器
jest.useFakeTimers()
- 立即执行
jest.runAllTimers()
,把所有代码都运行了 jest.runOnlyPendingTimers()
只想外部定时器执行
// timer.js
export default (callback) => {
setTimeout(()=>{
callback()
setTimeout(()=>{
callback()
}, 3000)
}, 3000)
}
// demo.test.js
import timer from './timer'
/* done的用法
test('timer 测试', (done)=>{
//timer是异步函数,不会等你里面的东西执行完再去测试成功或失败
timer(()=>{
expect(1).toBe(1) // 如果3秒后被执行为true,说明是ok的
done()
})
})
*/
// 模拟定时器
jest.useFakeTimers()
test('timer 测试', ()=>{
// 判断fn是否被执行
const fn = jest.fn()
timer(fn) // 还要等3秒
// jest.runAllTimers() // 马上执行,避免等待时间
// jest.runOnlyPendingTimers() // 只想外部定时器执行
jest.advanceTimersByTimer(3000) // 快进3秒,比如2秒是不会执行函数的
expect(fn).toHaveBeenCalledTimes(1) // 如果是嵌套定时器,次数要改为2
jest.advanceTimersByTimer(3000)
expect(fn).toHaveBeenCalledTimes(2)
})
对类的mock
创建实例对象,调用方法执行,判断结果,就可以对类进行测试
// util.js
class Util{
init(){ // 非常复杂 }
a(){ // 非常复杂 }
b(){ // 非常复杂 }
}
export default Util
// util.test.js
import Util from './util'
let util = null
beforeAll(()=>{
util = new Util()
})
test('测试a方法', ()=>{
expect(util.a(1, 2)).toBe('12')
})
不需要获取最终结果,可以不用让它真实执行,只想知道a和b的方法有没有执行过。可以对类进行模拟,创建简单的类,让简单的util类去执行,不是真实执行a和b的方法,就可以节约性能
jest.mock(类)
就是把类变成jest.fn,就可以测试mock实例上的方法是否被调用过
// demo.js 调用util的方法
import Util from './util'
const demoFunction = (a, b) =>{
const util = new Util()
util.a(a)
util.b(b)
}
export default demoFunction
// demo.test.js 重点测试 a和b是否被执行过,而不是返回结果
// mock模拟util,就是把Util、Util.a、Util.b都变成jest.fn,这样就可以测试util是否被调用了
jest.mock('./util')
/* 原理:jest.mock模拟类,会自动把类的构造函数和方法变成jest.fn
const Util = jest.fn() // 这就是虚构的Util
Util.a = jest.fn()
Util.b = jest.fn()
*/
// 引入之后,执行demoFunction函数,就会去执行mock的util
import Util from './util'
import demoFunction from './demo'
test('测试 demoFunction', ()=>{
// 只想知道a和b是否被执行过
demoFunction()
expect(Util).toHaveBeenCalled()
expect(Util.mock.instances[0].a).toHaveBeenCalled()
expect(Util.mock.instances[0].b).toHaveBeenCalled() // mock实例的方法是否被调用
console.log(Util.mock.instances[0])
})
jest.mock()的模拟是自动化模拟,可以把Util、Util.a、Util.b自动变成jest.fn,如果对它的模拟不满意,可以自己进行更深入模拟
// __mocks__/util.js
const Util = jest.fn(()=>{ console.log('constructor') })
Util.prototype.a = jest.fn(()=>{ console.log('a') })
Util.prototype.b = jest.fn(()=>{ console.log('b') })
export default Util
// 也可以直接在文件中写入
jest.mock('./util', ()=>{
const Util = jest.fn(()=>{ console.log('constructor') })
Util.prototype.a = jest.fn(()=>{ console.log('a') })
Util.prototype.b = jest.fn(()=>{ console.log('b') })
return Util
})
对DOM节点操作进行测试
node环境不具备dom,而jest在node环境下模拟了一套dom的api,jsDom
// demo.js
import $ from 'jquery'
const addDivToBody = () => {
$('body').append('<div/>')
}
export default addDivToBody
// demo.test.js
import addDivToBody from './demo'
import $ from 'jquery'
test('test addDivToBody ',()=>{
// 执行一次函数,增加一个div
addDivToBody()
expect($('body').find('div').length.toBe(2))
})
vue的集成
Test Driven Development(TDD)测试驱动的开发
tdd开发流程:还没开发就先写测试用例
- 编写测试用例
- 运行测试,测试用例无法通过测试
- 编写代码,使用测试用例通过测试
- 优化代码,完成开发
- 重复上述步骤
tdd优势:即使解决,减少回归bug;会使代码结构组织合理、代码质量更好;测试覆盖率高;不容易出现错误代码
// 使用脚手架创建项目
npm @vue/cli -g
vue create jest-vue
先babel转换,再jest测试
// jest.config.js
{
"moduleFileExtensions": ["vue", "js", "json", "jsx", "ts", "tsx", "node"], // 后缀
"transform": { // 做模块转换
"^.+\\.vue$": "vue-jest", // 识别vue语法
".+\\.(css|style|less|png|svg|jpg|woff)":"jest-transform-stub" // 把静态资源转换为字符串,只对逻辑做测试,不对样式做测试,遇到静态资源没必要解析它,所以转换为字符串就可以
"^.+\\.jxs$": "babel-jest" // 把es6语法转成es5语法
}
transformIgnorePatterns: ['/node_modules/'] // 不需要被转换
testPathIgnorePatterns: ['\.eslintrc\.js'] // 不会对这些文件进行测试
moduleNameMapper: {'^@/(.*)$': '<rootDir>/src/$1'} // 做模块映射,以@开头的都到根目录src下找,把别名映射到真正模块上
snapshotSerializers:['jest-serializer-vue'] // 用第三方模块对快照进行格式化
testMatch: ["<rootDir>/tests/unit/*.(js|jsx|ts|tsx)"], // jest 的时候到哪里找测试文件
/*testMatch: [
"**/test/**/*.js",
"**/__tests__/**/*.spec.js",
"**/__tests__/**/*.spec.ts"
]*/
testURL:'http://localhost/', // 模拟浏览器当前对应地址多少
watchPlugins: [ // 协助jest交互的插件
'jest-watch-typeahead/filename',
'jest-watch-typeahead/testname'
]
}
@vue/test-utils
import {shallowMount} from '@vue/test-utils'
import HelloWorld from '@/component/HelloWorld '
describe('HelloWorld.vue', ()=>{
it('renders props.msg when passed', ()=>{
const msg = 'new message'
/* 浅渲染shallowMount,只渲染自己,不会渲染子组件
mount会把子组件也渲染
*/
const wrapper = shallowMount(HelloWorld, {
propsData: {msg}
})
// expect(wrapper.text()).toMatch(msg)
expect(wrapper.findAll('.blue').length).toBe(1)
//console.log(wrapper.props('msg'))
expect(wrapper.props('msg')).toEqual(msg)
wrapper.setProps({msg: 'hello'})
expect(wrapper.props('msg')).toEqual('hello')
})
})
快照测试:
it('组件渲染正常', ()=>{
const wrapper = shallowMount(HelloWorld, {
propsData: {msg:'abc'}
})
// expect(wrapper).toEqual('hello')
/* 只要组件能够渲染也不出错的话,就会生成快照
错误时,如果确认组件的是正确无误的,按u更新快照
所以快照测试可以及时发现dom组件中发生的变化
*/
expect(wrapper).toMatchSnapshot()
})
react的集成
// 使用react脚手架安装
npm i create-react-app -g
create-react-app jest-react
cd jest-react
npm install
// 把项目中的各种配置文件都弄出来
npm run eject
// 把package.json的jest部分挪出到新建的jest.config.js里面
"test":"node scripts/test.js" // 会运行src目录下所有的xxx.test.js
import React from 'react'
import ReactDOM from 'react-dom'
import Add from './App'
it('renders without crashing', ()=>{
const div =document.createElement('div')
const container=div.getElementByClassName('App')
expect(container.length).toBe(2)
})
enzyme配置和使用
对react dom render进行包装,提供额外的方法供我们调用,能更灵活测试组件
// 安装
npm i enzyme enzyme-adapter-react-16 -D // 适配16版本的适配器
// 配置
import React from 'react'
import Enzyme, {shallow} from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
import Add from './App'
Enzyme.configure({adapter:new Adapter()])
it('test',()=>{
// shallow浅渲染,只会渲染当层,不会渲染子组件(子组件用字符标记)
const warpper = shallow(<App />)
expect(wrapper.find('.app-container').length).toBe(1)
expect(wrapper.find('.app-container').prop('title')).toBe('abc')
// <div className='app-container' title='abc' data-test='container'></div>
expect(wrapper.find('[data-test="container"]‘).prop('title')).toBe('abc') // 属性渲染器
console.log(wrapper.debug())
})
注:集成测试用mount,单元测试用shallow
- jest-enzyme库
// 安装库 jest-enzyme 可以使代码更简洁
npm i jest-enzyme -D
"setupFilesAfterEnv":["./node_modules/jest-enzyme/lib/index.js"]
const container = wrapper.find'[data-test="container"]')
expect(container).toExist()
expect(container).toHaveProp('title', 'abc')
- 操作事件simulate
it('header组件 用户输入',()=>{
const wrapper = shallow(<Header />)
const inputElm = wrapper.find("[data-test='input']")
inputElm.simulate('change', {
target: { value: 'learn jest' }
})
expect(wrapper.state('value')).toEqual('learn jest')
/* 为什么要重新获取elm,当数据发生变化的时候,老的inputElm还是之前的状态,
所以必须重新获取elm
const newInputElm = wrapper.find("[data-test='input']")
expect(newInputElm.prop('value')).toBe('learn jest')
*/
})
it('header组件 input回车,如果input无内容就没操作',()=>{
const fn = jest.fn()
const wrapper = shallow(<Header addUndoItem={fn} />)
const inputElm = wrapper.find("[data-test='input']")
const strInput ='learn jest'
wrapper.setState({value: strInput})
inputElm.simulate('keyup', {
keyCode: 13
})
// expect(wrapper.state('value')).toEqual('learn jest')
// expect(fn).not.toHaveBeenCalled() // 没有被调用过
expect(fn).toHaveBeenCalled()
expect(fn).toHaveBeenLastCalledWith(strInput)
const newiInputElm = wrapper.find("[data-test='input']")
expect(newiInputElm.prop('value')).toBe('') // 回车后input value为空
})
//TodoList 组件中 return( <Header addUndoItem={this.addUndoItem.bind(this)}/> )
it('TodoList给Header传递一个方法 ',()=>{
const wrapper = shallow(<TodoList />)
const Header= wrapper.find("Header")
expect(Header.prop('addUndoItem')).toBe(wrapper.instance().addUndoItem)
})
it('当Header回车时,undoList应该新增内容',()=>{
const wrapper = shallow(<TodoList />)
const Header= wrapper.find("Header")
const addFunc= wrapper.prop("addUndoItem")
addFunc('learn jest')
expect(wrapper.state('undoList').length).toBe(1)
expect(wrapper.state('undoList')[0]).toBe('learn jest')
})
- 生成快照
it('Header 渲染样式正常',()=>{
const wrapper = shallow(<TodoList />)
expect(wrapper).toMatchSnapshot()
})
- 提取通用代码
"setupFilesAfterEnv":["<rootDir>/src/utils/testSetup.js"]
// ./utils/testSetup.js 这里放常用方法或者初始化的类,每个测试用例文件就不用再加这些了
import Enzyme from 'enzyme'
import Adapter from 'enzyme-adapter-react-16'
Enzyme.configure({adapter:new Adapter()])
BDD(Behavior Driven Developmen)
TDD:先写测试再去开发(测试驱动开发)
BDD:用户的行为是什么样子就按用户的行为去开发,去编写测试用例(集成测试,多个组件测试,用户的行为涉及到各个组件的联动)(行为驱动开发)
不能使用shallow,要用mount
it(`
1. header 输入框输入内容
2. 点击回车
3. 列表中展示用户输入的内容项
`, ()=>{
const wrapper = mount(<TodoList />)
})
待续…