Jest前端自动化测试入门

3,739 阅读2分钟

前言

近几年,前端发展速度很快,这也意味着对前端工程化的要求会越来越高,而前端自动化测试作为代码质量保证的一环,也是前端工程化的范畴,目前很多开源框架或库都使用了前端自动化测试,例如Ant DesignElement UI等,所以我们有必要学习一下前端自动化测试。

Jest是facebook推出的一款测试框架,集成了Mocha,chai,jsdom,sinon等功能。所以其强大的功能还是值得我们学习的。

jest的简单配置

jest是运行在node环境下的,不支持es6Es Module,但我们测试代码通常是运行在浏览器的,所以有必要使用Babel进行代码转换,下面对项目进行初始化和jest的简单配置:

运行

npm init

进行项目初始化,并在package.json中配置测试的script命令:

"script": {
    "test": 'jset'
}

再安装jest、@babel/core、@babel/preset-env

npm i jest @babel/core @babel/preset-env -D

安装完成后,运行

npx jest --init

进行jest配置初始化,初始化完成后,就是再目录多出一个jest.config.js配置文件了。

接着创建.babelrc进行babel配置:

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

经过这些配置后,我们就可以在jest的测试文件中使用Es Module了。使用jest进行测试时,我们对模块的测试需要遵循一定的文件命名,命名规则为:

moduleName.test.js

例如需要对button.js文件模块进行测试,我们为此创建的测试文件名为button.test.js

一、前端自动化测试的原理

Jest拥有众多API,可以测试各种开发场景,其核心APIexpect()test(),每个测试用例都离不开这两个核心功能。

test()函数主要用于描述一个测试用例,expect()函数是用于指示我们期望的结果。而expect()test()原理思路很巧妙,并不复杂,下面代码就是两者的原理思路:

function expect (result) {
    return {
        toBe: function (actual) {
            if (result !== actual) {
                throw new Error(`
                预期值与实际值不相等,预期值为${actual},实际值为${result}`);
            }
        }
    }
}

function test (desc, callback) {
    try {
        callback();
        console.info(`${desc} 通过测试`);
    } catch(e) {
        console.info(`${desc} 没有通过测试:${e}`);
    }
}

在这段代码中expact(result) 将返回我们期望的结果,通常情况下我们只需要调用expact(result)就可以,括号中的可以是一个具有返回值的函数,也可以是表达式。后面的toBe 就是一个matcher,当Jest运行的时候它会记录所有失败的matcher的详细信息并且输出给用户,让维护者清楚的知道failed的原因。

二、Jest的API

2.1、 Matchers匹配器

匹配器(Matchers)是Jest中非常重要的一个概念,它可以提供很多种方式来让你去验证你所测试的返回值,匹配器可以通俗理解为相等操作。

2.1.1、相等匹配Matchers

  • toBe()

toBe()是测试expect的期望值是否全等于结果值。toBe()相当于js中的===、Object.is()

test('测试具体值:1 + 2 = 3',() => {
  expect(1 + 2).toBe(3);
});
  • toEqual()

相对于toBe()的完全相等,toEqual()匹配的是内容是否相等,一般用于测试对象或数组的内容相等。

test('测试对象内容相等',() => {
  let a = { a: 1 };
  expect(a).toEqual({ a: 1 });
});

2.1.2、判断匹配Matchers

toBeTruthy()是测试expect的期望值是否为true
toBeFalsy()是测试expect的期望值是否为false
toBeUndefined()是测试expect的期望值是否为undefined
toBeNull()是测试expect的期望值是否为null

test('判断匹配Matchers',() => {
  expect(true).toBeTruthy();
  expect(false).toBeFalsy();
  expect().toBeUndefined();
  expect(null).toBeNull();
});

2.1.3、数字匹配Matchers

toBeGreaterThan()是测试expect的期望值是否大于传入的参数;
toBeLessThan()是测试expect的期望值是否小于传入的参数;
toBeGreaterThanOrEqual()是测试expect的期望值是否大于或等于传入的参数;
toBeLessThanOrEqual()是测试expect的期望值是否否小于或等于传入的参数;

