foreword

Although many front-end teams are unlikely to use unit testing now or even in the future, including my own team, the reason is nothing more than delaying time, the development task itself is relatively heavy and so on.

But I think blindly trying to be fast is always drinking poison to quench thirst, falling into a vicious circle, 项目快 \--> 代码烂 \--> 修改和加功能花费更多的时间和精力 \--> 来不及做优化必须更快 \--> 项目快 \--> 代码烂 \-->an infinite loop.

This is why I think the most important reason for doing unit testing is that when refactoring the code, confirm that there is no problem with the function, not afraid of personnel turnover, function migration, and most importantly 产品撕b, the test case is the best proof 😁.

If you can’t use it for business projects, if you write a library and don’t write a single test, the students who may use it will have scruples, so writing a single test is a necessary skill for the front-end above the advanced level.

Unit Testing Framework Fundamentals

For example, the following test case, feel the basic appearance, we will implement a simple version of the method used in it later

// 意思是字符串hello是否包含ll
test('测试字符串中是否包含 ll'), () => {
    expect(findStr('hello')).toMatch('ll')
})

function findStr(str){
    return `${str} world`
}
复制代码

We can simply implement the methods used in the above test cases test、expect、toMatch, so that even if we have mastered the basic principles of the test framework

test

function test(desc, fn){
    try{
        fn();
        console.log(`✅  通过测试用例`)
    }catch{
        console.log(`❌ 没有通过测试用例`)
    }
}
复制代码

expect, toMatch

function expect(ret){
    return {
        toMatch(expRet){
            if(typeof ret === 'string'){ throw Error('') }
            if(!ret.includes(expRet)){ throw Error('') }
        }
    }
}
复制代码

jest basic configuration

Necessary tools:

$ npm i -D jest babel-jest ts-jest @types/jest
复制代码

Refer to the configuration jest.config.js, the test files are placed in the tests directory: the following testRegex represents the jsx or tsx files ending with test or spec in the matching tests folder

module.exports = {
  transform: {
    '^.+\\.tsx?$''ts-jest',
  },
  testRegex'(/tests/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$',
  moduleFileExtensions: ['tsx''ts''js''jsx''json''node'],
};
复制代码

Finally, add it to the scripts of package.json

{
    test"jest"
    // 如果要测试覆盖率,后面加上--coverage
    // 如果要监听所有测试文件 --watchAll
}
复制代码

matcher

Matchers are a very important concept in Jest, which can provide many ways for you to verify the return value of your test. Let's take an example to understand what a matcher is.

The matchers here can be glanced at, and you probably know that there is such a thing. Just look up the matchers you want when you use them, and you don't need to memorize them deliberately.

Equal matching, this is our most commonly used matching rule

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

In this code, the result we expect expact(2 + 2)will be returned. Usually, we only need to call expectit. The parentheses can be a function with a return value or an expression. The latter toBeis a matcher.

Here are some commonly used matchers:

normal matcher

  • toBe: object.is is equivalent to ===
test('测试加法 3 + 7', () => {
  // toBe 匹配器 matchers object.is 相当于 ===
  expect(10).toBe(10)
})
复制代码
  • toEqual: content is equal, matches content, does not match reference
test('toEqual 匹配器', () => {
  // toEqual 匹配器 只会匹配内容,不会匹配引用
  const a = { one1 }
  expect(a).toEqual({ one1 })
})
复制代码

Matchers related to true and false

  • true and false
  • toBeNull: only matches Null
test('toBeNull 匹配器', () => {
  // toBeNull
  const a = null
  expect(a).toBeNull()
})
复制代码

toBeUndefined: only matches undefined

test('toBeUndefined 匹配器', () => {
  const a = undefined
  expect(a).toBeUndefined()
})
复制代码

toBeDefined: Contrary to toBeUndefined, matching null is passed here

test('toBeDefined 匹配器', () => {
  const a = null
  expect(a).toBeDefined()
})
复制代码

toBeTruthy: matches any if statement to true

test('toBeTruthy 匹配器', () => {
  const a = 1
  expect(a).toBeTruthy()
})
复制代码

toBeFalsy: matches any if statement to false

test('toBeFalsy 匹配器', () => {
  const a = 0
  expect(a).toBeFalsy()
})
复制代码

not: negate

test('not 匹配器', () => {
  const a = 1
  // 以下两个匹配器是一样的
  expect(a).not.toBeFalsy()
  expect(a).toBeTruthy()
})
复制代码

number

toBeGreaterThan: Greater than

test('toBeGreaterThan', () => {
  const count = 10
  expect(count).toBeGreaterThan(9)
})
复制代码

toBeLessThan: less than

test('toBeLessThan', () => {
  const count = 10
  expect(count).toBeLessThan(12)
})
复制代码

toBeGreaterThanOrEqual: greater than or equal to

