Jest

P1什么是 Jest?

Jest

   Jest 是Facebook的一个专门进行Javascript单元测试的工具,适合React全家桶使用,具有零配置、内置代码覆盖率、强大的Mocks等特点。


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


jest 是 facebook 开源的,用来进行单元测试的框架,可以测试 javascipt 和 react。 单元测试各种好处已经被说烂了,这里就不多扯了。重点要说的是,使用 jest, 可以降低写单元测试的难度。

单元测试做得好,能够极大提高软件的质量,加快软件迭代更新的速度, 但是,单元测试也不是银弹,单元测试做得好,并不是测试框架好就行,其实单元测试做的好不好,很大程度上取决于代码写的是否易于测试。 单元测试不仅仅是测试代码,通过编写单元测试 case,也可以反过来重构已有的代码,使之更加容易测试。


Jest是一个JavaScript测试框架,由Facebook用来测试所有JavaScript代码,包括React应用程序。

不同级别的自动化测试:单元、集成、组件和功能. 单元测试可以看作是和在组件级别测试JavaScript对象和方法一样的最基本的。默认情况下,React Native提供在Android和iOS都可以使用的Jest来进行单元测试。现在,测试的覆盖率并不完美,但是根据Facebook的说法,未来将会有更强大测试能力的工具被引入到React Native,同时用户也可以构建他们自己的测试工具。



作者:小涛先生呢
链接:https://www.jianshu.com/p/a656a5459e73
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

 

P2安装

安装:

  1. npm i jest -D(安装到本地)
  2. npm i jest -g(安装到全局)

2 安装Jest

2.1 初始化package.json

在shell中输入以下命令,初始化前端项目并生成package.json:

1

npm init -y

2.2 安装Jest及相关依赖

在shell中输入以下命令,安装测试所需要的依赖:

1

npm install -D jest babel-jest babel-core babel-preset-env regenerator-runtime

babel-jest、 babel-core、 regenerator-runtime、babel-preset-env这几个依赖是为了让我们可以使用ES6的语法特性进行单元测试,ES6提供的 import 来导入模块的方式,Jest本身是不支持的。

2.3 添加.babelrc文件

在项目的根目录下添加.babelrc文件,并在文件复制如下内容:

1

2

3

{

 "presets": ["env"]

}

2.4 修改package.json中的test脚本

打开package.json文件,将script下的test的值修改为jest:

1

2

3

"scripts": {

  "test": "jest"

  }


jest 的零配置思路是我最喜欢的特性。

安装方式

# yarn
yarn add --dev jest

# OR npm
npm install --save-dev jest

P2.5

虽然说Jest是零配置,但也是可以配置

(一)配置位置

1. package.json
package.json添加配置项"jest" : { 配置项 }

2. jest.config.js
新建jest.config.js并添加配置项module.exports = { 配置项 }

3. 命令行(独有的option)
见:命令行配置

(二)配置项

1. testMatch
设置识别哪些文件是测试文件(glob形式),与testRegex互斥,不能同时写

testMatch: ['\*\*/\_\_tests\_\_/\*\*/\*.js?(x)','\*\*/?(*.)(spec|test).js?(x)']

2. testRegex
设置识别哪些文件是测试文件(正则形式),与testMatch互斥,不能同时写

testRegex: '(/\_\_tests\_\_).*|(\\\\.|/)(test|spec))\\\\.jsx?$'

3. testRnviroment
测试环境,默认值是:jsdom,可修改为node

testEnvironment: 'jsdom'

4. rootDir
默认值:当前目录,一般是package.json所在的目录。

rootDir: ' '

5. moduleFileExtensions
测试文件的类型

moduleFileExtensions: ['js','json','jsx','node']

一般配置:

module.exports = {
    testMatch: ['<rootDir>/test/\*\*/\*.js'],
    testEnvironment: 'jsdom',
    rootDir: '',
    moduleFileExtensions: ['js','json','jsx','node']
}

 



作者:One_Hund
链接:https://www.jianshu.com/p/ce4f46cd9372
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

 

P3. 编写你的第一个Jest测试

 

运行:

  1. npx jest(安装到本地的前提下)
  2. jest(安装到全局的前提下)
  3. 修改"script" 中的"test" 为"jest"后使用npm run testnpm t

创建src和test目录及相关文件

在项目根目录下创建src目录,并在src目录下添加functions.js文件

在项目根目录下创建test目录,并在test目录下创建functions.test.js文件

Jest会自动找到项目中所有使用.spec.js或.test.js文件命名的测试文件并执行,通常我们在编写测试文件时遵循的命名规范:测试文件的文件名 = 被测试模块名 + .test.js,例如被测试模块为functions.js,那么对应的测试文件命名为functions.test.js。

在src/functions.js中创建被测试的模块

1

2

3

4

5

export default {

  sum(a, b) {

      return a + b;

  }

}