test('判断匹配Matchers',() => {
  expect(2).toBeGreaterThan(1);
  expect(2).toBeLessThan(3);
  expect(2).toBeGreaterThanOrEqual(2);
  expect(2).toBeLessThanOrEqual(2);
});

2.1.4、其他常用匹配Matchers

toMatch()是测试expect的期望值是否符合传入的正则表达式匹配规则;
toContain()是测试expect的期望值是否包含传入的参数;
toThrow()是测试expect的期望值是否会抛出特定的异常信息;

test('其他常用匹配Matchers',() => {
  expect(/2/).toMatch(2);
  expect('Other Matchers').toContain('Matchers');
  expect(() => { throw new Error('error'); }).toThrow('error');
});

jest的匹配器有很多,但是我们不必全部都记住,可以灵活运用各种匹配器达到类似的效果,比如toBe(true)类似于toBeTruthy

三、Jest测试异步代码

我们在开发过程中,难免会进行数据请求等异步操作,Jest也考虑到了这一点,现在我们以异步请求数据为例,来说明如何使用Jest进行异步代码测试:

运行

npm i axios --save

3.1、回调函数异步类型测试

创建fetch.js

//fetch.js
import axios from 'axios';

//假设'https://juejin.im/editor'返回的data数据为{ success: true }
export const fetchData = (callback) => {
    aixos.get('https://juejin.im/editor').then(res => {
        callback(res.data);
    });
}

接着创建fetch.test.js进行测试:

//fetch.test.js
import { fetchData } from './fetchData.js';

test('测试返回的结果为 { success: true }', (done) => {
    fetchData((data) => {
        expect(data).toEqual({ success: true });
        done();
    })
});

3.2、返回Promise异步类型测试

3.2.1、Promise请求成功的测试

创建fetch.js

//fetch.js
import axios from 'axios';

//假设'https://juejin.im/editor'返回的data数据为{ success: true }
export const fetchData = (callback) => {
   return aixos.get('https://juejin.im/editor');
}

接着创建fetch.test.js进行测试:

//fetch.test.js
import { fetchData } from './fetchData.js';

test('测试返回的结果为 { success: true }', () => {
    return fetchData().then(res => {
        const { data } = res;
        expect(data).toEqual({ success: true });
    })
});

3.2.2、Promise请求失败的测试

创建fetch.js

//fetch.js
import axios from 'axios';

//假设'https://juejin.im/editor/xxxx'没有返回数据,为404状态
export const fetchData = (callback) => {
   return aixos.get('https://juejin.im/editor/xxxx');
}

接着创建fetch.test.js进行测试:

//fetch.test.js
import { fetchData } from './fetchData.js';

test('测试返回的结果为 404', () => {
    expect.assertions(1);
    return fetchData().catch(e => {
        expect(e.toString().indexOf('404') > -1).toBe(true);
    })
});

四、Jest的钩子函数

Jest的钩子函数类似于Vue的生命周期函数,会在在代码执行的特定时刻,自动运行一个函数。Jest中有4个核心的钩子函数,分别为beforeAll、beforeEach、afterEach、afterAll,钩子函数均接受回调函数作为参数。

4.1、Jest的钩子函数执行顺序

顾名思义,Jest的4个核心钩子函数执行顺序依次为beforeAllbeforeEachafterEachafterAll

  • beforeAll:该钩子函数会在所有测试用例执行之前执行,通常用于进行初始化。
  • beforeEach:该钩子函数会在每个测试用例执行之前执行。
  • afterEach:该钩子函数会在每个测试用例执行之后执行。
  • afterAll:该钩子函数会在所有测试用例执行之后执行。

下面以hook.test.js为例来说明一下钩子函数的执行顺序:

//hook.test.js

beforeAll(() => {
    console.info('beforeAll 钩子函数执行');
})

beforeEach(() => {
    console.info('beforeEach 钩子函数执行');
})

afterEach(() => {
    console.info('afterEach 钩子函数执行');
})

afterAll(() => {
    console.info('afterAll 钩子函数执行');
})

