前端自动化测试-Jest(一)

一  前端自动化测试产生的背景及原理

在没有前端自动化测试的时候,一般是项目使用过程中发现问题。

前端自动化测试:是写了一段测试的js代码,通过测试的js代码,去运行项目(含有需要测试的代码),查看预期值跟结果的值,是否相等,相等则正确,否则有误。 

简单的理解就是 一段额外的测试代码就可以在上线之前对它进行测试,而这些测试不是人肉的去点击,而是通过已经写好的代码去运行的,得到结果进行判断,是否有问题

如 小案例:要测试math.js的方法

math.js

function add(a, b) {
    return a - b;
}
function del(a, b) {
    return a - b;
}

math.test.js 测试

/**
 * expect 方法 是输出错误
 * result  结果值
 * actual  预期值
 */
function expect(result) {
    return {
        toBe: function (actual) {
            if (result !== actual) {
                throw new Error(`预期值${actual}和结果值${result}不相等`);
            }
        }
    }

}
/**
 * 
 * @param {*} desc  描述语
 * @param {*} fn 执行测试的方法
 */

function test(desc, fn) {
    try {
        fn();
        console.log(`${desc} 通过测试`)
    } catch (e) {
        console.log(`${desc} 没有通过测试,${e}`)
    }
}
test('测试加法3+7:', () => {
    expect(add(3, 7)).toBe(10);
})

test('测试减法法7-7:', () => {
    expect(del(7, 7)).toBe(0);
})

二. 前端自动化测试框架 Jest

前端自动化测试框架 主流: Jasmine、  Mocha+chaiJest

这些 功能,性能,易用性都很好

Jest 是一个令人愉快的 JavaScript 测试框架,专注于 简洁明快。

Jest是 Facebook 的一套开源的 JavaScript 测试框架, 它自动集成了断言、JSDom、覆盖率报告等开发者所需要的所有测试工具,是一款几乎零配置的测试框架。并且它对同样是 Facebook 的开源前端框架 React 的测试十分友好。

jest 优点:

1.速度快

比如有2个A,B模块,第一次运行项目A,B同时执行到;第二次运行项目前,改了A,这时B是就不会在运行的,只会运行A,因为他知道B没修改,所以没必须要运行,这样的话,测试效率很高,速度很快

2.API 简单 

3.易配置

安装下jest ,简单配置下,就可以使用

4.隔离性好

jest 里面的很多测试文件,但是每个文件被执行的时候环境是隔离的

5.监控模式

这种模式下可以更灵活的

6.IDE整合

7.Snapshot

8.多项目并行

9.覆盖率

10.Mock丰富

三. Jest 使用

1.安装Jest

在项目里执行

npm install jest -D

2. 运行测试

在 package.json 中

  "scripts": {
    "test": "jest"
  },

执行

npm run test

即可启动jest对项目中所有以.test.js结尾的文件进行测试。

自动监控测试文件

jest自动监控测试文件,一有更新,就自动运行测试。package.json中的jest那里加上--watchAll参数

"script": {
    "test": "jest --watchAll"
}

四 Jest 简单配置

(1)生成配置文件jest.config.js

使用jest,会使用默认的配置,如果想修改配置,执行下面的命令,来初始化生成一个jest.config.js文件

npx jest --init

默认配置了,

(2)生成测试覆盖率报告

如果想修改测试覆盖率报告的文件夹名称,可以在jest.config.js中配置,
修改这一项 coverageDirectory: "coverage",

执行命令 npx jest --coverage
在项目目录下会生成一个文件夹,存放测试覆盖率的文件

// coverageDirectory: "coverage",
// 也可以修改为其他名字
coverageDirectory: "reports",

这样我们在运行

npx jest --coverage

或者将package.json 里面增加

"scripts": {
    "coverage": "jest --coverage"
}

这样就可以直接

npm run coverage

生成测试覆盖率报告

打开lcov-report/index.html 就可以看测试覆盖率报告

(3)配置可以用esModule模块导入的测试环境

我们运行Jest的时候 当前坏境是一个node环境【node 不支持import ,nodejs采用的是CommonJS的模块化规范,使用require引入模块;而import是ES6的模块化规范关键字】,Jest在node环境下对于esModule的语法无法解析,只辨识commonJS的模块语法

esModule 写法:

// math.js
export function add(a, b) {
    return a + b
}