在test/functions.test.js文件中创建测试用例

1

2

3

4

import functions  from '../src/functions';

test('sum(2 + 2) 等于 4', () => {

  expect(functions.sum(2, 2)).toBe(4);

})

运行npm run test, Jest会在shell中打印出以下消息:

1

2

3

4

5

6

PASS  test/functions.test.js

  √ sum(2 + 2) 等于 4 (7ms)

Test Suites: 1 passed, 1 total

Tests:       1 passed, 1 total

Snapshots:   0 total

Time:        4.8s

 

 

 

 

 

 

P4断言

 

基本方法:

  • 分组(Test Group):descripe(描述语,function)
  • 测试用例(Test Case):test(描述语,function)
  • 断言(Assert):expect(运行需测试的方法并返回实际结果).toBe(预期结果)

例:

Pencil.query =(name, url)=> {  //方法,返回捕获
    // ?hello=test&wonghan=handsome
    var reg = new RegExp('(?:\\?|&)' + name + '=(.*?)(?:&|$)')
    var ret = reg.exec(url) || []
    return ret[1]
}
test('query',()=>{  // testCase
    // 断言
    expect(Pencil.query('hello', '?hello=test')).toBe('test')
    expect(Pencil.query('hello', '?hello2=test')).toBe(undefined)  
    //可以写多个`ecpect()`
})
test('query2',()=>{
    expect(Pencil.query('hello/test', '?hello/test=test')).toBe('test')
}) 
    //可以写多个`test()`
describe('test query',()=>{
    test('query3',()=>{  // testCase
        // assert
        expect(Pencil.query('hello', '?hello=test')).toBe('test')
        expect(Pencil.query('hello', '?hello2=test')).toBe(undefined)
    })
})

结果:



 


    Matchers俗称断言库,例如上面的expect().toBe()便是其中之一,其他常见用法如下:

1.相等断言

toBe(value): 比较数字、字符串
toEqual(value): 比较对象、数组
toBeNull()
toBeUndefined()

2.包含断言

toHaveProperty(keyPath, value): 是否有对应的属性
toContain(item): 是否包含对应的值,括号里写上数组、字符串
toMatch(regexpOrString): 括号里写上正则

3.逻辑断言

toBeTruthy()
toBeFalsy()
在JavaScript中,有六个falsy值:false0''nullundefined,和NaN。其他一切都是Truthy。

toBeGreaterThan(number): 大于
toBeLessThan(number): 小于

4.not

取反,用法见下面例子

例:

test('matchers',()=>{
    const a = {
        hello: 'jest',
        hi :{
            name: 'jest'
        }
    }
const b = {
    hello: 'jest',
    hi:{
        name: 'jest'
    }
}
// 以下结果均为true
expect(a).toEqual(b)
expect([1,2,3]).toEqual([1,2,3])
expect(null).toBeNull()
expect([1,2,3]).toContain(1)
expect(b).toHaveProperty('hi')
expect('123').toContain('2')
expect('123').toMatch(/^\d+$/)
expect('123').not.toContain('4')
})
  • 使用npx jest测试执行,结果为passed



作者:One_Hund
链接:https://www.jianshu.com/p/ce4f46cd9372
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。


https://www.jianshu.com/p/a656a5459e73

上面测试用例中的expect(functions.sum(2, 2)).toBe(4)为一句断言,Jest为我们提供了expect函数用来包装被测试的方法并返回一个对象,该对象中包含一系列的匹配器来让我们更方便的进行断言,上面的toBe函数即为一个匹配器。我们来介绍几种常用的Jest断言,其中会涉及多个匹配器。

.not

1

2

3

4

5

//functions.test.js

import functions  from '../src/functions'

test('sum(2, 2) 不等于 5', () => {

  expect(functions.sum(2, 2)).not.toBe(5);

})

.not修饰符允许你测试结果不等于某个值的情况,这和英语的语法几乎完全一样,很好理解。

.toEqual()

1

2

3

4

5

6

7

8

9

// functions.js

export default {

  getAuthor() {

      return {

            name: 'LITANGHUI',     

            age: 24,

    }

  }

}

1

2

3

4

5

6

7

8

// functions.test.js

import functions  from '../src/functions';

test('getAuthor()返回的对象深度相等', () => {

  expect(functions.getAuthor()).toEqual(functions.getAuthor());

})

test('getAuthor()返回的对象内存地址不同', () => {

  expect(functions.getAuthor()).not.toBe(functions.getAuthor());

})

.toEqual匹配器会递归的检查对象所有属性和属性值是否相等,所以如果要进行应用类型的比较时,请使用.toEqual匹配器而不是.toBe。

.toHaveLength

1

2

3

4

5

6

7

8

9

10

11

12

13

// functions.js