test('测试Jest中的钩子函数', () => {
    console.info('测试用例执行');
    expect(1).toBe(1);
});

运行

npm run test

我们会发现终端,打印出以下信息:

'beforeAll 钩子函数执行'
'beforeEach 钩子函数执行'
'测试用例执行'
'afterEach 钩子函数执行'
'afterAll 钩子函数执行'

这也恰恰印证了上面说明的钩子函数的执行顺序。

4.2、借助钩子函数,提高代码维护性

合理运用钩子函数,可以使我们的测试代码更加易于维护,下面以Calculator.js为例来说明一下钩子函数的运用:

创建Calculator.js

//Calculator.js
class Calculator {
    constructor() {
        this.number = 0
    }
    add() {
        this.number += 1;
    }
    minus() {
        this.number -= 1;
    }
}

接着创建Calculator.test.js进行测试:

//Calculator.test.js
import Calculator from './Calculator.js';

let calculator = null;

//Jest推荐使用钩子函数进行初始化
beforeAll(() => {
    calculator = new Calculator();
})

test('测试Calculator中的 add 方法', () => {
    calculator.add();
    expect(calculator.number).toBe(1);
});

test('测试Calculator中的 minus 方法', () => {
    //共用了同一个```calculator```实例,与上一个代码耦合
    calculator.minus();
    expect(calculator.number).toBe(0); 
});

运行

npm run test

我们会发现测试用例是顺利通过的,不过Calculator.test.js中的代码写法有一个问题:在每个测试用例中都共用了同一个calculator实例,导致每个测试函数互相耦合,不利于维护,为了解决这个问题我们可以借助JestbeforeEach钩子函数进行解耦。

修改Calculator.test.js

//Calculator.test.js
import Calculator from './Calculator.js';

let calculator = null;

beforeEach(() => {
    calculator = new Calculator();
})

test('测试Calculator中的 add 方法', () => {
    calculator.add();
    expect(calculator.number).toBe(1);
});

test('测试Calculator中的 minus 方法', () => {
    calculator.minus();
    expect(calculator.number).toBe(-1);
});

运行

npm run test

我们会发现测试用例是顺利通过的,通过beforeEach钩子函数,我们在每个测试用例执行之前,重新创建了calculator实例,这样每个测试用例之间就没有互相耦合,代码会更加容易维护。

五、借助describe进行测试用例分组管理

我们在开发过程中都会遇到功能繁多的复杂模块,为此我们需要写大量测试用例来测试这些模块,但如果单纯为模块的每个功能写测试用例而不加以分类的话,Jest测试文件将十分混乱而难以维护,因此我们需要对模块功能进行分类,在借助Jest的describe进行分组管理。

以上面的Calculator.js以及Calculator.test.js为例,来说明如何使用describe进行分组管理:

修改Calculator.js

//Calculator.js
class Calculator {
    constructor() {
        this.number = 0
    }
    add() {
        this.number += 1;
    }
    minus() {
        this.number -= 1;
    }
    multiply() {
        this.number *= 2;
    }
    divide() {
        this.number /= 10;
    }
}

修改Calculator.test.js

//Calculator.test.js
import Calculator from './Calculator.js';

describe('测试Calculator模块所有功能',() => {
    let calculator = null;

    beforeEach(() => {
        calculator = new Calculator();
    });
    
    describe('测试Calculator中增加数量相关的方法',() => {
        test('测试Calculator中的 add 方法', () => {
            calculator.add();
            expect(calculator.number).toBe(1);
        });
        test('测试Calculator中的 multiply 方法', () => {
            calculator.multiply();
            expect(calculator.number).toBe(0);
        });
    });
    
    describe('测试Calculator中减少数量相关的方法',() => {
        test('测试Calculator中的 minus 方法', () => {
            calculator.minus();
            expect(calculator.number).toBe(-1);
        });
        test('测试Calculator中的 divide 方法', () => {
            calculator.divide();
            expect(calculator.number).toBe(0);
        });
    });
})

运行

npm run test

我们会发现终端,打印出以下层次分明、可读性良好的测试信息:

'测试Calculator模块所有功能'
    '测试Calculator中增加数量相关的方法''测试Calculator中的 add 方法''测试Calculator中的 multiply 方法'
    '测试Calculator中减少数量相关的方法''测试Calculator中的 minus 方法''测试Calculator中的 divide 方法'

我们可以看到,对模块功能进行分类,以及借助Jest的describe进行分组管理,我们测试代码的可维护性以及测试运行结果信息可读性都有显著的提高了。

六、describe中的钩子函数执行规则

每个describe的回调函数都有各自的作用域,都可以使用Jest的4个核心钩子函数,而describe中的钩子函数都可以作用于它回调函数中的所有测试用例。

面对像上面Calculator.test.js文件中,describe回调函数嵌套describe的场景,每个describe中的钩子函数执行顺序会有点特别。

以下对Calculator.test.js进行修改,来说明describe回调函数嵌套describe场景的钩子函数执行顺序:

修改Calculator.test.js

//Calculator.test.js
import Calculator from './Calculator.js';

describe('测试Calculator模块所有功能',() => {
    let calculator = null;
    
    beforeAll(() => {
        console.info('beforeAll: 父级 beforeAll 执行');
    });
    
    beforeEach(() => {
        calculator = new Calculator();
        console.info('beforeEach: 父级 beforeEach 执行');
    });
    
    afterEach(() => {
        console.info('afterEach: 父级 afterEach 执行');
    });
    
    describe('测试Calculator中增加数量相关的方法',() => {
        beforeAll(() => {
            console.info('beforeAll: 第一个子级 beforeAll 执行');
        });
        
        test('测试Calculator中的 add 方法', () => {
            console.info('测试Calculator中的 add 方法');
            calculator.add();
            expect(calculator.number).toBe(1);
        });
    });
    
    describe('测试Calculator中减少数量相关的方法',() => {
        beforeEach(() => {
            console.info('beforeEach: 第二个子级 beforeEach 执行');
        });
        
        test('测试Calculator中的 minus 方法', () => {
            console.info('测试Calculator中的 minus 方法');
            calculator.minus();
            expect(calculator.number).toBe(-1);
        });
    });
})

运行

npm run test

我们会发现终端,打印出以下信息:

'beforeAll: 父级 beforeAll 执行'
'beforeAll: 第一个子级 beforeAll 执行'
'beforeEach: 父级 beforeEach 执行'
'测试Calculator中的 add 方法'
'afterEach: 父级 afterEach 执行'
'beforeEach: 父级 beforeEach 执行'
'beforeEach: 第二个子级 beforeEach 执行'
'测试Calculator中的 minus 方法'
'afterEach: 父级 afterEach 执行'

从中,我们可以发现在describe回调函数嵌套describe的场景下,describe中的钩子函数都可以作用于它回调函数中的所有测试用例,并且每个测试用例在describe作用域中运行时,相同类型钩子函数的执行顺序是先从父级到子级从外部到内部的。

七、Jest中的Mock

从测试的角度来说我只关心测试的方法的内部逻辑,并不关注与当前方法本身依赖的实现,所以,我们在测试某些依赖了外部的一些接口的实现的方法时,通常会进行Mock实现依赖接口的返回,只测试方法的内部逻辑而规避外部的依赖,基于这个思想,jest中提供了强大的Mock功能,方便开发者进行mock操作。

7.1、使用jest.fn()函数,捕获函数的调用

开发过程中,一个功能函数传入回调函数作为参数的场景很常见,如果想测试这类功能函数,我们就需要借助Mock函数,捕获函数的调用。

callback.js以及callback.test.js为例,来说明Mock函数:

创建callback.js

//callback.js
export const testCallback = (callback) => {
    callback();
}

创建callback.test.js

//callback.test.js
import { testCallback } from './callback.js';

test('测试 testCallback 方法', () => {
    const fn = jest.fn();
    testCallback(fn);
    expect(fn).toBeCalled();
})

运行

npm run test