// math.test.js
import {add} from './math'

实际项目中更多使用的是ES的语法来定义module,但是如果我们直接改成了ES语法,则运行jest就报错了。如何做兼容呢,我们可以使用babel【必须引入babel转义支持,通过babel进行编译,使其变成node的模块化代码。

安装 babel

npm install @babel/core@7.4.5 @babel/preset-env@7.4.5 -D

配置babel

在项目根目录新建.babelrc文件,并写入以下内容

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "node": "current"
        }
      }
    ]
  ]
}

配置完毕,再运行npm run test,就不会报错

这个过程:

  1. 在运行npm run test之后
  2. jest内部集成一个插件babel-jest
  3. 它会检测当前项目是否安装了babel-core
  4. 如果安装了则会去读取.babelrc配置
  5. 在运行测试之前,会根据babel配置对代码进行转换
  6. 最后,运行转化后的测试用例代码

五 Jest 中的匹配器

有关完整列表,请参阅expectAPI doc。下面是简单的介绍

基础语法

expect()返回被称作“expectation”的对象。toBe()被称作matcher。Jest会对两者进行比较并输出测试结果。

相应的,expect()toBe()还可以调用被测的function

//1. toBe 相当于object.js 或者是===,比较的是内存地址
test('测试1与1相匹配:', () => {
    expect(1).toBe(1);
})

//2. toEqual 匹配是内容相等
test('测试对象内容相等:', () => {
    const a = { one: 1 }
    expect(a).toEqual({ one: 1 })
})

比较null,undefined,true,false

//3. toBeNull 匹配是否为null
test('测试是否为null:', () => {
    const a = null;
    expect(a).toBeNull()
})
//4.  toBeUndefined 真假有关的匹配
test('测试是否为undefined:', () => {
    const a = undefined;
    expect(a).toBeUndefined()
})

//5. toBeDefined 变量是被定义的
test('测试是否为定义过:', () => {
    const a = null; //a = undefined; 的时候是表示没被定义
    expect(a).toBeDefined()
})

//6. toBeTruthy  是否为真的 true
test('测试是否为BeTruthy:', () => {
    const a = 1; // a 为null ,0,undefined 都是被认为是false,
    expect(a).toBeTruthy()
})

//7. toBeFalsy  是否为假 false
test('测试是否为BeTruthy:', () => {
    const a = null; // a 为null ,0,undefined 都是被认为是false,
    expect(a).toBeFalsy()
})

not

// 8. not 就是否的意思
test('测试是否为BeTruthy:', () => {
    const a = null; // a 为null ,0,undefined 都是被认为是false,
    expect(a).toBeFalsy()
    expect(a).not.toBeTruthy()
})

toBe():使用Object.is()实现精确匹配
toEqual():递归检查对象或数组的每一个字段。
toBeNull():判断是否为null
toBeUndefined():判断是否是undefined
toBeDefined():判断是否是定义过的
toBeTruthy():判断是否为真
toBeFalsy():判断是否为假
not:取反匹配器 expect(a).not.toBeFalsy()

 数字相关匹配器

1. toBeGreaterThan 是否大于
2. toBeLessThan  匹配是否小于
3. toBeGreaterThanOrEqual
4. toBeLessThanOrEqual
5. toBeCloseTo 特别用来处理浮点数

test('测试浮点数:', () => {
    const a = 0.1;
    const b = 0.1;
    expect(a + b).toBeCloseTo(0.2);
})

字符串string

使用toMatch(obj) 方法 obj 可以是正则表达式,也可以是字符串

test('toMatch', () => {
    const str = "http://www.baidu.com";
    expect(str).toMatch(/bai/);
    expect(str).not.toMatch(/bai-du/);
});

数组Array

使用toContain()方法:判断数组是否包含某项

const shoppingList = [
    'diapers',
    'kleenex',
    'trash bags',
    'paper towels',
    'beer',
];

test('the shopping list', () => {
    const arr = new Set(shoppingList);
    expect(shoppingList).toContain('beer');
    expect(arr).toContain('beer');
    expect(shoppingList).not.toContain('pork');
});

异常情况下  

toThrow - 要测试的特定函数会在调用时抛出一个错误 。toThrow 也是可以写正则表达式,和字符串