export default {

  getIntArray(num) {

      if (!Number.isInteger(num)) {

            throw Error('"getIntArray"只接受整数类型的参数');

    }

        let result = [];   

        for (let i = 0, len = num; i < len; i++) {

      result.push(i);

    }   

    return result;

  }

}

1

2

3

4

5

// functions.test.js

import functions  from '../src/functions';

test('getIntArray(3)返回的数组长度应该为3', () => {

  expect(functions.getIntArray(3)).toHaveLength(3);

})

.toHaveLength可以很方便的用来测试字符串和数组类型的长度是否满足预期。

.toThrow

1

2

3

4

5

6

7

8

// functions.test.js

import functions  from '../src/functions';

test('getIntArray(3.3)应该抛出错误', () => {

  function getIntArrayWrapFn() {

    functions.getIntArray(3.3);

  }

  expect(getIntArrayWrapFn).toThrow('"getIntArray"只接受整数类型的参数');

})

.toThorw可能够让我们测试被测试方法是否按照预期抛出异常,但是在使用时需要注意的是:我们必须使用一个函数将将被测试的函数做一个包装,正如上面getIntArrayWrapFn所做的那样,否则会因为函数抛出导致该断言失败。

.toMatch

1

2

3

4

5

// functions.test.js

import functions  from '../src/functions';

test('getAuthor().name应该包含"li"这个姓氏', () => {

  expect(functions.getAuthor().name).toMatch(/li/i);

})

.toMatch传入一个正则表达式,它允许我们用来进行字符串类型的正则匹配。


.expect(value)

.lastCalledWith(arg1, arg2, ...) is an alias for .toHaveBeenLastCalledWith(arg1, arg2, ...)

.not

.toBe(value)

.toBeCalled() is an alias for .toHaveBeenCalled()

.toBeCalledWith(arg1, arg2, ...) is an alias for .toHaveBeenCalledWith(arg1, arg2, ...)

.toHaveBeenCalled()

.toHaveBeenCalledTimes(number)

.toHaveBeenCalledWith(arg1, arg2, ...)

.toHaveBeenLastCalledWith(arg1, arg2, ...)

.toBeCloseTo(number, numDigits)

.toBeDefined()

.toBeFalsy()

.toBeGreaterThan(number)

.toBeGreaterThanOrEqual(number)

.toBeLessThan(number)

.toBeLessThanOrEqual(number)

.toBeInstanceOf(Class)

.toBeNull()

.toBeTruthy()

.toBeUndefined()

.toContain(item)

.toContainEqual(item)

.toEqual(value)

.toHaveLength(number)

.toMatch(regexp)

.toMatchObject(object)

.toMatchSnapshot()

.toThrow()

.toThrowError(error)

.toThrowErrorMatchingSnapshot()



作者:小涛先生呢
链接:https://www.jianshu.com/p/a656a5459e73
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

使用匹配器

使用不同匹配器可以测试输入输出的值是否符合预期。下面介绍一些常见的匹配器。

普通匹配器

最简单的测试值的方法就是看是否精确匹配。首先是toBe()

test('two plus two is four', () => {
  expect(2 + 2).toBe(4);
});

toBe用的是JavaScript中的Object.is(),属于ES6中的特性,所以不能检测对象,如果要检测对象的值的话,需要用到toEqual。toEquel递归检查对象或者数组中的每个字段。

test('object assignment', () => {
  const data = {one: 1};
  data['two'] = 2;
  expect(data).toEqual({one: 1, two: 2});
});

Truthiness
在实际的测试中,我们有时候需要区分undefined、null和false。以下的一些规则有助于我们进行。

  • toBeNull只匹配null
  • toBeUndefined只匹配undefined
  • toBeDefine与toBeUndefined相反
  • toBeTruthy匹配任何if语句为真
  • toBeFalsy匹配任何if语句为假

数字匹配器
大多数的比较数字有等价的匹配器。

  • 大于。toBeGreaterThan()
  • 大于或者等于。toBeGreaterThanOrEqual()
  • 小于。toBeLessThan()
  • 小于或等于。toBeLessThanOrEqual()
  • toBe和toEqual同样适用于数字
    注意:对比两个浮点数是否相等的时候,使用toBeCloseTo而不是toEqual

例子如下:

test('two plus two', () => {
  const value = 2 + 2;
  expect(value).toBeGreaterThan(3);
  expect(value).toBeGreaterThanOrEqual(3.5);
  expect(value).toBeLessThan(5);
  expect(value).toBeLessThanOrEqual(4.5);

  // toBe and toEqual are equivalent for numbers
  expect(value).toBe(4);
  expect(value).toEqual(4);
});
test('两个浮点数字相加', () => {
  const value = 0.1 + 0.2;
  //expect(value).toBe(0.3);           这句会报错,因为浮点数有舍入误差
  expect(value).toBeCloseTo(0.3); // 这句可以运行
});