我们会发现测试用例顺利通过,上面代码通过jest.fn()mock出一个fn函数作为testCallback的回调函数,在使用toBeCalled来捕获fn的调用情况来验证测试结果。值得注意的是,只有jest.fn()mock出的函数才可以被toBeCalled捕获。

7.2、Mock函数的.mock属性

jest.fn()mock出的函数会有一个.mock属性,借助.mock属性我们可以多种方式去测试功能模块。

以上面的callback.js以及callback.test.js为例,来说明Mock函数:

修改callback.test.js

//callback.test.js
import { testCallback } from './callback.js';

test('测试 testCallback 方法', () => {
    const fn = jest.fn();
    testCallback(fn);
    testCallback(fn);
    console.info(fn.mock);
    expect(fn).toBeCalled();
})

运行后,我们会发现终端,打印出以下.mock属性信息:

{
    calls: [ [], [] ],
    instances: [ undefined, undefined ],
    invocationCallOrder: [ 1, 2 ],
    results: [
        { type: 'return', value: undefined },
        { type: 'return', value: undefined }
    ]
}

7.2.1、mock对象的calls属性

fn.mock中的calls属性是一个二维数组,二维数组中的数组项表示传入jest.fn()mock出的函数的实参,类似于arguments属性。

从上面callback.test.js可以看到,fn是由jest.fn()mock出的函数,fn传入testCallback中被调用了两次并且fn并没有接受参数,所以,calls二维数组length为2,有2个空数组项。

我们可以修改callback.js,给callback传入参数,看一下此时calls的变化:

//callback.js
export const testCallback = (callback) => {
    callback(1, 2, 3);
}

运行callback.test.js后,我们会发现终端,打印出以下.mock属性信息:

{
    calls: [ [1, 2, 3], [1, 2, 3] ],
    instances: [ undefined, undefined ],
    invocationCallOrder: [ 1, 2 ],
    results: [
        { type: 'return', value: undefined },
        { type: 'return', value: undefined }
    ]
}

我们可以发现calls变为了[ [1, 2, 3], [1, 2, 3] ],也就是说calls的数组项的确是表示传入jest.fn()mock出的函数的实参。

7.2.2、mock对象的invocationCallOrder属性

fn.mock中的invocationCallOrder属性是一个数组,数组指示jest.fn()mock出的函数执行的顺序。

从上面callback.test.js可以看到,fn是由jest.fn()mock出的函数,fn传入testCallback中被调用了两次,所以,invocationCallOrder数组的length为2。

7.2.3、mock对象的results属性

fn.mock中的results属性是一个数组,数组中的对象指示jest.fn()mock出的函数每次执行的返回值,返回值由value属性表示。

从上面callback.test.js可以看到,fn是由jest.fn()mock出的函数,fn传入testCallback中被调用了两次并且fn并没有返回值,所以,results数组的length为2,有2个对象,对象中的value属性均为undefined

我们可以修改callback.test.js,使fn有返回值,看一下此时results的变化:

//callback.test.js
import { testCallback } from './callback.js';

test('测试 testCallback 方法', () => {
    const fn = jest.fn(() => {
        return 123;
    });
    testCallback(fn);
    testCallback(fn);
    console.info(fn.mock);
    expect(fn).toBeCalled();
})

运行callback.test.js后,我们会发现终端,打印出以下.mock属性信息:

{
    calls: [ [1, 2, 3], [1, 2, 3] ],
    instances: [ undefined, undefined ],
    invocationCallOrder: [ 1, 2 ],
    results: [
        { type: 'return', value: 123 },
        { type: 'return', value: 123 }
    ]
}

我们可以发现results中的对象的value变为了fn的返回值123,也就是说results数组中的对象的确指示jest.fn()mock出的函数每次执行的返回值,返回值由value属性表示。

7.2.4、mock对象的instances属性

fn.mock中的instances属性是一个数组,数组指示jest.fn()mock函数的this指向。当mock函数当作普通函数调用时,this指向undefined;当mock函数当作构造函数被new实例化时,this指向mockConstructor{}