//throwNewErrorFunc  抛出的错误内容要跟toThrow里的相等
const throwNewErrorFunc = () => {
    throw new Error('this is a new error')
}
test('异常情况下', () => {
    expect(throwNewErrorFunc).toThrow('this is a new odl error')
});

其他

  • .resolves 和 .rejects - 用来测试 promise

用来测试 promise

 
//resolves
test('resolves to lemon', () => {
  // make sure to add a return statement
  return expect(Promise.resolve('lemon')).resolves.toBe('lemon');
});
 
//rejects
test('resolves to lemon', async () => {
  await expect(Promise.resolve('lemon')).resolves.toBe('lemon');
  await expect(Promise.resolve('lemon')).resolves.not.toBe('octopus');
});
 
  • .toHaveBeenCalled() - 用来判断一个函数是否被调用过

.toHaveBeenCalled() 也有个别名是.toBeCalled(),用来判断一个函数是否被调用过。

describe('drinkAll', () => {
  test('drinks something lemon-flavored', () => {
    const drink = jest.fn();
    drinkAll(drink, 'lemon');
    expect(drink).toHaveBeenCalled();
  });
 
  test('does not drink something octopus-flavored', () => {
    const drink = jest.fn();
    drinkAll(drink, 'octopus');
    expect(drink).not.toHaveBeenCalled();
  });
});
 
  • .toHaveBeenCalledTimes(number) - 判断函数被调用过几次

和 toHaveBeenCalled 类似,判断函数被调用过几次。

test('drinkEach drinks each drink', () => {
  const drink = jest.fn();
  drinkEach(drink, ['lemon', 'octopus']);
  expect(drink).toHaveBeenCalledTimes(2);
});

 

六 Jest 的命令行参数

f,只重新运行上次测试失败的测试用例。

o,有多个测试用例文件,当某个测试文件修改,则只重新运行修改的那个文件里的所有测试。 需要配合使用git管理项目(不然jest 不知道哪个文件修改)

p,只重新运行测试文件名字跟输入的匹配的测试文件。

t,只重新运行测试用例名字跟输入的匹配的测试用例。

在 package.json 中

  "scripts": {
    "test": "jest --watchAll"
  },

"test": "jest --watchAll",相当于a模式,当任何一个测试文件修改,则重新运行所有的测试

"test": "jest --watch" ,相当于o模式,

但我们运行的jest命令行带了--watchAll 或--watch,执行命令后,终端中该命令并不会退出,而是在等待状态,这时候你可以输入上述中的各种模式的代号,进行重新运行测试

 

七 异步代码的测试

官网

1.回调函数型型的异步函数

创建fetchData.js

import axios from 'axios';

export const fetchData = (fun) => {
    axios.get('http://www.dell-lee.com/react/api/demo.json').then((res) => {
        fun(res.data)
    })
}

测试用例 fetchData.test.js: 安装之前学的内容,我们上来就写如下测试代码

test('异步函数', () => {
    fetchData((data)=> {
        expect(data).toEqual({success: true})
    })
})
运行,测试通过。

但是!但是!这是错误的写法,因为test刷就执行完了,expect压根就没运行,也就是说哪怕要测试的函数返回值并不是{success: true},这里也会测试通过的,咋整?稍微修改一下

import { fetchData } from './fetchData'
// 回调函数型型的异步函数
test('异步函数', (done) => {
    fetchData((data) => {
        expect(data).toEqual({ success: true });
        done();
    })
})

test的回调函数传个参数(函数) ,告诉他,这个传入的函数执行完了,你这个test才算执行完了。这样就可以等到expect执行之后,才算这个测试用例完事

2.返回Promise 型的异步函数

创建fetchData.js

import axios from 'axios';

// 返回Promise 型的异步函数
export const fetchDataPromise = () => {
    return axios.get('http://www.dell-lee.com/react/api/demo1.json');
}

方法1:Promise

如果测试正常情况,则

test('返回Promise异步函数:', () => {
    return fetchDataPromise((res) => {
        console.log(res, 'res :::')
        expect(res.data).toEqual({ success: true })
    })
})

如果要测试抛出异常的Promise的话,我们觉得应该这么写