如果使用toBe就会产生以下结果:

错误

字符串
使用toMatch()测试字符串,传递的参数是正则表达式。

test('there is no I in team', () => {
  expect('team').not.toMatch(/I/);
});

test('but there is a "stop" in Christoph', () => {
  expect('Christoph').toMatch(/stop/);
});

数组
如何检测数组中是否包含特定某一项?可以使用toContain()

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

test('购物清单(shopping list)里面有啤酒(beer)', () => {
  expect(shoppingList).toContain('beer');
});

另外
如果你想在测试特定函数的时候抛出一个错误,在它调用的时候可以使用toThrow。

function compileAndroidCode() {
  throw new ConfigError('you are using the wrong JDK');
}

test('compiling android goes as expected', () => {
  expect(compileAndroidCode).toThrow();
  expect(compileAndroidCode).toThrow(ConfigError);

  // You can also use the exact error message or a regexp
  expect(compileAndroidCode).toThrow('you are using the wrong JDK');
  expect(compileAndroidCode).toThrow(/JDK/);
});



作者:GpingFeng
链接:https://www.jianshu.com/p/e54218d67628
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。



P5函数mock


(一)Mock Function

  • 主要使用let mockFunc = jest.fn()来模拟function(),可测试调用的次数传入的参数
function forEach(items,callback) {
  for(let index = 0;index<items.length;index++){
    callback(items[index])
  }
}
test('forEach',()=>{
  // mockFunc模拟函数
  let mockFunc = jest.fn()

  forEach([1,2,3],mockFunc)
  // 调用了多少次
  expect(mockFunc.mock.calls.length).toBe(3)
   // 参数
  expect(mockFunc.mock.calls[0]).toEqual([1])
  expect(mockFunc.mock.calls[0][0]).toBe(1)
})
  • 使用npx jest测试执行,结果为passed
     

(二)manual_mock

  • 用于手动模拟测试模块,覆盖模块依赖。如无法调用AjaxjQuery进行测试的情况下,自己手动写一个有相同逻辑的"Ajax""jQuery"进行模块覆盖,以此达到正常测试的目的

  • 用法:应在相应的目录下新建__mock__文件夹,并在文件夹里新建manual_mock.js

    • 若模拟本地模块,则在同目录下新建__mock__文件夹,并在文件夹里新建manual_mock.js
    • 若模拟node_modules文件夹里的模块,则在node_modules文件夹相应目录(一般是根目录)下新建__mock__文件夹,并在文件夹里新建manual_mock.js。例如本文章新建一个jquery.js为例。
  • /__mock__/jquery.js

module.exports = (selector) => document.querySelector(selector)
  • /test/mocks.js
jest.mock('jquery')
const $ = require('jquery')

test('jquery',()=>{
  document.body.innerHTML = '<div id="jquery">jquery</div>'
  expect($('#jquery').innerHTML).toBe('jquery')
})
  • 使用npx jest测试执行,结果为passed



作者:One_Hund
链接:https://www.jianshu.com/p/c4740a2bef95
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

 


三个jest.fn(),jest.mock()jest.spyOn()https://www.jianshu.com/p/70a4f026a0f1

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

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

  • 捕获函数调用情况
  • 设置函数返回值
  • 改变函数的内部实现

我们接着使用上篇文章中的目录结构,在test/functions.test.js文件中编写测试代码,src/目录下写被测试代码。

1. jest.fn()

jest.fn()是创建Mock函数最简单的方式,如果没有定义函数内部的实现,jest.fn()会返回undefined作为返回值。

// functions.test.js

test('测试jest.fn()调用', () => {
  let mockFn = jest.fn();
  let result = mockFn(1, 2, 3);

  // 断言mockFn的执行后返回undefined
  expect(result).toBeUndefined();
  // 断言mockFn被调用
  expect(mockFn).toBeCalled();
  // 断言mockFn被调用了一次
  expect(mockFn).toBeCalledTimes(1);
  // 断言mockFn传入的参数为1, 2, 3
  expect(mockFn).toHaveBeenCalledWith(1, 2, 3);
})

jest.fn()所创建的Mock函数还可以设置返回值定义内部实现返回Promise对象

// functions.test.js

test('测试jest.fn()返回固定值', () => {
  let mockFn = jest.fn().mockReturnValue('default');
  // 断言mockFn执行后返回值为default
  expect(mockFn()).toBe('default');
})

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.fn()提供的几个常用的API和断言语句,下面我们在src/fetch.js文件中写一些被测试代码,以更加接近业务的方式来理解Mock函数的实际应用。

被测试代码中依赖了axios这个常用的请求库和JSONPlaceholder这个上篇文章中提到免费的请求接口�,请先在shell中执行npm install axios --save安装依赖,。

// fetch.js

import axios from 'axios';