test('toBeGreaterThanOrEqual', () => {
  const count = 10
  expect(count).toBeGreaterThanOrEqual(10// 大于等于 10
})
复制代码

toBeLessThanOrEqual: less than or equal to

test('toBeLessThanOrEqual', () => {
  const count = 10
  expect(count).toBeLessThanOrEqual(10// 小于等于 10
})
复制代码

toBeCloseTo: Calculate floating point numbers

test('toBeCloseTo', () => {
  const firstNumber = 0.1
  const secondNumber = 0.2
  expect(firstNumber + secondNumber).toBeCloseTo(0.3// 计算浮点数
})
复制代码

string

toMatch: match a specific item string, support regular

test('toMatch', () => {
  const str = 'http://www.zsh.com'
  expect(str).toMatch('zsh')
  expect(str).toMatch(/zsh/)
})
复制代码

array

toContain: whether the match contains a specific item

test('toContain', () => {
  const arr = ['z''s''h']
  const data = new Set(arr)
  expect(data).toContain('z')
})
复制代码

abnormal

toThrow

const throwNewErrorFunc = () => {
  throw new Error('this is a new error')
}
test('toThrow', () => {
  // 抛出的异常也要一样才可以通过,也可以写正则表达式
  expect(throwNewErrorFunc).toThrow('this is a new error')
})
复制代码

Test asynchronous code

Suppose the request function is as follows

const fethUserInfo = fetch('http://xxxx')
复制代码

There are several ways to test asynchronous code, I recommend the one that I think is more common

// fetchData.test.js

// 测试promise成功需要加.resolves方法
test('the data is peanut butter'async () => {
    await expect(fethUserInfo()).resolves.toBe('peanut butter');
});

// 测试promise成功需要加.rejects方法
test('the fetch fails with an error'async () => {
    await expect(fethUserInfo()).rejects.toMatch('error');
});
复制代码

scope

Jest provides a descible function to separate each test case, that is, put the related code into a group, so simple, just look at an example to understand.

// 分组一
describe('Test xxFunction', () => {
  test('Test default return zero', () => {
      expect(xxFunction()).toBe(0)
  })

  // ...其它test
})

// 分组二
describe('Test xxFunction2', () => {
  test('Pass 3 can return 9', () => {
      expect(xxFunction2(3)).toBe(9)
  })

  // ...其它test
})
复制代码

hook function

There are 4 hook functions in jest

  • beforeAll: execute before all tests
  • afterAll: After all tests are executed
  • beforeEach: executed before each test instance
  • afterEach: executed after each test instance completes

We illustrate why they are needed with an example.

index.jsWrite some methods to be tested in

export default class compute {
  constructor() {
    this.number = 0
  }
  addOne() {
    this.number += 1
  }
  addTwo() {
    this.number += 2
  }
  minusOne() {
    this.number -= 1
  }
  minusTwo() {
    this.number -= 2
  }
}
复制代码

Suppose we want index.test.jsto write test instances in

import compute from './index'

const Compute = new compute()

test('测试 addOne', () => {
  Compute.addOne()
  expect(Compute.number).toBe(1)
})

test('测试 minusOne', () => {
  Compute.minusOne()
  expect(Compute.number).toBe(0)
})
复制代码
  • Here, the two test instances affect each other and share a computet instance. We can const Compute = new compute()solve it by placing it in beforeEach. Renew compute before each test instance.

  • Similarly, what you want to run separately after each test can be put afterEachinto

Let's take a look at what circumstances to use beforeAll, if we test whether the database data is saved correctly

  • At the very beginning of the test, that is, in the  beforeAlllife cycle, we add 1 piece of data to the database
  • After the test, that is,  in the afterAllcycle, delete the previously added data
  • Finally, use the global scope  afterAll to confirm whether the database is restored to its original state

here it says

// 模拟数据库
const userDB = [
  { id1name'小明' },
  { id2name'小花' },
]

// 新增数据
const insertTestData = data => {
  // userDB,push数据
}

// 删除数据
const deleteTestData = id => {
  // userDB,delete数据
}

// 全部测试完
afterAll(() => {
  console.log(userDB)
})

describe('Test about user data', () => {

  beforeAll(() => {
      insertTestData({ id99name'CS' })
  })
  afterAll(() => {
      deleteTestData(99)
  })

})
复制代码

Mock in jest

Why use Mock function?

In the project, it is often encountered that the A module drops the B module. Moreover, in unit testing, we may not need to care about the execution process and result of the method called internally, just want to know whether it is called correctly, and even specify the return value of the function. At this point, a mock function is needed.

The following three features provided by the Mock function are very useful when we write test code:

  • Capture function calls
  • set function return value
  • Change the internal implementation of the function

jest.fn()

jest.fn()It is the most common way to create mock functions.

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

  // 断言mockFn被调用
  expect(mockFn).toBeCalled();
  // 断言mockFn被调用了一次
  expect(mockFn).toBeCalledTimes(1);
  // 断言mockFn传入的参数为1
  expect(mockFn).toHaveBeenCalledWith(1);
})
复制代码

jest.fn()The created Mock function can also set the return value , define the internal implementation or return the Promiseobject .

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(1010)).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]");
})
复制代码