test('返回结果为 404', () => {
    expect.assetions(1) // 至少要执行一个 expect 
    return fetchDataPromise().catch(e => {
        expect(e.toString().indexOf('404') > -1).toBe(true)
    })
})

 注意: 如果不加expect.assetions(1) 时,如果该函数正常返回了{success: true},测试结果竟然还是通过!因为catch那里根本没有执行。加上expect.assetions(1) 时,如果expect执行次数不够1,在这里也就是说catch那里没有被执行,就测试失败。

方法2 :.resolves/.rejects

可以使用./resolves匹配器匹配你的期望的声明(跟Promise类似),如果想要被拒绝,可以使用.rejects

  如果测试正常情况,则

test('返回Promise异步函数:', () => {
    return expect(fetchDataPromise()).resolves.toMatchObject({
        data: {
            success: true
        }
    })
})

如果要测试抛出异常的Promise的话,我们觉得应该这么写

test('返回结果为 404', () => {
    // toThrow() 抛出异常
    return expect(fetchDataPromise()).rejects.toThrow()

})

方法3:Async/Await

若要编写async测试,只要在函数前面使用async关键字传递到test。

// 测试正常情况
test('promise', async () => {
    const res = await fetchDataPromise()
    expect(res.data).toEqual({ success: true })
})

// 测试异常情况
test('返回 404', async () => {
    expect.assertions(1)
    try {
        await fetchDataPromise()
    } catch (e) {
        expect(e.toString()).toEqual('Error: Request failed with status code 404')
    }
})

方法4: 可以asyncawait.resolves或结合使用.rejects

// 如果测试正常情况,则
test('返回Promise异步函数:', async () => {
    await expect(fetchDataPromise()).resolves.toMatchObject({
        data: {
            success: true
        }
    })
})

// 如果要测试抛出异常的Promise的话,我们觉得应该这么写

test('返回结果为 404', async () => {
    // toThrow() 抛出异常
    await expect(fetchDataPromise()).rejects.toThrow()
})

八 Jest 的钩子函数

在jest中,如果测试用例中需要使用到某个对象 或 在执行测试代码的某个时刻需要做一些必要的处理,直接在测试文件中写基础代码是不推荐的,可以使用jest的钩子函数。

钩子函数的作用:在代码执行的某个时刻,会自动运行的一个函数。

简单例子

新建counter.js文件,代码如下:

export default class Counter {
    constructor() {
        this.number = 0;
    }
    addOne() {
        this.number += 1;
    }
    minusOne() {
        this.number -= 1;
    }
}

编写对应的测试用例:counter.test.js文件,代码如下:

import Counter from './counter.js'
const counter = new Counter();
test('测试counter中addOne方法', () => {
    counter.addOne();
    expect(counter.number).toBe(1)
})
test('测试counter中minusOne方法', () => {
    console.log('counter.number:', counter.number)
    counter.minusOne();
    expect(counter.number).toBe(0)
})

上边的测试用例可以正常执行,但是addOne()函数调用的次数会影响counter.number的值,就会影响到minusOne方法执行结果。如果你想addOne与minusOne方法调用互不影响时,此时就不得不引入jest的钩子函数。

钩子函数:

beforeAll:在所有测试用例执行之前执行
beforeEach:每个测试用例执行前执行,可让每个测试用例中使用的变量互不影响,因为分别为每个测试用例实例化了一个对象
afterAll:等所有测试用例都执行之后执行 ,可以在测试用例执行结束时,做一些处理
afterEach:每个测试用例执行结束时,做一些处理

import Counter from './counter.js'

// 使用类中的方法,首先要实例化
let counter = null

beforeAll(() => {
    console.log('所有测试实例运行之前执行')
})
afterAll(() => {
    console.log('所有实例运行之后执行')
})
beforeEach(() => {
// 每个测试用例执行前执行,可让每个测试用例中使用的变量互不影响
//因为分别为每个测试用例实例化了一个对象

    counter = new Counter();
    console.log('每个测试实例运行之前执行')
})
afterEach(() => {
    console.log('每个测试实例运行之后执行')
})
test('测试counter中addOne方法', () => {
    counter.addOne();
    expect(counter.number).toBe(1)
})
test('测试counter中minusOne方法', () => {
    console.log('counter.number:', counter.number)
    counter.minusOne();
    expect(counter.number).toBe(0)
})

所以在beforeEach加上 counter = new Counter(),测试用例使用的变量就互不影响。

根据上边案例实际打印结果可以看出这四个钩子函数的执行顺序,如下:

(1)beforeAll > (2)beforeEach > (3)afterEach > (4)afterAll

钩子函数是在测试用例执行前或执行后执行的

对测试进行分组

如果测试用例比较多,比如一堆测试加法的,一堆测试减法的,那么我们就可以分组,通过describe()来实现。其实也可以理解所有测试最外层默认包裹了一层describe。

describe('测试加法', () => {
    test('测试counter中addOne方法', () => {
        counter.addOne();
        expect(counter.number).toBe(1)
    })
})
describe('测试减法', () => {
    test('测试counter中minusOne方法', () => {
        console.log('counter.number:', counter.number)
        counter.minusOne();
        expect(counter.number).toBe(-1)
    })
})

钩子函数的作用域

 在describe内部和外部都可以写钩子函数,外部的先执行然后执行内部的

beforeAll(() => {
    console.log('我是外部的钩子函数');
})
beforeEach(() => {
    console.log('我是外部的钩子函数');
})

describe('测试加法', () => {
    beforeAll(() => {
        console.log('我是内部的钩子函数')
    })
    test('测试counter中addOne方法', () => {
        counter.addOne();
        expect(counter.number).toBe(1)
    })
})
describe('测试减法', () => {
    test('测试counter中minusOne方法', () => {
        counter.minusOne();
        expect(counter.number).toBe(-1)
    })
})

由打印结果可知,写在内层describe中的beforeAll钩子函数只作用了当前的分组,钩子函数是有作用域的,取决于钩子函数位于 describe 中的位置

但是注意 如果describe 里是beforeAll,外面则是beforeEach,那则会,

所以,如果是同类型函数,则外部执行,那如果是钩子函数执行顺序,那还是按之前的:(1)beforeAll > (2)beforeEach > (3)afterEach > (4)afterAll

test.only()

如果测试用例比较多,而我们已经定位到其中一个测试用例有问题,那么我们可以单独的输出这个测试用例的结果,即test.only()

beforeAll(() => {
    console.log('我是外部的钩子函数: beforeAll')
})
beforeEach(() => {
    console.log('我是外部的钩子函数:beforeEach');
    counter = new Counter()
})
describe('测试加法', () => {
    beforeAll(() => {
        console.log('我是内部的钩子函数: beforeAll')
    })
    test.only('测试addOne方法', () => {
        counter.addOne();
        expect(counter.number).toBe(1);
    })
})
describe('测试减法', () => {
    test('测试counter中minusOne方法', () => {
        counter.minusOne();
        expect(counter.number).toBe(-1)
    })
})

看结果 测试减法 被忽略 

Jest中的Snapshot快照测试

当我们在写业务代码或者配置文件的测试用例时,可能会牵扯到不断修改的过程。那么就会面临着一个比较麻烦的问题:就是每次修改业务代码时,都要相对应的修改测试用例中的断言代码。那么如何避免这个问题呢?使用Snapshot快照即可解决这个大麻烦!

Snapshot快照的作用就是:把每次修改后的代码形成快照,与上次代码生成的快照做对比,若有修改部分,则会出现异常快照提示

1、快照测试

demo.js

export const generateConfig = () => {
    return {
        host: "http://www.localhost",
        port: "8080"
    }
}

demo.test.js
import { generateConfig } from './demo'
    test('测试 generatoConfig 函数', () => {
    expect(generateConfig()).toEqual({
        host: "http://www.localhost",
        port: '8080'
    })

})

当配置项不断增加的时候,就需要不断去更改测试用例。麻烦

所以使用快照测试,简单容易。

test('测试 generatoConfig 函数', () => {
    expect(generateConfig()).toMatchSnapshot();
})

 第一次执行 npm run test,就会生成__snapshots__ 文件包

toMatchSnapshot() 会为expect 的结果做一个快照并与前面的快照做匹配。(如果前面没有快照那就保存当前生成的快照即可)

这在配置文件的测试的时候是很有用的,因为配置文件,一般不需要变化。

当然,确实要改配置文件,然后要更新快照,也可。

 更新配置文件,这时使用u模式可以同时更新快照,

假设配置项中有随机数或者当前时间,如下

export const generateConfig = () => {
    return {
        host: "http://www.localhost",
        port: "80801",
        time: new Date()
    }
}
那case 中snapshot 就需要使用expect.any() 了,如下
test('测试 generatoConfig 函数', () => {
    expect(generateConfig()).toMatchSnapshot({
        time: expect.any(Date)
    });
})

