手撕Jest弄清原理

单元测试离不开Jest,但是Jest的实现方式你知道吗?尤其jest.fn(), jest.mock(), jest.spyOn()到底都是什么鬼?

一: 首先看看jest.fn()是什么

image.png

图片展示得出几个结论

  • jest是个Object或者Class Instance,他有一个方法/函数fn
  • 这个fn函数接受一个函数
  • 经过fn处理后,返回的是一个新的函数,即 [Function: mockConstructor]
  • 返回的新函数,莫名其妙的增加了新的方法,比如mock这个getter,还有mockImplementation
  • 因此这个fn是一个工厂函数,接受的参数是个原始函数,返回一个模拟函数

因此有三个参与者

  • 工厂函数 Factory Func
  • 原始函数 Original Func
  • 模拟函数 Mocked Func

二: 继续看看返回的mock的getter是什么

image.png

  • 可见这个mock是一个存储instances【this实例】和results【运行结果】的东西,暂且不表。

三: Jest的文档展示mock这个plain object的三个key分别是

  • Calls: 存储每次调用被动函数的参数
  • Instances:存储每次调用原始函数(副本)的this对象
  • Results:存储每次调用被动函数的结果

调用被动函数产生的结果只有三种

  • 返回结果value
  • 没有返回任何value,而是undefined
  • 抛出错误Error

四: Expect是什么鬼?其实就是展示mock这个对象的语法糖

  • expect(fn).toBeCalled()
  • expect(fn).toBeCalledTimes(n)
  • expect(fn).toBeCalledWith(arg1, arg2, ...)
  • expect(fn).lastCalledWith(arg1, arg2, ...)

为了方便查看mock对象的三个key的结果,Jest提供了上面的几个语法糖。比如.toBeCalledTime()就是Results.length

5: 明白了jest原理,我们来手写一个工厂Jest函数吧!

先简单做个工厂函数,即,套子。注意是:复制一个原始函数。

// object literal 
const jest={ }

function fn(impl= ()=>{} ) {

  const mockFn = function(...args) {
    return impl(...args); 
   };
  
  return mockFn;
}

jest.fn=fn;

复制代码

解读:这个套子等于什么都没有做,只是原封不动复制了原始函数。

测试:

image.png

然后进一步,利用闭包closure记录一下mock的calls数组

 

function fn(impl= ()=>{} ) {

  const mockFn = function(...args) {
    mockFn.mock.calls.push(args);
    return impl(...args); 
   };
   
  // 这里是mock对象对应的calls
  mockFn.mock = {
    calls: []
  };
  
  return mockFn;
}


复制代码

测试:

image.png

解读:

  • 工厂函数利用闭包原理,包原始函数包裹,并且隔离起来,生成一个新的函数,同时附加了监控数组。
  • 注意⚠️:模拟函数跟原始函数不是一个函数。证明如下图。
  • 且,只有在调用模拟函数的时候才能被监控,调用原始函数没有意义,因为二者指向不同的内存地址,之所以调用模拟函数,是因为mock的意义就是这样。

image.png

最后完整实现


 function fn(impl = () => {}) {
  
   const mockFn = function(...args) {
     mockFn.mock.calls.push(args);
     mockFn.mock.instances.push(this);
     try {
      const value = impl.apply(this, args);  
        mockFn.mock.results.push({ type: 'return', value });
        return value;  
     } catch (value) {
       mockFn.mock.results.push({ type: 'throw', value });
       throw value;  
    }
   }
 
    mockFn.mock = { calls: [], instances: [], results: [] };
    return mockFn;
 }
复制代码

当然,真实的jest的mock函数比这个复杂,见 jestjs.io/docs/mock-f… 但是区别也仅是增加了更多的方法,如 mockImplementationOnce,等(见上图)

延伸总结

const mockedFunc= jest.fn( originalFunc );
mockedFunc可以看成是原始函数的加强版,注入了监控方法而已。其他核心没有变化。
即,此时如果调用mockedFunc(), 则相当于 originalFunc().

复杂的例子
const myMockFn = jest.fn(cb => cb(null, true));

myMockFn((err, val) => console.log(val));
// > true

step1: 简化 myMockFn ~= (cb)=> {
     return cb(null, true);
 }
 
step2: myMockFn((err, val) => console.log(val)) 相当于curry方法 
return console.log(true)



复制代码