2. jest.mock()

fetch.jsThe request method encapsulated in the folder may not need to make an actual request when other modules are called (the request method has passed the single test or the method needs to return non-real data). jest.mock()At this point, it is very necessary to use to mock the entire module.

Next we src/fetch.jscreate one in the same level directory src/events.js.

import fetch from './fetch';

export default {
  async getPostList() {
    return fetch.fetchPostsList(data => {
      console.log('fetchPostsList be called!');
      // do something
    });
  }
}
复制代码
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);
});
复制代码

In the test code we use jest.mock('../src/fetch.js')to mock the entire fetch.jsmodule. If you comment out this line of code, the following error message will appear when executing the test script

From this error, we can draw an important conclusion:

In jest, if you want to capture the calling of a function, the function must be mocked or spy!

3. jest.spyOn()

jest.spyOn()The method also creates a mock function, but the mock function can not only capture the calling of the function, but also execute the spy function normally. Actually, jest.spyOn()yes jest.fn()syntactic sugar, it creates a mock function with the same internal code as the function being spyed.

The above picture is jest.mock()a screenshot of the correct execution result in the previous sample code. From the shell script, you can see console.log('fetchPostsList be called!');that this line of code is not printed in the shell, because jest.mock()after passing, the method in the module will not be actual by jest implemented. Then we need to use it 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);
})
复制代码

After execution npm run test, you can see the print information in the shell, indicating that it passed jest.spyOn()and fetchPostsListwas executed normally.

snapshot

A snapshot is to save a copy of the data you are comparing. What does it mean? Let's take an example:

This isindex.js

export const data2 = () => {
  return {
    name'zhangsan',
    age26,
    timenew Date()
  }
}
复制代码

index.test.jsWrite some test instances in

import { data2 } from "./index"

it('测试快照 data2', () => {
  expect(data2()).toMatchSnapshot({
    name'zhangsan',
    age26,
    time: expect.any(Date//用于声明是个时间类型,否则时间会一直改变,快照不通过
  })
})
复制代码
  • toMatchSnapshotwill match the parameters to the snapshot
  • expect.any(Date)for matching a time type

The execution npm run testwill generate a __snapshots__folder with the generated snapshots. When you modify the test code, you will be prompted that the snapshots do not match. If you are sure you need to make changes, press the u key to update the snapshot. This is useful for testing UI components.

React's BDD unit test

Next, let's take a look at how the react code is tested, using a small example to illustrate.

Enzyme was introduced in the case. _Enzyme_ from airbnb is a JavaScript testing tool for React that allows you to judge, manipulate and traverse React Components output.

我们达成的目的是检测:

  • 用户进入首页,看到两个按钮,分别是counter1和counter2
  • 点击counter1,就能看到两个按钮的文字部分分别是"counter1"和"counter2"

react代码如下

import React from 'react';
function Counter(){
    return (
        <ul>
            <li>
                <button id='counter1' className='button1'>counter1</button>
            </li>
            <li>
                <button id='counter2' className='button2'>counter2</button>
            </li>
        </ul>

    )
}
复制代码

单测的文件:

import Counter from xx;
import { mount } from 'enzyme';

describle('测试APP',() => {
    test('用户进入首页,看到两个按钮,分别是counter1和counter2,并且按钮文字也是counter1和counter2',()=>{
        const wrapper = mount(<Counter />);
        const button = wrapper.find('button');
        except(button).toHaveLength(2);
        except(button.at(0).text()).toBe('counter1');
        except(button.at(1).text()).toBe('counter2');
    })
})
复制代码

Jest | 測試設置分類(describe)及作用域[1]

jest入门单元测试[2]

作者:孟祥_成都

https://juejin.cn/post/7092188990471667749

--- EOF ---
图片前端技术交流群图片
随着读者越来越多,我也建了几个技术交流群,九分聊技术,一分聊风雪,欢迎有兴趣的同学加入我们。
可以长按识别下方二维码,一定要注意:城市+昵称+前端,根据格式,可以更快捷地通过选择内容进入群。

图片

▲长按扫描

—  —

关注公众号后,回复下面关键词获取

图片

回复 面试,获取最新大厂面试资料。
回复 简历,获取 3200 套 简历模板。
回复 TypeScript,获取 TypeScript 精讲课程。
回复 uniapp,获取 uniapp 精讲课程。
回复 Node,获取 Nodejs+koa2 实战教程。
回复 架构师,获取 架构师学习资源教程。
更多教程资源应用尽有,欢迎 关注获取