jest对象&异步&定时器

以下是本文章内容:

  • jest对象
    • jest.requireActual()和jest.mock()
    • jest.mocked(source,{shallow:true})
    • doMock(moduleName,factiory,options)
    • jest.spyOn()
    • jest.isMockFunction(fn)
    • jest.isMockFunction(fn)
  • 模拟异步
    • async/await
    • assertions/hasAssertions
  • 定时器
    • 启用假计时

jest对象

函数讲完了,‘生命周期’也说了,现在在聊聊jest这个对象。官网并没有单独对jest对象做讲解,都是用到再说,因此,我总结了一下我日常生活中用到的,大家可以当作积累来对待,甚至可以跳过这一节内容。


jest指南:https://www.jestjs.cn/docs/setup-teardown

jest对象自动位于每个测试文件的范围内。对象中的方法jest有助于创建模拟并让您控制 Jest 的整体行为。它也可以通过 via 显式导入import {jest} from '@jest/globals'。

jest.requireActual()和jest.mock()

jest.requireActural()返回实际模块而不是模拟,绕过所有关于模块是否应该接收模拟实现的检查。常用于获取模块本身,与jest.mock一起使用

而jest.mock()模拟具有自动模拟版本的模块,对于下面的test文件,如果jest.mock()不传第二参数(箭头函数),那么对应路径下的所有文件将被处于模拟的mock状态,即他们函数的本身行为将被抹除,因此当我们这样进行mock的时候jest.mock('../utils/mock/jestFn'),jestFn里面的所有函数行为变成一个假的mock,即执行函数返回undefined,但是却没有mock的那些值(如calls,result等。

但是,当我们将jest.requireActual()所返回的源模块充当jest.mock()的第二个回调参数的返回值时,此时jest.mock()所mock的各个函数又变回了初始磨样,即执行jestFn里面的方法返回值不为undefined

温馨提示:我们的项目都是基于第一节搭建的,如果说从第一节开始,顺着来的

我们新建文件夹utils/mock/jestFn.ts

export default {
    authorize: () => {
      return 'token';
    },
    isAuthorized: (secret: string) => secret === 'wizard',
};

新建测试_test/testJest.test.tsx

jest.mock('../utils/mock/jestFn',() => {
    const originModule = jest.requireActual<typeof import('../utils/mock/jestFn')>('../utils/mock/jestFn');
    return {
        __esModule: true,
        ...originModule
    }
});
import utils from '../utils/mock/jestFn';
describe('test jest something',() => {
    it('test jest object',() => {
        const res = utils.authorize();
        console.log('ll',res);
    })
})

再继续讲一点:

jestFn.ts函数是直接默认导出,因此我们模拟mock重新函数的时候,函数应该放在default中,如果jestFn我们再新加一个foo导出,那么模拟的时候便可以直接写

jestFn.ts

export const foo = () => 'foo';

testJest.test.tsx

jest.mock('../utils/mock/jestFn',() => {
    ...
    return {
        ...
        default: {
            authorize: jest.fn(() => 'jack'),
            isAuthorized: jest.fn((secret: string) => secret === 'mike')
        },
        foo: () => 'mock foo'
    }
});

有人可能会问,我为啥要这么做,直接导入不行么?

两个原因:其一是为了模拟函数的返回值,或部分实现。例如我们模拟axios,jest不会真的去发送网络请求,那么我们如果拿到axios返回的数据并填充到页面当中呢,此时模拟函数的返回值就很有必要了,这样我们在页面渲染的时候,只需要判断这个函数执行即可。其二是为了测试覆盖率,虽然我们直接将函数导入进来模拟也行,但是这样就无法满足第一点,那么,而只是将函数导入进来,又不使用,那么是不会覆盖到这个jestFn文件的,因此jest.mock()也能提高覆盖率。

jest.mocked(source,{shallow:true})

jest.mocked()模拟函数的类型定义和包装对象的类型及其深层嵌套成员,如果不需要深层次定义,那么shallow:false。翻译成人话就是给让一个函数处于mock状态,这样我们就能去设置他的mockReturnValue了,并且无视他的嵌套。

新建文件utils/mock/jestFn.ts

export const foo = (val: string) => val + 'foo';
export const song = {
    one: {
        more: {
          time: (t: number) => {
            return t;
          },
        },
    },
}

export default {
    authorize: () => {
      return 'token';
    },
    isAuthorized: (secret: string) => secret === 'wizard',
};

新建文件_test/testJestMock.test.tsx

import { song } from "../utils/mock/jestFn";

// 注意点一
jest.mock('../utils/mock/jestFn');
// 移除console
jest.spyOn(console, 'log').mockReturnValue();
// 注意点二
const mockedSong = jest.mocked<any>(song);

describe('test jest object', () => {
    it('tes jest mocked and Mocked',() => {
        console.log('---')
        mockedSong.one.more.time.mockReturnValue(12);

        expect(mockedSong.one.more.time(10)).toBe(12);
        expect(mockedSong.one.more.time.mock.calls).toHaveLength(1);
    })
})

这里有几个注意点:首先我们在mocked函数的时候,得先把我们需要把对应的函数路径mock一次;其次由于是ts文件,mocked的时候必须要带上一个any属性,否则会提示我们找不到mockReturnValue属性。

那既然是ts,有没有ts专门用的呢,也有,那就是jest.MockedClass或jest.MockedFunction。jest.MockedObject,这里我是用的是第二个

新建utils/mock/type.ts

export interface LoadDD {
    username: string,
    age: number
}

在utils/mock/jestFn.ts新增方法

import type { LoadDD } from "./type";
export const loadData = (): LoadDD => {
  return {
    username: 'jack',
    age: 23
  }
}

在testJestMock.test.tsx中新增测试

import { song,loadData } from "../utils/mock/jestFn";

jest.mock('../utils/mock/jestFn');
const mock_loadData = loadData as jest.MockedFunction<typeof loadData>

it('test jest MockFunction',() => {

        mock_loadData.mockReturnValue({
            username: 'jack',
            age: 23
        })
        const res = mock_loadData();
        console.log(res,'load data')
    })

使用这个方法,我们似乎可以更好的使用类型定义,并且避免了any大法。也许你们会说,不还有一个Mocked么,确实,但是我暂时没用到,而且由于我需要使用类型定义,那么这个方法反而不容易用到了,如需了解,可以访问官方:
https://www.jestjs.cn/docs/mock-function-api

doMock(moduleName,factiory,options)

当你想在同一个文件中以不同的方式模拟一个模块时,他就很有用,即多次mock,但是多次mock需要注意:我们需要在每次mock测试完之后,重置模块注册表 。我这里只介绍es6的import导入,如果是require可以参考官网(前端多用import):
https://www.jestjs.cn/docs/jest-object#jestunmockmodulename

我们新建测试_test/testDo.test.tsx

import { loadData } from "../utils/mock/jestFn";

// 移除console
jest.spyOn(console, 'log').mockReturnValue();

describe('test jest object', () => {
    beforeEach(() => {
        // 必须重置模块,否则无法再次应用 doMock 的内容
        jest.resetModules();
    });
    it('test do mock 1',() => {
        jest.doMock('../utils/mock/jestFn', () => {
            return {
              __esModule: true,
              default: 'default1',
              loadData: () => {
                return {
                    username: 'jack123',
                    age: 23
                }
              },
            };
        });
        return import('../utils/mock/jestFn').then(moduleName => {
            expect(moduleName.default).toBe('default1');
            expect(moduleName.loadData()).toEqual({
                username: 'jack123',
                age: 23
            });
        });
    })

    it('test do mock 2',() => {
        jest.doMock('../utils/mock/jestFn', () => {
            return {
              __esModule: true,
              default: 'default1',
              loadData: () => {
                return {
                    username: 'jack234',
                    age: 23
                }
              },
            };
        });
        return import('../utils/mock/jestFn').then(moduleName => {
            expect(moduleName.default).toBe('default1');
            expect(moduleName.loadData()).toEqual({
                username: 'jack234',
                age: 23
            });
        });
    })
})

直接跑就完事,写则是参照来写,当然,这基本上也是官网的写法,我只是加上了理解与示例。

jest.spyOn()

创建一个类似于jest.fn但也跟踪调用的模拟函数object[methodName]。返回 Jest模拟函数,可以模拟一个文件中的某个函数,类似部分模拟,同doMock类似,如果想实现多次模拟mock,那么我们也可以使用spyOn,并且官方也推荐我们使用,毕竟简洁多了.

我们新建一个测试_test/testSpy.test.tsx

import * as JestFn from "../utils/mock/jestFn"

describe('test jest object2', () => {
    it('test jest spyOn',() => {
        // loadData为JestFn的一个属性
        jest.spyOn(JestFn,'loadData').mockReturnValue({
            username: 'jack123',
            age: 23
        })
        expect(JestFn.loadData()).toEqual({
            username: 'jack123',
            age: 23
        })
    })
    it('test jest spyOn2',() => {
        // loadData为JestFn的一个属性
        jest.spyOn(JestFn,'loadData').mockReturnValue({
            username: 'jack1234',
            age: 23
        })
        expect(JestFn.loadData()).toEqual({
            username: 'jack1234',
            age: 23
        })
    })
})

如果需要模拟函数重新实现,则使用mockImplementation,并且我们也结合了前面的jestMockedFunction

我们给testSpy.test.tsx加一个测试

import * as JestFn from "../utils/mock/jestFn";

const mock_foo = jest.spyOn(JestFn,'foo') as jest.MockedFunction<typeof JestFn.foo>
mock_foo.mockImplementation(
    (val: string) => val + '_foo'
)

describe('test jest object2', () => {
    it('test jest spyOn implementation',() => {
        const res = mock_foo('zl');
        console.log(res,'mock implementation')
    })
   ...
})

jest.isMockFunction(fn)

确定给定函数是否为模拟函数,感觉没啥用,根据我的测试,只有使用的函数是jest.fn()才返回true。加上它是因为我看到别人用过了。。。

jest.spied

也用处不大,并且官方也建议,如果需要模拟函数的重新实现,可以使用mockImplementation,理由也是同上。

jest对象中还包含了计时器,但是在说计时器之前,我们先聊一聊异步的模拟,毕竟计时器就可以说是异步了。

模拟异步

异步运行代码在 JavaScript 中很常见。当您有异步运行的代码时,Jest 需要知道它正在测试的代码何时完成,然后才能继续进行另一个测试。Jest 有几种方法来处理这个问题,都是一个个真实的实例,需要大家亲自验证!

模拟async/await

我们新建文件utils/async/index.ts

export const getDD = (count: number) => {
    return new Promise((resolve,reject) => {
        if (count > 20) {
            reject('count is too large')
        } else {
            resolve('count is right')
        }
    })
}

然后新建测试文件_test/jest/testAsync.test/tsx

import { getDD } from "utils/async";

describe('test async',() => {
    it('test await/async', async () => {
        const res2 = await getDD(10);
        expect(res2).toEqual('count is right');
        try {
            await getDD(10);
        } catch (e) {
            expect(e).toBe('count is too large');
        }
    })
})

注:这里我把jest对象相关的测试放置到一个文件夹了


这个是我此时测试文件的层级展示

这样我们就模拟了一个异步了。

我们也可以使用异步的语法糖resolves/rejects去判断,语法糖可以用一个await或者return,如下:

describe('test async',() => {
    ...
    it('test syntastic sugar',async () => {
        await expect(getDD(10)).resolves.toBe('count is right');
        // return expect(getDD(10)).resolves.toBe('count is right');
        await expect(getDD(30)).rejects.toBe('count is too large');
        // return expect(getDD(30)).rejects.toBe('count is too large');
    })
})

assertions/hasAssertions

验证在测试期间调用了一定数量的断言。这在测试异步代码时通常很有用,以确保回调中的断言确实被调用。说人话就是提前预定有几个异步,比如我上面只有一个异步,那么就可以这样写;而hasAssertions就是判断是否有异步代码,额不就等同于assertions(>1)。我认为这个有用,在于必须提前知道异步的数量,那么如何知道呢?一个是项目是你自己写的,自然可以数出来有多少个,另一个是直接写一个很大的值assertions(1000),这样肯定报错,然后报错就会告诉你有几个异步了。。。

it('test await/async', async () => {
    expect.hasAssertions();
    expect.assertions(1);
    const res2 = await getDD(10);
    expect(res2).toEqual('count is right');
    try {
      await getDD(10);
    } catch (e) {
      expect(e).toBe('count is too large');
    }
})

定时器

当我们了解了异步的这个概念之后,现在我们可以开始说定时器。定时器模拟是jest对象介绍的扩展,因为接口也是jest的,但是我这里单独拿出来做讲解,并且在有些时候,定时器模拟会给你带来覆盖率的提升。

启用假计时

新建文件utils/fake/timeGame.ts

type CallBack = () => void

export const timerGame = (callback: CallBack) => {
    console.log('Ready....go!');
    setTimeout(() => {
      console.log("Time's up -- stop!");
      callback && callback();
    }, 1000);
}

新建测试_test/fake/timeGame.test.tsx

import { timerGame } from "utils/fake/timeGame";

jest.useFakeTimers();
jest.spyOn(global, 'setTimeout');

describe('test fake time',() => {
    it('test start fake',() => {
        const fn = jest.fn()
        timerGame(fn);
        expect(setTimeout).toHaveBeenCalledTimes(1);
        expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function),1000);
        // 打印:Ready....go!
    })
})

