Tear Jest를 통해 원리 이해하기

단위 테스트는 Jest와 분리할 수 없지만 Jest가 어떻게 구현되는지 알고 있습니까? 특히 jest.fn(), jest.mock(), jest.spyOn()은 무엇입니까?

하나: jest.fn()이 무엇인지 먼저 살펴보십시오.

이미지.png

사진은 몇 가지 결론을 보여줍니다

  • jest는 객체 또는 클래스 인스턴스이며 fn 메서드/함수가 있습니다.
  • 이 fn 함수는 함수를 받습니다.
  • fn 처리 후 [Function: mockConstructor]라는 새 함수가 반환됩니다.
  • 모의 getter 및 mockImplementation과 같은 설명할 수 없는 새로운 메서드가 추가된 새 함수가 반환되었습니다.
  • 따라서 이 fn은 원시 함수를 받아들이고 모의 함수를 반환하는 팩토리 함수입니다.

그래서 세 명의 선수가 있습니다.

  • 팩토리 기능 팩토리 기능
  • 원래 기능 원래 기능
  • 모의 함수 모의 기능

2: 반환된 mock의 getter가 무엇인지 계속 확인

이미지.png

  • 이 mock은 인스턴스[이 인스턴스]와 결과[실행 결과]를 저장하는 것으로, 당분간 나열되지 않음을 알 수 있습니다.

3: Jest의 문서는 모의 일반 객체의 세 가지 키가 다음과 같다는 것을 보여줍니다.

  • 호출: 수동 함수에 대한 각 호출의 매개변수를 저장합니다.
  • 인스턴스: 원래 함수(복사본)에 대한 각 호출을 저장하는 이 객체
  • 결과: 수동 함수에 대한 각 호출의 결과를 저장합니다.

수동 함수를 호출한 결과는 3개뿐입니다.

  • 반환 결과 값
  • 값을 반환하지 않았지만 정의되지 않음
  • 오류 발생 오류

4: 도대체가 무엇을 기대하는 겁니까? 사실 모의 객체를 표시하는 것은 구문상의 설탕입니다.

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

모의 객체의 세 키 결과를 쉽게 볼 수 있도록 Jest는 위의 여러 구문 설탕을 제공합니다. 예를 들어 .toBeCalledTime()은 Results.length입니다.

5: 농담의 원리를 이해하고 공장 Jest 함수를 작성해 봅시다!

먼저 팩토리 함수, 즉 집합을 만드십시오. 참고: 원래 기능을 복사하십시오.

// object literal 
const jest={ }

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

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

jest.fn=fn;

复制代码

해석: 이 집합은 아무 것도 하지 않고 원래 기능을 그대로 복사하는 것과 같습니다.

테스트:

이미지.png

그런 다음 더 나아가 클로저 클로저를 사용하여 모의 호출 배열을 기록합니다.

 

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

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


复制代码

테스트:

이미지.png

해석:

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

이미지.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官方例子就简单了,比如

이미지.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哪里来的?

이미지.png

위의 손글씨를 읽은 후에 우리는 현재 fn 팩토리 함수가 실제로 함수 생성자라는 것을 이해해야 합니다. 즉, 고대에 프로그래머는 함수 생성자에 의존하여 여러 인스턴스를 생성하는 클래스를 대체했습니다. ()... 동시에 이것을 바인딩하고 다음을 확인합니다.

이미지.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