export default {
  async fetchPostsList(callback) {
    return axios.get('https://jsonplaceholder.typicode.com/posts').then(res => {
      return callback(res.data);
    })
  }
}

我们在fetch.js中封装了一个fetchPostsList方法,该方法请求了JSONPlaceholder提供的接口,并通过传入的回调函数返回处理过的返回值。如果我们想测试该接口能够被正常请求,只需要捕获到传入的回调函数能够被正常的调用即可。下面是functions.test.js中的测试的代码。

import fetch from '../src/fetch.js'

test('fetchPostsList中的回调函数应该能够被调用', async () => {
  expect.assertions(1);
  let mockFn = jest.fn();
  await fetch.fetchPostsList(mockFn);

  // 断言mockFn被调用
  expect(mockFn).toBeCalled();
})

2. jest.mock()

fetch.js文件夹中封装的请求方法可能我们在其他模块被调用的时候,并不需要进行实际的请求(请求方法已经通过单侧或需要该方法返回非真实数据)。此时,使用jest.mock()去mock整个模块是十分有必要的。

下面我们在src/fetch.js的同级目录下创建一个src/events.js

// events.js

import fetch from './fetch';

export default {
  async getPostList() {
    return fetch.fetchPostsList(data => {
      console.log('fetchPostsList be called!');
      // do something
    });
  }
}

functions.test.js中的测试代码如下:

// functions.test.js

import events from '../src/events';
import fetch from '../src/fetch';

jest.mock('../src/fetch.js');

test('mock 整个 fetch.js模块', async () => {
  expect.assertions(2);
  await events.getPostList();
  expect(fetch.fetchPostsList).toHaveBeenCalled();
  expect(fetch.fetchPostsList).toHaveBeenCalledTimes(1);
});

在测试代码中我们使用了jest.mock('../src/fetch.js')去mock整个fetch.js模块。如果注释掉这行代码,执行测试脚本时会出现以下报错信息

从这个报错中,我们可以总结出一个重要的结论:

在jest中如果想捕获函数的调用情况,则该函数必须被mock或者spy!

3. jest.spyOn()

jest.spyOn()方法同样创建一个mock函数,但是该mock函数不仅能够捕获函数的调用情况,还可以正常的执行被spy的函数。实际上,jest.spyOn()jest.fn()的语法糖,它创建了一个和被spy的函数具有相同内部代码的mock函数。

上图是之前jest.mock()的示例代码中的正确执行结果的截图,从shell脚本中可以看到console.log('fetchPostsList be called!');这行代码并没有在shell中被打印,这是因为通过jest.mock()后,模块内的方法是不会被jest所实际执行的。这时我们就需要使用jest.spyOn()

// functions.test.js

import events from '../src/events';
import fetch from '../src/fetch';

test('使用jest.spyOn()监控fetch.fetchPostsList被正常调用', async() => {
  expect.assertions(2);
  const spyFn = jest.spyOn(fetch, 'fetchPostsList');
  await events.getPostList();
  expect(spyFn).toHaveBeenCalled();
  expect(spyFn).toHaveBeenCalledTimes(1);
})

执行npm run test后,可以看到shell中的打印信息,说明通过jest.spyOn()fetchPostsList被正常的执行了。

4. 总结

这篇文章中我们介绍了jest.fn(),jest.mock()jest.spyOn()来创建mock函数,通过mock函数我们可以通过以下三个特性去更好的编写我们的测试代码:

  • 捕获函数调用情况
  • 设置函数返回值
  • 改变函数的内部实现

在实际项目的单元测试中,jest.fn()常被用来进行某些有回调函数的测试;jest.mock()可以mock整个模块中的方法,当某个模块已经被单元测试100%覆盖时,使用jest.mock()去mock该模块,节约测试时间和测试的冗余度是十分必要;当需要测试某些必须被完整执行的方法时,常常需要使用jest.spyOn()。这些都需要开发者根据实际的业务代码灵活选择。



作者:李棠辉
链接:https://www.jianshu.com/p/ad87eaf54622
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

P6异步调用


在实际开发过程中,我们经常会遇到一些异步的JavaScript代码。当你有以异步方式运行的代码的时候,Jest需要知道当前它测试的代码是否已经完成,然后它可以转移动另一个测试。也就是说,测试用例一定要在测试对象结束之后才能够结束

为了达到这一目的,Jest有多种方法可以做到。
回调
最常见的异步模式就是回调函数。

注意:回调函数和异步没有必然的联系,回调只是异步的一种调用方式而已,不要将异步和回调两个概念结合起来谈

比如以下代码:

// 这里是同步执行的,完全没有异步
function fun1(callback) {
  callback();
}

现在假设一个fetchData(call)函数,获取一些数据并在完成的时候调用call(data),而我想要测试返回的数据是不是字符串'peanut butter'

默认情况下,一旦到达运行上下文底部,jest测试就会立即结束。这意味着这个测试将不能按照预期的进行。