运行,执行 u,之前使用snapshot 的时候,都会生成一个snapshot 的文件。

2. 行内的Snapshot (toMatchInlineSnapshot)

行内toMatchInlineSnapshot主要是:直接在测试代码里面插入返回值 。使用行内Snapshot的前提是:安装 prettier

npm i prettier -D

安装完成之后,修改测试用例:

test('测试 generatoConfig 函数', () => {
    expect(generateConfig()).toMatchInlineSnapshot({
        time: expect.any(Date)
    });
})

运行测试用例之后,会多出一个参数来,结果如下:

十 Jest中的Mock

1. 为什么要使用Mock函数?

在项目中,一个模块的方法内常常会去调用另外一个模块的方法。在单元测试中,我们可能并不需要关心内部调用的方法的执行过程和结果,只想知道它是否被正确调用即可,甚至会指定该函数的返回值。此时,使用Mock函数是十分有必要。

2. Mock函数

提供的以下三种特性,在我们写测试代码时十分有用:

  • 捕获函数调用情况(mock函数,捕获函数的调用 和 返回结果 以及 this指向 和 调用顺序.)
  • 设置函数返回值
  • 改变函数的内部实现

在测试中没有提供测试用的数据,那么可以使用jest提供的mock函数,来捕获函数的调用。

const func = jest.fn();  //mock函数,捕获函数的调用
console.log(func.mock)

在mock函数里面包含了什么内容?我们可以通过console.log来查看
calls------ 函数被调用返回的结果
instances----- 函数指向的this原型
invocationCallOrder ------ 函数执行的顺序
results----- 函数执行的结果,为数组类型,有返回的类型和返回的值

 {
      calls: [ [] ], // 被调用了多少次 以及 传入参数
      instances: [ undefined ], //每次func运行时,this的指向
      invocationCallOrder: [  ],  //函数执行的顺序
      results: [ //函数执行的结果,为数组类型,有返回的类型和返回的值
        { type: 'return', value: undefined }
      ]
    }

示例说明:1,2点 

 

补充:

test('测试jest.fn()内部实现', () => {
  let mockFn = jest.fn((num1, num2) => {
    return num1 * num2;
  })
  // 断言mockFn执行后返回100
  expect(mockFn(10, 10)).toBe(100);
})

test('测试jest.fn()返回Promise', async () => {
  let mockFn = jest.fn().mockResolvedValue('default');
  let result = await mockFn();
  // 断言mockFn通过await关键字执行后返回值为default
  expect(result).toBe('default');
  // 断言mockFn调用后返回的是Promise对象
  expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
})

 

改变内部函数的实现

jest.mock(‘axios’) 让jest对axios做一个模拟. 在测试用例中即可用同步代码模拟数据,不需要发送真实的请求。

mock测试异步axios请求: 

 

3. 异步函数的测试

(1).模拟请求

对于异步函数的测试,Jest中封装了独立的API,通过例子整理分类如下。

目录结构:

|--demo.js
|--demo.test.js
|--package-lock.json
|--package.json
|--node_modules
|--.babelrc

业务代码:demo.js

import axios from 'axios'

export const fetchData = () => {
	return axios.get('/').then(res => res.data)
}

测试代码:demo.test.js

import { fetchData } from './demo'
import Axios from 'axios'
jest.mock('axios') // 模拟axios

test('fetchData 测试', () => {
	// 模拟请求
	Axios.get.mockResolvedValue({
		data: "(function(){return '123'})()"
	})
	return fetchData().then(data => {
		expect(eval(data)).toEqual('123');
	})
})

(2).模拟demo

在根目录下新建一个模拟文件夹,名字为__mocks__,在__mocks__文件夹下新建demo.js文件。

情景:当我们测试发送异步请求的代码时,我们可以直接通过模拟demo的方式,测试请求方法是否正常执行.
demo.js

|--__mocks__
	|--demo.js
|--demo.js
|--demo.test.js
|--package-lock.json
|--package.json
|--node_modules
|--.babelrc

mocks _/demo.js

import axios from 'axios'

export const fetchData = () => {
	// 发异步请求
	return axios.get('/').then(res => res.data)
}

demo.test.js