继续理解除了监控方法外的其他方法

  • 除了监控数组mock之外(getter)
  • 还有其他方法被附加到了mockedFunc上面,比如mockImplementation,mockImplementationOnce,mockReturnValue等
  • 怎么理解?


function fn(impl = ()=>{} ) {

  const mockFn = function(...args) {
    mockFn.mock.calls.push(args);
    // 如果提供了value则直接短路,不执行原始函数
    if(mockReturnValue.value){
     return mockReturnValue.value;
    }
    
    // 如果提供替代的函数则跳过原始函数
    if(this.mockImplementation){
      return this.mockImplementation(...args);
    }
    return impl(...args); 
   };
   
  ...
  mockFn.mockReturnValue = (value)=>{
  this.mockReturnValue=value;
  };
  
  mockFn.mockImplementation = (func)=>{
  this.mockImplementation=func;
  };
  
  
  return mockFn;
}

复制代码

明白了上面的psuedo代码,看jest官方例子就简单了,比如

image.png

过程:

  1. 首先需要jest.mock('../foo') 相当于把jest.fn(foo)一下,即,套住foo;
  2. 如果没有1,则下面foo.mockImplementation...会报错,因为foo没有这个方法;
  3. 一旦使用了1,则原来的foo已经不再是原来的foo了; 而是一个副本,一个增加了方法的副本;
  4. foo.mockImplementation(() => 42); 其实就是

const newFoo= jest.fn(foo).mockImplementation(()=>42); 此时 newFoo()调用则绕过了原始的foo;当然由于3的存在,foo的名字没有变化,还是foo。

官方的这个例子是怎么回事?new哪里来的?

image.png

看了上面我们的手写,应该明白此时的fn工厂函数其实是个 function constructor,即,在远古时代,程序员靠function constructor来代替class,生成多个instance,即 instance1= new myMock()... 同时来绑定this,验证:

image.png

更多的例子

const { checkIfExists } = require("../../functions/simple");
const fs = require("fs");
const { readFileSync } = require("fs");

jest.mock("fs");
jest.mock("../../functions/simple");

// 尤其全局的mock;每次mock都会记录在mock的数组中;所以需要清空。
beforeEach(function () {
  jest.clearAllMocks();
});


describe("understand module mock w. fs", () => {

    test("should return true if file exists", () => {
        // 如果不mock;则mock module返回的是undefined;合理。需要你去mock implementation;
        // 如果不做,则类似 jest.fn();
        const fileName = "file.txt";
        const reading=  fs.readFileSync(fileName);

        console.log(reading);
        expect(fs.readFileSync.mock.calls[0][0]).toBe(fileName);

       
    });
});

describe("understand jest mock fun", () => {

 // 可以继续使用const不会跟mock的冲突
  test("method 1 to mock", () => {
    const checkIfExists1 = jest.fn(() => true);
    const value = checkIfExists1();
    expect(checkIfExists1).toBeCalledTimes(1);
    expect(checkIfExists1()).toBeTruthy();
    console.log(checkIfExists1.mock);
  });

 // toBeTruthy()必须调用();不然可以使用 mockednFunc.mock.results[0].value;
 // expect(checkIfExists).toBeTruthy(); 因为函数本身不是null,所以一定truthy
  test("method 2 to mock", () => {
    checkIfExists.mockImplementation(() => {
      console.log("mockImplementation");
    });

    checkIfExists();
    expect(checkIfExists).toBeCalledTimes(1);
    expect(checkIfExists).toBeTruthy();
  });

  test("method 3 to mock", () => {
    checkIfExists.mockImplementation();

    checkIfExists();
    expect(checkIfExists).toBeCalledTimes(1);
    expect(checkIfExists()).not.toBeTruthy();
  });

 // 如果mock的时候么有implementation等,则返回的是undefined; 合理,因为就是 jest.fn()
  test("method 4 to mock original", () => {
    checkIfExists();
    expect(checkIfExists).toBeCalledTimes(1);
    expect(checkIfExists()).toBeTruthy();
  });
  
});


describe("understand async mock", () => {
    // async 的mock和assert需要看官方文档
    test("method async 1 to mock", async () => {
        const mockedFetch = jest.fn(() => {
          return Promise.resolve({
            json: () =>
              Promise.resolve({
                data: 1,
              }),
          });
        });
    
        return expect(mockedFetch().then((res) => res.json())).resolves.toEqual({
          data: 1,
        });
      });
})
复制代码

猜你喜欢

转载自juejin.im/post/7080493139764461605