function fetchData(call) {
  setTimeout(() => {
    call('peanut butter1')
  },1000);
}

test('the data is peanut butter', () => {
  function callback(data) {
    expect(data).toBe('peanut butter'); // 这里没有执行到
    // done()
  }
  fetchData(callback);
});

这样做是不会报错的,因为没有执行到我们想要测试的语句中的时候Jest测试已经结束了。(一旦fetchData执行结束,此测试就在没有调用回调函数前结束,因为使用了setTimeout,产生了异步)

而我们可以改成以下:
使用单个参数调用done,而不是将测试放在一个空参数的函数中,Jest会等done回调函数执行结束后,结束测试。

function fetchData(call) {
  setTimeout(() => {
    call('peanut butter1')
  },1000);
}

test('the data is peanut butter', (done) => {
  function callback(data) {
    expect(data).toBe('peanut butter');
    done()
  }
  fetchData(callback);
});

可行

如果done()永远不会被调用,则说明这个测试将失败,这也正是我们所希望看到的。

Promise
如果我们的代码中使用到了Promises,只需要从你的测试中返回一个Promise,Jest就会等待这个Promise来解决。如果承诺被拒绝,则测试将会自动失败。

举个例子,如果fetchData,使用Promise代替回调的话,返回值是应该解析为一个字符串'peanut butter'的Promise。那么我们可以使用以下方式进行测试代码:

test('the data is peanut butter', () => {
  expect.assertions(1);
  return fetchData().then(data => {
    expect(data).toBe('peanut butter');
  });
});

注意:一定要返回Promise,如果省略了return语句,测试将会在fetchData完成之前完成。

另外一种情况,就是想要Promise被拒绝,我们可以使用.catch方法。另外,要确保添加了expect.assertions来验证一定数量的断言被调用。否则一个fulfilled态的Promise不会让测试失败。

test('the fetch fails with an error', () => {
  expect.assertions(1);
  return fetchData().catch(e => expect(e).toMatch('error'));
});

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

test('the data is peanut butter', () => {
  expect.assertions(1);
  return expect(fetchData()).resolves.toBe('peanut butter');
});
test('the fetch fails with an error', () => {
  expect.assertions(1);
  return expect(fetchData()).rejects.toMatch('error');
});

Async/Await
若要编写async测试,只要在函数前面使用async关键字传递到test。比如,可以用来测试相同的fetchData()方案

test('the data is peanut butter', async () => {
  expect.assertions(1);
  const data = await fetchData();
  expect(data).toBe('peanut butter');
});

test('the fetch fails with an error', async () => {
  expect.assertions(1);
  try {
    await fetchData();
  } catch (e) {
    expect(e).toMatch('error');
  }
});



作者:GpingFeng
链接:https://www.jianshu.com/p/e54218d67628
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。


上面的是普通函数,对于异步函数,比如 ajax 请求,测试写法同样容易 待测试文件:utils/client.js

export const get = (url, headers = {}) => {
  return fetch(url, {
    method: 'GET',
    headers: {
      ...getHeaders(),
      ...headers
    }
  }).then(parseResponse)
}

测试文件:client.test.js

import { get } from 'utils/client'

test('fetch by get method', async () => {
  expect.assertions(1)
  // 测试使用了一个免费的在线 JSON API
  const url = 'https://jsonip.com/'

  const data = await get(url)
  const { about } = data
  expect(about).toBe('/about')
})

 

 


 

5 测试异步函数

安装axios
这里我们使用最常用的http请求库axios来进行请求处理

1

npm install axios

编写http请求函数
我们将请求http://jsonplaceholder.typicode.com/users/1,这是由JSONPlaceholder提供的mock请求地址

image_1cm9b6gjprkq1ij11o48a4b1as01j.png

 

JSONPlaceholder

1

2

3

4

5

6

7

8

9

// functions.js

import axios from 'axios';

export default {

  fetchUser() {

      return axios.get('http://jsonplaceholder.typicode.com/users/1')

      .then(res => res.data)

      .catch(error => console.log(error));

  }

}

1

2

3

4

5

6

7

8

9

// functions.test.js

import functions  from '../src/functions';

test('fetchUser() 可以请求到一个含有name属性值为Leanne Graham的对象', () => {

  expect.assertions(1); 

  return functions.fetchUser()

    .then(data => {

      expect(data.name).toBe('Leanne Graham');

    });

})

上面我们调用了expect.assertions(1),它能确保在异步的测试用例中,有一个断言会在回调函数中被执行。这在进行异步代码的测试中十分有效。

使用async和await精简异步代码

1

2

3

4

5

test('fetchUser() 可以请求到一个用户名字为Leanne Graham', async () => {

  expect.assertions(1);

    const data =  await functions.fetchUser();

  expect(data.name).toBe('Leanne Graham')

})