测试我们只会打印:** Ready....go!**

然后我们运行计时器:jest.runAllTimes(),将执行所有挂起的宏任务和微任务

测试代码调整:

...
describe('test fake time',() => {
    it('test start fake',() => {
        ...
        expect(fn).not.toBeCalled();
        jest.runAllTimers();
        expect(fn).toBeCalled();
        expect(fn).toBeCalledTimes(1);
        // 此时先后打印:
        // Ready....go!
        // Time's up -- stop!
    })
})

以上便是定时器的使用,附带两个官网地址:

定时器:
https://www.jestjs.cn/docs/timer-mocks

api: https://www.jestjs.cn/docs/jest-object#jestadvancetimerstonexttimersteps

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 通过前面的介绍,我们基本上对jest有了一个初步的了解,并搭建了环境开始上手测试,接下来就开始说函数。本节内容主要...
    溪风_耶罗阅读 7,051评论 1 1
  • 翻译自:https://jestjs.io/docs/zh-Hans/configuration最新更新:2020...
    秋名山车神12138阅读 21,933评论 0 7
  • 这篇文章你可以学到以下知识: jest入门开始从sum开始的test配置别名(important)测试对象 jes...
    溪风_耶罗阅读 1,580评论 0 1
  • 前言 单元测试是一种用于测试“单元”的软件测试方法,其中“单元”的意思是指软件中各个独立的组件或模块。开发者需要为...
    袋鼠云数栈前端阅读 2,890评论 0 1
  • 对于一个完整的前端工程,单元测试是不可缺少的一部分。但我们之所以很少使用单元测试,是对单元测试的认知不够,所以接下...
    Fiorile阅读 4,871评论 0 1

友情链接更多精彩内容