从上面callback.test.js可以看到,fn是由jest.fn()mock出的函数,fn作为回调函数传入testCallback中被调用了两次,所以,fnthis指向undefinedinstances数组的length为2,数组项均为undefined

我们可以修改callback.js,使fn以构造函数形式被调用,看一下此时instances的变化。

修改callback.js

//callback.js
export const testCallback = (callback) => {
    callback();
};

export const testInstances = (callback) => {
    new callback();
}

再修改callback.test.js

//callback.test.js
import { testCallback, testInstances } from './callback.js';

test('测试 testInstances 方法', () => {
    const fn = jest.fn();
    testInstances(fn);
    testInstances(fn);
    console.info(fn.mock);
    expect(fn).toBeCalled();
})

运行callback.test.js后,我们会发现终端,打印出以下.mock属性信息:

{
    calls: [ [], [] ],
    instances: [ mockConstructor{}, mockConstructor{} ],
    invocationCallOrder: [ 1, 2 ],
    results: [
        { type: 'return', value: undefined },
        { type: 'return', value: undefined }
    ]
}

我们可以发现instances中变为了[ mockConstructor{}, mockConstructor{} ],也就是说instances的确指示了jest.fn()mock函数的this指向。当mock函数当作普通函数调用时,this指向undefined;当mock函数当作构造函数被new实例化时,this指向mockConstructor{}

7.3、使用Mock函数,改变内部函数的实现

在测试功能模块时,有时候我们想省略不想功能模块的某一执行步骤,不完全按照功能模块的代码逻辑执行,如果我们去改变功能模块的源代码,那是不可取的,为此,我们可以借助Mock函数,改变内部函数的实现。

假设现在我们

创建mockFetch.js:

//mockFetch.js
import axios from 'axios';

export const fetchData = () => {
    return axios.get('https://juejin.im/editor').then(res => res.data);
}

创建mockFetch.test.js:

//mockFetch.test.js
import axios from 'axios';
import { fetchData } from './mockFetch.js';

jest.mock(axios);

test('测试 Mock 函数,改变内部函数的实现', () => {
    axios.get.mockResolvedValue({ data: { success: true } });
    return fetchData().then(data => {
        expect(data).toEqual({ success: true });
    })
});

运行

npm run test