当然我们既然安装了Babel,为何不使用async和await的语法来精简我们的异步测试代码呢? 但是别忘记都需要调用expect.assertions方法。

P7测试的生命周期

jest 测试提供了一些测试的生命周期 API,可以辅助我们在每个 case 的开始和结束做一些处理。 这样,在进行一些和数据相关的测试时,可以在测试前准备一些数据,在测试后,清理测试数据。

4 个主要的生命周期函数:

  • afterAll(fn, timeout): 当前文件中的所有测试执行完成后执行 fn, 如果 fn 是 promise,jest 会等待 timeout 毫秒,默认 5000
  • afterEach(fn, timeout): 每个 test 执行完后执行 fn,timeout 含义同上
  • beforeAll(fn, timeout): 同 afterAll,不同之处在于在所有测试开始前执行
  • beforeEach(fn, timeout): 同 afterEach,不同之处在于在每个测试开始前执行
BeforeAll(() => {
  console.log('before all tests to excute !')
})

BeforeEach(() => {
  console.log('before each test !')
})

AfterAll(() => {
  console.log('after all tests to excute !')
})

AfterEach(() => {
  console.log('after each test !')
})

Test('test lifecycle 01', () => {
  expect(1 + 2).toBe(3)
})

Test('test lifecycle 03', () => {
  expect(2 + 2).toBe(4)
})

 

P8setup and teardown:

写测试的时候,我们经常需要进行测试之前做一些准备工作,和在进行测试后需要进行一些整理工作。Jest提供辅助函数来处理这个问题。

为多次测试重复设置
如果你有一些要为多次测试重复设置的工作,可以使用beforeEach和afterEach。

有这样一个需求,需要我们在每个测试之前调用方法initializeCityDatabase(),在每个测试后,调用方法clearCityDatabase()

beforeEach(() => {
  initializeCityDatabase();
});

afterEach(() => {
  clearCityDatabase();
});

test('city database has Vienna', () => {
  expect(isCity('Vienna')).toBeTruthy();
});

test('city database has San Juan', () => {
  expect(isCity('San Juan')).toBeTruthy();
});

一次性设置
在某些情况下,你只需要在文件的开头做一次设置。这种设置是异步行为的时候,你不太可能一行处理它。Jest提供了beforeAll和afterAll处理这种情况。

beforeAll(() => {
  return initializeCityDatabase();
});

afterAll(() => {
  return clearCityDatabase();
});

test('city database has Vienna', () => {
  expect(isCity('Vienna')).toBeTruthy();
});

test('city database has San Juan', () => {
  expect(isCity('San Juan')).toBeTruthy();
});

作用域
默认情况下,before和after的块可以应用到文件中的每一个测试。此外可以通过describe块来将将测试中的某一块进行分组。当before和after的块在describe块内部的时候,则只适用于该describe块内的测试。

比如说,我们不仅有一个城市的数据库,还有一个食品数据库。我们可以为不同的测试做不同的设置︰

// Applies to all tests in this file
beforeEach(() => {
  return initializeCityDatabase();
});

test('city database has Vienna', () => {
  expect(isCity('Vienna')).toBeTruthy();
});

test('city database has San Juan', () => {
  expect(isCity('San Juan')).toBeTruthy();
});

describe('matching cities to foods', () => {
  // Applies only to tests in this describe block
  beforeEach(() => {
    return initializeFoodDatabase();
  });

  test('Vienna <3 sausage', () => {
    expect(isValidCityFoodPair('Vienna', 'Wiener Schnitzel')).toBe(true);
  });

  test('San Juan <3 plantains', () => {
    expect(isValidCityFoodPair('San Juan', 'Mofongo')).toBe(true);
  });
});

注意:顶级的beforeEach描述块内的beforeEach之前执行,以下的例子可以方便我们认识到执行的顺序

beforeAll(() => console.log('1 - beforeAll'));
afterAll(() => console.log('1 - afterAll'));
beforeEach(() => console.log('1 - beforeEach'));
afterEach(() => console.log('1 - afterEach'));
test('', () => console.log('1 - test'));
describe('Scoped / Nested block', () => {
  beforeAll(() => console.log('2 - beforeAll'));
  afterAll(() => console.log('2 - afterAll'));
  beforeEach(() => console.log('2 - beforeEach'));
  afterEach(() => console.log('2 - afterEach'));
  test('', () => console.log('2 - test'));
});

// 1 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
// 2 - beforeAll
// 1 - beforeEach  //特别注意
// 2 - beforeEach
// 2 - test
// 2 - afterEach
// 1 - afterEach
// 2 - afterAll
// 1 - afterAll



P9

测试DOM

1.操作DOM

简单例子:

/**
* 移除节点
* @param {DOM} node
*/
Pencil.removeNode = node => {
  return node.parentNode.removeChild(node)
}