jest.mock('./demo'); // 使用jest模拟demo,当执行测试用例时,就会去__mocks__文件下去找demo.js
import { fetchData } from './demo' // 通过jest模拟demo之后,再通过import引入fetchData
// 这里引的fetchData是我们模拟的fetchData
test('fetchData 测试', () => {
	return fetchData().then(data => {
		expect(eval(data)).toEqual('123')
	})
})

(3).自动模拟设置

目录结构和代码同模拟demo一致。
通过修改jest.config.js配置,让其自动模拟进行测试.
首先, 在命令行执行 npx jest --init 让jest的配置文件暴露出来. 把配置文件中的自动模拟项(automock)设置为true.

然后: 把demo.test.js文件中的 jest.mock(’./demo’) 删除掉.

import { fetchData } from './demo'

test('fetchData 测试', () => {
	return fetchData().then(data => {
		expect(eval(data)).toEqual('123')
	})
})

此时,再执行 npm test ,测试用例依然会通过。

注: 当我们把配置项 automock 修改为 true 后,jest就会开启自动模拟功能,就算测试文件中没有声明模拟代码,jest依然会去自动查找根目录中是否有mocks文件的存在,mocks文件夹下是否有相对应的demo.js文件。如果有,那么在使用 import { fetchData } from ‘./demo’ 引入demo时,会拿mocks下的demo代替我们写的业务代码demo被引入。如果没有,则会引入根目录下得我们写的业务文件demo。这就是自动模拟的运行机制。

(4).异步函数模拟—同步函数不模拟

目录结构:

|--__mocks__
	|--demo.js
|--demo.js
|--demo.test.js
|--package-lock.json
|--package.json
|--node_modules
|--.babelrc

demo.js

import axios from 'axios'

// 异步函数
export const fetchData = () => {
	// 发异步请求
	return axios.get('/').then(res => res.data)
}

// 同步函数
export const getNumber = () => {
	return 123;
}

demo.test.js

jest.mock('./demo'); // 使用jest模拟demo,当执行测试用例时,就会去__mocks__文件下去找demo.js
import { fetchData } from './demo'; // 通过jest模拟demo,这里的fetchData是来自于模拟的demo里的
const { getNumber } = jest.requireActual('./demo'); // 这里的getNumber是来自于真正的demo里的

test('fetchData 测试', () => {
	return fetchData().then(data => {
		expect(eval(data)).toEqual('123');
	})
})

test('getNumber 测试', () => {
	expect(getNumber()).toBe(123);
})
  • jest.requireActual(’./demo’) 引入真正的业务代码文件。(不是通过jest模拟的)
  • 对异步函数模拟、对同步函数不模拟,这样既可实现同步函数 和 异步函数的完美测试。

 

4.取消模拟

jest.unmock('./mock'); // 取消模拟mock
jest.unmock('axios'); // 取消模拟axio

5. mock timers的使用 

平时开发中我们经常用到定时器setInterval 或者setTimeout ,现在我们就写一个定时器的测试用例代码如下:

从运行来看,setTimeout 是没有执行到的。

 如何解决这个问题呢?

方法一:根据执行的done()来操作

import { lazy } from './mocktimer'

// done(); 操作
test('should call fn after 3s', (done) => {
    const callback = jest.fn();
    lazy(callback);
    setTimeout(() => {
        expect(callback).toBeCalled();
        done();
    }, 3001);
})

方法二:

今天我们学习一种新的解决办法,使用mock timer解决这个问题。jest 提供了mock timer 的功能,不要再使用真实的时间在这里等了,一个假的时间模拟一下就可以了。

首先是jest.useFakeTimers() 的调用,它就告诉jest 在以后的测试中,可以使用假时间。当然只用它还不行,因为它只是表示可以使用,我们还要告诉jest在哪个地方使用,当jest 在测试的时候,到这个地方,它就自动使用假时间。

两个函数,jest.runAllTimers(), 它表示把所有时间都跑完。具体到我们这个测试,我们希望执完lazy(callback) 就调用, 把lazy函数中的3s时间立刻跑完。可以使用jest.runAllTimers();

import { lazy } from './mocktimer'
jest.useFakeTimers();//可以使用假函数
test('should call fn after 3s', () => {
    const callback = jest.fn();
    lazy(callback);
    jest.runAllTimers();//让所有定时器立即执行
    expect(callback).toBeCalled();
})