我们会发现测试用例顺利通过了。在上面代码中,我们使用jest.mock()axios进行包装处理并且使用mockResolvedValue定义了axios.get请求的数据为{ data: { success: true } },这样就使得fetchData不会真实地异步请求'https://juejin.im/editor'接口,而是同步地以{ data: { success: true }为数据请求结果。

借助jest.mock()mockResolvedValue,我们就可以改变axios模块本身的函数实现,使得该模块不会完全按照自身代码逻辑来执行。

7.4、创建__mocks__文件夹,改变内部函数的实现

除了像7.3小节那样使用jest.mock函数,来改变内部函数的实现外,我们还可以借助创建__mocks__文件,改变内部函数的实现。

假设现在我们继续以mockFetch.js为例,对7.3小节的文件进行修改:

mockFetch.js同级目录下创建__mocks__文件夹,在该文件夹下创建mockFetch.js,如下:

//__mocks__文件夹中的mockFetch.js

export const fetchData = () => {
   console.log('__mocks__文件夹中的mockFetch.js 的fetchData执行');
   const data = { success: true };
   return Promise.resolve(data);
}

修改mockFetch.js:

//mockFetch.js
import axios from 'axios';

export const fetchData = () => {
   console.log('mockFetch.js 的fetchData执行');
   return axios.get('https://juejin.im/editor').then(res => res.data);
}

修改mockFetch.test.js:

//mockFetch.test.js
jest.mock('./mockFetch.js');
import { fetchData } from './mockFetch.js';

test('测试创建__mocks__文件夹,改变内部函数的实现', () => {
    return fetchData().then(data => {
        expect(data).toEqual({ success: true });
    })
});

运行

npm run test

我们会发现测试用例顺利通过了,查看控制台可以看到以下的信息:

'__mocks__文件夹中的mockFetch.js 的fetchData执行'

这样就说明了,测试用例执行的是__mocks__文件夹中的mockFetch.jsfetchData,之所以这样,是因为在上面代码中,我们使用jest.mock('./mockFetch.js')./mockFetch.js进行mock处理,使得import { fetchData } from './mockFetch.js';中的./mockFetch.js为我们创建的__mocks__文件夹中的mockFetch.js,通过这样就借助创建__mocks__文件,改变文件的引用来改变内部函数的实现了。

八、Snapshot快照测试

我们在开发组件的过程中,往往需要为组件创建一份默认Props配置,在组件升级迭代时,我们有可能会增加或修改默认Props的配置,这样导致我们可能会修改错误某些配置而我们没有感知到,造成修改引入bug,为了避免这种情况,我们可以借助Snapshot生成文件快照历史记录,以便在每次修改时进行修改提示,使开发者感知修改。

创建generateProps.js进行Snapshot说明:

//generateProps.js
export const generateProps = () => {
    return {
        name: 'jest',
        time: '2020',
        onChange: () => {}
    }
}

创建generateProps.test.js:

//generateProps.test.js
import { generateProps } from './generateProps.js'

test('Snapshot快照测试', () => {
    expect(generateProps()).toMatchSnapshot();
})

首次运行

npm run test

我们可以发现测试用例顺利通过,并且会在当前文件目录中新增一个_snapshot_文件夹,_snapshot_文件夹就是generateProps()返回值的一份快照,记录了当次generateProps()执行的结果,以便作为下次变更的参照。

修改generateProps.js:

//generateProps.js
export const generateProps = () => {
    return {
        name: 'jest',
        time: '2020',
        desc: '测试',
        onChange: () => {}
    }
}

运行

npm run test

我们会发现终端控制台报错:

1 snapshot failed

这是因为修改后的文件内容与快照不匹配,如果我们确定需要更新修改,那么我们就要在控制台终端进入Jest命令行模式,输入u来确定更新快照。

九、测试Timer定时器

开发过程中的setTimeoutsetInterval()等定时器都是异步的,如果想测试它们,我们可以参照 三、Jest测试异步代码

不过如果定时器设置的时间过于大的场景下,需要我们去等待定时器的触发才可以知道测试结果的话,这明显是不合理的,我们没必要浪费时间去等待,Jest也清楚这一点,所以,我们可以借助jest.useFakeTimers()以及jest.advanceTimersByTime()立即触发定时器,提高开发效率。

下面创建timer.js,来说明如何测试定时器,:

// timer.js
export const timer = (callback) => {
    setTimeout(() => {
        callback()
    }, 2000) ;
}

创建timer.test.js:

// timer.test.js
import { timer } from './timer.js';

jest.useFakeTimers();

test('测试定时器', () => {
    const fn = jest.fn();
    timer(fn);
    jest.advanceTimersByTime(2000);
    expect(fn).toHaveBeenCalledTimes(1);
});

运行

npm run test

我们可以发现测试用例不用等待定时器设置的时间,就顺利通过了。在这里,我们使用jest.useFakeTimers()来启动假的定时器,然后借助jest.advanceTimersByTime()来快进2000秒,所以相当于定时器已经成功触发1次了,符合测试期望值。

十、对DOM节点的测试

Jest是运行在node的环境的,理论上node并没有DOM这个概念,Jest为了方便开发者可以测试DOM节点操作,Jest自己在node的环境下模拟了一套API,我们可以称它为JSDOM。借助JSDOM特性,我们也可以在使用Jest来测试DOM节点。

下面以dom.js以及jQuery为例,来说明如何使用Jest来测试DOM节点:

运行

npm i jquery

创建dom.js

//dom.js
import $ from 'jquery';

export const createDiv = () => {
    $('body').append('<div/>')
}

创建dom.test.js

//dom.test.js
import { createDiv }from './dom.js';

test(' 测试 DOM 节点 ', () => {
    createDiv();
    let length = $('body').find('div').length;
    expect(length).toBe(1);
});

运行

npm run test

我们可以发现测试用例顺利通过,也就是说,Jest支持测试DOM节点。