Mock请求和函数

在单元测试中, 必须排除本函数之外的其它因素对函数自身的影响,换句话说单元测试必须是确定性的,这种情况下就需要用到Mock.

[模拟函数]

参考官方文档: 模拟函数

test('mock function', () => {
    const sum = jest.fn();
    let val = sum(1, 2);
    console.log(val);
});

test('mock function 的返回值', () => {
    const sum = jest.fn().mockReturnValue('8');
    let val = sum(1, 2);
    console.log(val);
});

test('mock function 的模拟实现', () => {
    const sum3 = jest.fn().mockImplementation(
        (a, b) => {
            return a + b;
        }
    );
    let val = sum3(4, 5);
    expect( sum3(4, 5)).toBe(9);
});

这些模拟的函数可以检查是否被调用过, 参考文档:

const mockCallback = jest.fn();
forEach([0, 1], mockCallback);

// 此模拟函数被调用了两次
expect(mockCallback.mock.calls.length).toBe(2);

// 第一次调用函数时的第一个参数是 0
expect(mockCallback.mock.calls[0][0]).toBe(0);

// 第二次调用函数时的第一个参数是 1
expect(mockCallback.mock.calls[1][0]).toBe(1);

[模拟现有module或者不存在的module]

开发中经常会用到第三方库甚至是还没开发完的库, 这时候我们可以对这些库进行模拟. 这时候直接模拟库里的单个函数是不管用的. 参考文档:

http://facebook.github.io/jest/docs/zh-Hans/manual-mocks.html#content

jest.mock(moduleName, factory, options)参考: 官方API文档

moduleName 模块名, 如果是npm定义的, 路径是单字符串, 否则是模块的相对路径, 主要这个相对路径可能和被测的类并不一样. 例如:

__tests__/a-test.js
    jest.mock("../..B");
A.js
import '../B';

这段代码会在请求时自动返回一个mock版, 被mock的模块中的每个函数都会变成mock版. factoryoptions 参数是可选的.

例如: jest.mock('react-native-alert');

factory 第二个参数, 工厂方法, 用来替代默认的Jest mock实现.

jest.mock('../moduleName', () => {
  return jest.fn(() => 42);
});

const moduleName = require('../moduleName'); // This runs the function specified as second argument to `jest.mock`.
moduleName(); // Will return '42';

options 第三个参数, 选项, 可以用来创建虚拟的 mock – 模拟在系统中任何地方都不存在的module, 或者是那种没有开发完成的模块.

jest.mock('../moduleName', () => {
  /*
   * Custom implementation of a module that doesn't exist in JS,
   * like a generated module or a native module in react-native.
   */
}, {virtual: true});

下面是个完整的例子, 注意 virtualApi 压根就没开发:

import virtualApi from 'virtualApi';

jest.mock('virtualApi', () => {
        return {
            sendVerifyVCode:
                () => {
                    return 'abc';
                    // 也可返回async的Promise方法
                    // return new Promise((resolve, reject) => {
                    //     process.nextTick(
                    //         () =>
                    //             resolve(JSON.parse('{"success":true,"code":200,"msg":null,"data":null}'))
                    //     )
                    // });
                }
        }
    }, {virtual: true}
);

it('can fetch', async () => {
    expect(virtualApi.sendVerifyVCode()).toEqual('abc');
});

一般情况下我们的项目使用的是 ES 模块 import语法 , 这时候所有的 import 语句都会写在测试文件的开头. 但是通常需要先mock再使用. 因此, Jest 会自动把所有的 jest.mock 调用放到模块的顶层来执行 (在任何 import语句之前生效).

[模拟定时器]

很多测试都用到了定时器操作, 没有必要完全去等待. 具体参考文档: http://facebook.github.io/jest/docs/zh-Hans/timer-mocks.html#content

function timerGame(callback) {
    console.log('Ready....go!');
    setTimeout(() => {
        console.log('Times up -- stop!');
        callback && callback();
    }, 1000);

    setTimeout(() => {
        console.log('Times up -- stop!');
    }, 2000);
}

test('waits 1 second before ending the game', () => {
    jest.useFakeTimers();
    const callback = jest.fn();

    timerGame(callback);

    expect(setTimeout.mock.calls.length).toBe(2);// 总共调用了两次

    expect(setTimeout.mock.calls[1][1]).toBe(2000);// 第二次调用的参数是2000
    jest.runAllTimers();// 立即执行所有函数
    // Now our callback should have been called!
    expect(callback).toBeCalled();
    expect(callback.mock.calls.length).toBe(1);// callback方法被调用了一次
});

[模拟fetch请求]

使用fetch-mock框架, 主页 文档

参考文章:

Testing JavaScript's Fetch with Jest - Happy Path https://codereviewvideos.com/course/react-redux-and-redux-saga-with-symfony-3/video/testing-javascript-s-fetch-with-jest-happy-path

下面是一个具体的例子:

import fetchMock from 'fetch-mock';

it('can fetch', async () => {
    // Mock请求的语句请写到测试方法里面, 这样不会互相干扰
    // fetchMock.post('*', JSON.parse('{"success":true,"code":200,"msg":null,"data":null, "jest-post": true}'));
    fetchMock.get('http://fake.com', JSON.parse('{"success":true,"code":200,"msg":null,"data":null, "jest": true}'));
    fetchMock.get('http://www.***.cn/api/v1', JSON.parse('{"success":true,"code":200,"msg":null,"data":null, "abc": true}'));
    // fetchMock.get('http://fake.com', {hello: "world"});
    console.log("fetch******=", fetch);
    const response = await fetch('http://fake.com');
    const result = await response.json();
    expect(result.success).toEqual(true);
    let return2 = await fetch('http://www.***.cn/api/v1');
    let text = await return2.text();
    console.log("fetch response ******=", text);
    expect(text).toEqual('{"success":true,"code":200,"msg":null,"data":null,"abc":true}');
});

[实际案例]

[案例: 解决React Native中对本地库的依赖]

修复 Invariant Violation: Native module cannot be null.

jest.mock('NetInfo', () => {
    return {
        isConnected: {
            fetch: () => {
                return new Promise((accept, resolve) => {
                    accept(true);
                })
            },
            addEventListener: jest.fn()
        }
    }
});

[案例: 模拟Fetch返回值]

仅供参考,实际开发中不会这么用 来源: https://www.snip2code.com/Snippet/1682585/Mock-Fetch-with-Jest/

const mockResponse = (status, statusText, response) => {
  return new window.Response(response, {
    status: status,
    statusText: statusText,
    headers: {
      'Content-type': 'application/json'
    }
  });
};

test('fetch something', () => {
  window.fetch = jest.fn().mockImplementation(() =>
    Promise.resolve(mockResponse(200, null, '{"foo":"bar"}')));

  fetch('/foo/bar')
    .then(r => r.json())
    .then(json => expect(json.foo).toEqual('bar'));
});

[案例: 模拟全局对象]

参考: https://stackoverflow.com/questions/40449434/mocking-globals-in-jest

每个测试在它自己的环境中运行, 因此可以 mock 全局global对象, 直接在单个test中覆盖即可. 所有的全局变量都可以通过 global 命名空间访问.

global.navigator = {
  onLine: true
}

[这个覆盖只会影响当前测试而不会和其它测试互相影响. 这种办法也可以用来模拟 Math.randomDate.now.]

你可能感兴趣的:(Mock请求和函数)