Unit testing can’t do without Jest, but do you know how Jest is implemented? Jest. Fn (), jest. Mock (), jest. SpyOn ()

What is jest. Fn ()

The display draws several conclusions

  • Jest is an Object or Class Instance, and it has a method/function fn
  • This fn function takes a function
  • After fn processing, a new Function is returned, that is [Function: mockConstructor]
  • Returns new functions, inexplicably adding new methods, such as mock getters, and mockImplementation
  • So fn is a factory function that takes a primitive function and returns a mock function

So there are three players

  • Factory function Factory Func
  • The Original function Original Func
  • The simulation function Mocked Func

What is the getter for the returned mock

  • This is a mock that stores instances of this and results.

The Jest document shows the mock plain object’s three keys

  • Calls: Stores arguments for each call to a passive function
  • Instances: Stores this object for each call to the original function (a copy)
  • Results: Stores the Results of each call to a passive function

There are only three results from calling a passive function

  • Return value
  • Instead of returning any value, undefined
  • Throw Error

Four: What the devil Expect? This is the syntactic sugar that shows the mock object

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

To make it easy to see the results of the mock object’s three keys, Jest provides a few of the syntax sugars above. For example, toBeCalledTime() is results.length

5: Understand the jEST principle, let’s write a factory JEST function!

So let’s just do a simple factory function, the case. Note: copy the original function.

// object literal 
const jest={ }

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

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

jest.fn=fn;

Copy the code

Interpretation: This loop does nothing but copy the original function exactly.

Testing:

Then go a step further and log the mock’s array of calls using the closure

 

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

  const mockFn = function(. args) {
    mockFn.mock.calls.push(args);
    returnimpl(... args); };// Here is the mock object corresponding to calls
  mockFn.mock = {
    calls: []};return mockFn;
}


Copy the code

Testing:

Reading:

  • The factory function uses the closure principle, wrapping the original function and isolating it to generate a new function with an additional monitoring array.
  • Note ⚠️ : the simulated function is not the same as the original function. The proof is shown below.
  • Also, it can only be monitored when a mock function is called. It makes no sense to call the original function because they point to different memory addresses. Mock functions are called because that’s what mock means.

Final full implementation

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; }Copy the code

Of course, the real jest mock function is more complex than this, see jestjs. IO /docs/mock-f… But the difference is simply that more methods are added, such as mockImplementationOnce, etc. (see figure above)

Extension of summary

constmockedFunc= jest.fn( originalFunc ); MockedFunc can be seen as an enhanced version of the original function, injecting monitoring methods. The other cores are unchanged. If mockedFunc() is called, it is equivalent to originalFunc(). Complex exampleconst myMockFn = jest.fn(cb= > cb(null.true));

myMockFn((err, val) = > console.log(val));
// > trueStep1: Simplify myMockFn ~=(cb) = > {
     return cb(null.true);
 }
 
step2: myMockFn((err, val) = > console.log(val)) is equivalent to the Curry methodreturn console.log(true)



Copy the code

Continue to understand other methods than monitoring methods

  • In addition to monitoring array mocks (getters)
  • There are other methods attached to mockedFunc, such as mockImplementation, mockImplementationOnce, mockReturnValue, and so on
  • How to understand?


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

  const mockFn = function(. args) {
    mockFn.mock.calls.push(args);
    // If value is provided, the original function is not executed
    if(mockReturnValue.value){
     return mockReturnValue.value;
    }
    
    // Skip the original function if an alternative function is provided
    if(this.mockImplementation){
      return this.mockImplementation(... args); }returnimpl(... args); }; . mockFn.mockReturnValue =(value) = >{
  this.mockReturnValue=value;
  };
  
  mockFn.mockImplementation = (func) = >{
  this.mockImplementation=func;
  };
  
  
  return mockFn;
}

Copy the code

With the psuedo code above, it’s easy to look at the official JEST example, for example

Process:

  1. Jest. Mock (‘.. /foo’) is equivalent to pinning jest. Fn (foo), i.e., trapping foo;
  2. If there is no 1, then the following foo mockImplementation… An error is reported because foo does not have this method;
  3. Once 1 is used, the original foo is no longer the original foo; It’s a copy, a copy with methods added;
  4. Foo mockImplementation (() = > 42); In fact, is

Const newFoo = jest. Fn (foo) mockImplementation (() = > 42); The newFoo () call bypasses the original foo; Of course, because of the 3, foo’s name doesn’t change, it’s still foo.

What about the official example? Where does new come from?

The fn factory function (fn) constructor (fn) constructor (fn) constructor (fn) constructor (fn) Namely, instance1 = new myMock ()… Also bind this to verify:

More examples

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

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

// Especially global mock; Each mock is recorded in the mock array; So it needs to be emptied.
beforeEach(function () {
  jest.clearAllMocks();
});


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

    test("should return true if file exists".() = > {
        // If no mock; The mock Module returns undefined; Reasonable. You need to mock implementation;
        // If you do not do this, it is similar to 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".() = > {

 // We can continue to use const without interfering with 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() must call (); Mockednfunc.mock.results [0].value;
 // expect(checkIfExists).toBeTruthy(); Since the function itself is not null, it must be 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();
  });

 // If the mock does not have implementation, undefined is returned; Jest. Fn ()
  test("method 4 to mock original".() = > {
    checkIfExists();
    expect(checkIfExists).toBeCalledTimes(1);
    expect(checkIfExists()).toBeTruthy();
  });
  
});


describe("understand async mock".() = > {
    // Async mocks and assert require official documentation
    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}); }); })Copy the code