原理を理解するためにジェストを引き裂く

ユニットテストはJestと切り離せませんが、Jestがどのように実装されているか知っていますか?特に、jest.fn()、jest.mock()、jest.spyOn()とは何ですか?

1つ:最初にjest.fn()とは何かを見てください

image.png

写真はいくつかの結論を示しています

  • jestはオブジェクトまたはクラスインスタンスであり、メソッド/関数fnを持っています
  • このfn関数は関数を受け入れます
  • fn処理後、新しい関数、つまり[関数:mockConstructor]が返されます。
  • 新しい関数が返され、mockのgetterやmo​​ckImplementationなどの不可解に追加された新しいメソッドが返されました
  • したがって、このfnは、raw関数を受け入れ、モック関数を返すファクトリ関数です。

つまり、3人のプレーヤーがいます

  • ファクトリ関数ファクトリ関数
  • オリジナル機能オリジナル機能
  • モック機能モック機能

2:返されたモックのゲッターが何であるかを引き続き確認します

image.png

  • このモックは、インスタンス[このインスタンス]と結果[実行結果]を格納するものであることがわかりますが、当面はリストされていません。

3:Jestのドキュメントによると、モックプレーンオブジェクトの3つのキーは次のとおりです。

  • 呼び出し:パッシブ関数への各呼び出しのパラメーターを格納します
  • インスタンス:元の関数への各呼び出しを格納するこのオブジェクト(コピー)
  • 結果:パッシブ関数への各呼び出しの結果を格納します

パッシブ関数を呼び出した結果は3つだけです

  • 結果値を返す
  • 値を返しませんでしたが、未定義
  • スローエラーエラー

4:一体何が期待されているのですか?実際、モックオブジェクトを表示するのは糖衣構文です。

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

モックオブジェクトの3つのキーの結果を表示しやすくするために、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

次に、クロージャークロージャーを使用して、モックの呼び出し配列を記録します

 

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ファクトリ関数は実際には関数コンストラクターであることを理解する必要があります。つまり、古代では、プログラマーはクラスを置き換えて複数のインスタンスを生成するために関数コンストラクターに依存していました。つまり、instance1 = new myMock ()...同時に、これをバインドして確認します。

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