多个定时器的情况

export const lazy = (fn) => {
    setTimeout(() => {
        fn();
        console.log('第一个定时器执行')
        setTimeout(() => {
            console.log('第二个定时器执行')
        }, 3000)
    }, 3000);
}

 测试用例:

只执行第一个,

jest.useFakeTimers();//可以使用假函数
test('should call fn after 3s', () => {
    const callback = jest.fn();
    lazy(callback);

    //jest.runOnlyPendingTimers() 只执行一个定时操作
    jest.runOnlyPendingTimers(); //只执行一个定时操作
    expect(callback).toHaveBeenCalledTimes(1)
})

 jest.advanceTimer() 快进几秒

jest.useFakeTimers();//可以使用假函数
test('should call fn after 3s', () => {
    const callback = jest.fn();
    lazy(callback);

    // jest.advanceTimer()// 快进几秒
    jest.advanceTimersByTime(3000)
    expect(callback).toHaveBeenCalledTimes(1)
})

2个定时器都执行,使用jest.advanceTimersByTime(n)快进n时间执行定时,多个advanceTimerByTime连用时,后一个会以前一个的时间为基点,如果不想互相影响,我们可以使用钩子函数beforeEach解决这个问

import { lazy } from './mocktimer'

jest.useFakeTimers();//可以使用假函数
test('should call fn after 3s', () => {
    const callback = jest.fn();
    lazy(callback);

    jest.advanceTimersByTime(3000)
    jest.advanceTimersByTime(6000)//第二个的时间以第一个的时间为基数
    //expect(callback).toBeCalled();
    expect(callback).toHaveBeenCalledTimes(1)
})

 

参考:

十一.TDD介绍

测试驱动开发(Test Driven Development,简称TDD)。TDD 是敏捷开发中的一项核心实践和技术,也是一种设计方法论。TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。TDD 是 XP(Extreme Programming)的核心实践。它的主要推动者是 Kent Beck。

测试驱动开发的思想就是“测试的目的是让你知道,什么时候算是完成了。如果你聪明,你就应该先写测试,这样可以及时知道你是否真地完成了。否则,你经常会不知道,到底有哪些功能是真正完成了,离预期目标还差多远。”

TDD 有三层含义:


 - Test-Driven Development,测试驱动开发。 Task-Driven
 - Development,任务驱动开发,要对问题进行分析并进行任务分解。 Test-Driven
 -  Design,测试保护下的设计改善。TDD    并不能直接提高设计能力,它只是给你更多机会和保障去改善设计。

TDD 的开发流程


 - 编写测试用例
 - 运行测试,测试用例无法通过测试
 - 编写代码,使测试用例通过测试
 - 优化代码,完成开发
 - 重复上述步骤

你可能会问,我写一个测试用例,它明显会失败,还要运行一下吗?
是的。你可能以为测试只有成功和失败两种情况,然而,失败有无数多种,运行测试才能保证当前的失败是你期望的失败。
一切都是为了让程序符合预期,这样当出现错误的时候,就能很快定位到错误(它一定是刚刚修改的代码引起的,因为一分钟前代码还是符合我的预期的)。
通过这种方式,节省了大量的调试代码的时间。

TDD 的优势


 - 长期减少回归 bug
 - 代码质量更好(组织,可维护性)
 - 测试覆盖率高
 - 错误测试代码不容易出现

TDD + 单元测试适用范围


 - 如果要写一个纯库类,跟业务没有关系,非常适合用 TDD。
 - 如果是写业务代码,常常由于测试代码中要用功能代码的数据结构,造成耦合性高

十二.案例

1. 简单使用

math.js

function add(a, b) {
    return a + b;
}
function del(a, b) {
    return a + b;
}

module.exports = {
    add,
    del
}

main.test.js

const { add, del } = require('./math.js')
test('测试加法3+7:', () => {
    expect(add(3, 7)).toBe(10);
})

test('测试减法7-7:', () => {
    expect(del(7, 7)).toBe(0);
})

测试结果:可以看到“减法方法报错”

但是呢,如果我们的html文件引入该js则会报错说module是个什么玩意,这么我们的暂时解决方案是try一下(实际上现在的项目很少直接自己在html里引用js,都前端工程化了)

// math.js
try { 
    module.exports = {
    	add
	}
} catch (e) {}

 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值