/**
* 绑定事件
*/
Pencil.on = (node,type,handle) => {
  node.addEventListener(type,handle,false)
}

describe('test node',()=>{
  test('remove node',()=>{
    document.body.innerHTML = '<div id="p"><p id="c"></p></div>'
    const p = document.getElementById('p')
    expect(p.nodeName.toLowerCase()).toBe('div')

    const c = document.getElementById('c')
    Pencil.removeNode(c)
    expect(document.getElementById('c')).toBeNull()
  })
})

describe('test on',()=>{
  test('on',()=>{
    document.body.innerHTML = '<div id="p"><button id="c">点击</button></div>'
    const btn = document.getElementById('c')
    Pencil.on(btn,'click',()=>{
        btn.innerHTML = 'clicked!'
    })
    btn.click() //模拟点击
    expect(btn.innerHTML).toBe('clicked!')
  })
})
  • 使用npx jest测试执行,结果为passed
     

2.测试DOM原理

  • Jest能做 DOM 操作,是因为内置了 JSDOM
  • JSDOM是一套模拟的 DOM 环境,在 node 上运行
  • 如果可以的话,Chrome headless是比JSDOM更好的选择
     

3.测试 Radio 组件

  • 测试只能测试逻辑(是否包含某类名或属性)
  • 例:
    1. radio.html
<div id="radio" class="radio">
  <span class="case"><span class="letter">B</span></span>
  <span class="case"><span class="letter">I</span></span>
  <span class="case"><span class="letter">U</span></span>
</div>

2. ../js/radio.js(组件逻辑)

function Radio(options) {
  const def = {
    boxId:'',
    active: 0,
    activeClass: 'radio-active'
  }
  // 重点!初始化
  const opts = this.opts = Object.assign({},def,options)
  const box = this.box = document.getElementById(opts.boxId)
  this.case = box.querySelectorAll('.case')
  this.letter = box.querySelectorAll('.letter')
  this.unactive()
  this.active(opts.active)
  this.events()
}
  
Radio.prototype.active =function(active){
  let cur = this.current
  if(typeof cur==='number') {
    this.case[cur].classList.remove(this.opts.activeClass)
    this.letter[cur].classList.remove(this.opts.activeClass)
  }
  this.case[active].classList.add(this.opts.activeClass)
  this.letter[active].classList.add(this.opts.activeClass)
  this.current = active
}
  
Radio.prototype.unactive = function(){
  const len=this.letter.length
  for(var i=0;i<len;i++){
    this.case[i].classList.remove(this.opts.activeClass)
  }
}
Radio.prototype.events = function(){
  const self = this
  for(var i = 0,len=this.case.length;i<len;i++){
    const casei = self.case[i]
    casei.order = i
    casei.addEventListener('click',function(){
        self.active(this.order)
    },false)
  }
}
module.exports = Radio

// 以上若有修改属性的操作可写为[例]:this.case[i].style.display = 'none'

  • 使用npx jest测试执行,结果为passed

3. ../test/radio.js(测试用例)

const Radio = require('../js/radio')
const fs = require('fs')
const path = require('path')

test('Radio',()=>{
// 重点!同步加载页面
  document.body.innerHTML = fs.readFileSync(path.resolve(__dirname,'./assert/radio.html'))
  const radio = new Radio({
    boxId:'radio'
  })
  expect(radio.case[0].classList.contains('radio-active')).toBe(true)
  expect(radio.case[1].classList.contains('radio-active')).toBe(false)
  expect(radio.case[2].classList.contains('radio-active')).toBe(false)
})

test('Radio click',()=>{
  document.body.innerHTML = fs.readFileSync(path.resolve(__dirname,'./assert/radio.html'))
  const radio = new Radio({
      boxId:'radio'
  })
  radio.case[2].click()
  expect(radio.case[0].classList.contains('radio-active')).toBe(false)
  expect(radio.case[1].classList.contains('radio-active')).toBe(false)
  expect(radio.case[2].classList.contains('radio-active')).toBe(true)
})
  • 使用npx jest测试执行,结果为passed
     

二、测试异步

  • 通过使用done实现
    简单例子:
function delay(callback){
  setTimeout(()=>{
    callback('give you data')
  },1000)
}

test('async',(done)=>{
  delay((data)=>{
    expect(data).toBe('give you data')
    done()
  })
})
  • 使用npx jest测试执行,结果为passed



作者:One_Hund
链接:https://www.jianshu.com/p/f837d6afc9c6
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

 

reference:

[1].http://www.php.cn/js-tutorial-411835.html

[2].https://www.jianshu.com/p/a656a5459e73

[3].https://www.jianshu.com/p/ad87eaf54622

[4].https://www.jianshu.com/p/e54218d67628

[5].https://www.jianshu.com/p/f837d6afc9c6

[6].https://www.jianshu.com/p/c4740a2bef95

  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值