This article was first published in Nuggets, please indicate the source of reprint.

Mock modules

In Jest, it is very easy to mock a module, just using Jest. Mock. There are two main cases of mocking a module:

  1. Only non-default exports in mock modules

    Mock For non-default exports (export const, export class, etc.), just use jest. Mock and return an object containing the function or variable you want to mock:

    // Foo in mock 'moduleName'
    jest.mock('.. /moduleName'.() = > ({
      foo: jest.fn().mockReturnValue('mockValue'),}));Copy the code
  2. The default export in the mock module

    For a mock exported by default, instead of returning a simple object, you need to include a default attribute in the object along with __esModule: true.

    When using the factory parameter for an ES6 module with a default export, the __esModule: true property needs to be specified. This property is normally generated by Babel / TypeScript, but here it needs to be set manually. When importing a default export, it’s an instruction to import the property named default from the export object

    import moduleName, { foo } from '.. /moduleName';
    
    jest.mock('.. /moduleName'.() = > {
      return {
        __esModule: true.default: jest.fn(() = > 42),
        foo: jest.fn(() = > 43),}; }); moduleName();// Will return 42
    foo(); // Will return 43
    Copy the code

Part of the mock module

If you only want part of the mock module and leave the rest as is, you can use jest. RequireActual to introduce the real module:

import { getRandom } from '.. /myModule';

jest.mock('.. /myModule'.() = > {
  // Require the original module to not be mocked...
  const originalModule = jest.requireActual('.. /myModule');

  return {
    __esModule: true.// Use it when dealing with esModules. originalModule,getRandom: jest.fn().mockReturnValue(10),}; }); getRandom();// Always returns 10
Copy the code

Mock module internal functions

Consider a case where there is an utils.ts file that internally exports two functions funcA and funcB, and then references funcA in funcB:

// utils.ts
export const funcA = () = > {
  // ...
}

export const funcB = () = > {
  funcA();
  // ...
}
Copy the code

FuncB will fail if we try to mock it when we unit test it:

import { funcA, funcB } from '.. /src/utils';

jest.mock('.. /src/utils'.() = > {
  const originalModule = jest.requireActual('.. /src/utils');
  return {
    ...originalModule,
    funcA: jest.fn(),
  };
});

describe('Utils.ts unit Test'.() = > {
  test('test funcB'.() = > {
    funcB();
    expect(funcA).toBeCalled();
  });
});
Copy the code

Running a single test results in an error

Obviously, our mock on funcA failed, because the funcA reference we imported from outside the module is not the same as the funcA reference we used directly inside the module, and modifying funcA through jest. Mock does not affect the internal call. There are two suggested solutions to this situation:

  • Split file willfuncASplit into different files. This can cause problems with too many and scattered files.
  • Implement functions that call each other as methods of a utility class. The functions that call each other will be placed in the same utility class.

Mock class ()classConstructor calls to other member functions

When we mock a class’s method, we simply assign the corresponding method of the class object to jest.fn(), but we cannot do the same for member methods called in the constructor. Because methods in a class can only be mock Ed after the instantiation is complete, you cannot prevent the original function from being executed in constructor.

Class member methods are essentially methods that are attached to the class prototype, so we just need the mock constructor’s prototype methods:

class Person {
  constructor() {
    this.init();
    // ...
  }
  public init(){}}Copy the code
Person.prototype.init = jest.fn();
Copy the code

Private functions in Mock classes (for TypeScript)

You can’t get a ts class’s private function directly (although you can ignore ts, but it’s not recommended). Instead, mock on the class’s prototype using the same method:

class Person {
  private funcA(){}}Copy the code
Person.prototype.funcA = jest.fn();
Copy the code

Mock object read-only propertiesgetter)

In a single test, we can easily mock readable and writable properties by assigning them to the corresponding mock value, such as platform.os. This cannot be done directly with a mock for read-only getters. Usually for read-only attributes (here in the document. The body. ClientWidth, for example) has the following two mock ways:

  1. throughObject.defineProperty
    Object.defineProperty(document.body, 'clientWidth', {
      value: 10.set: jest.fn(),
    });
    Copy the code
  2. throughjest.spyOn
    const mockClientWidth = jest.spyOn(document.body, 'clientWidth'.'get');
    mockClientWidth.mockReturnValue(10);
    Copy the code

Seven, the use oftoBeCalledWithAssert an anonymous function in a parameter

When we need to test a method, we sometimes need to assert that the method is called with specific parameters. ToBeCalledWith can do this, but consider the following situation

export const func = (): void= > {
  if (/* condition 1 */) {
    moduleA.method1(1.() = > {
      // do something
    });
  } else {
    moduleA.method1(2); }}Copy the code

In some cases, moduleA.method1 will be passed argument 1 and an anonymous function. How do you use toBeCalledWith to assert that moduleA.method1 was called with those arguments? Since the second argument is an anonymous function, there is no external way to mock. At this point, we can use expect. Any (Function) to assert:

moduleA.method1 = jest.fn();
// create condition 1
func();
expect(moduleA.method1).toBeCalledWith(1, expect.any(Function));
Copy the code

Because we only care if moduleA.method1 is passed a second parameter and the parameter is a Function, we can expect. Any (Function).

Eight, the mocklocalStorage

Mock localStorage; mock localStorage; mock localStorage; mock localStorage; mock localStorage

class LocalStorageMock {
  private store: Record<string.string> = {};

  public setItem(key: string, value: string) {
    this.store[key] = String(value);
  }

  public getItem(key: string) :string | null {
    return this.store[key] || null;
  }

  public removeItem(key: string) {
    delete this.store[key];
  }

  public clear() {
    this.store = {};
  }

  public key(index: number) :string | null {
    return Object.keys(this.store)[index] || null;
  }

  public get length() :number {
    return Object.keys(this.store).length; }}global.localStorage = new LocalStorageMock();
Copy the code

We recommend putting mocks in a separate Mocks file and introducing them separately where we need to test them:

import './__mocks__/localStorage';
Copy the code

Nine, the mockindexedDB

IndexedDB is similar to localStorage. It is a transactional database system in the browser environment and cannot be obtained in the Node environment. However, it is not recommended to implement indexedDB by yourself because there are many interfaces and types. A common practice is to use the fake-IndexedDB library, which implements the various interfaces to IndexedDB in memory using pure JS and is primarily used to test indexedDB-dependent code in a Node environment.

For files that need to be tested, simply introduce fake-IndexedDB/Auto at the beginning of the file:

import 'fake-indexeddb/auto';
Copy the code

If you need to import fake-IndexedDB for all your files, just add the following configuration to your JEST configuration:

// jest.config.js
module.exports = {
  // ...
  setupFiles: ['fake-indexeddb/auto']};Copy the code

Or in the package. In json

"jest": {..."setupFiles": ["fake-indexeddb/auto"]}Copy the code

Test asynchronous functions

In a single test, if you need to test an asynchronous function, you can perform the following operations for different situations:

  1. The callback function is asynchronous

    For asynchronous callback functions (such as the setTimeout callback), there is no way to get the correct assertion if you test like a synchronous function:

    export const funcA = (callback: (data: number) = > void) :void= > {
      setTimeout(() = > {
        callback(1);
      }, 1000);
    };
    Copy the code
    test('funcA'.() = > {
      funcA((data) = > expect(data).toEqual(2));
    });
    Copy the code

    FuncA, like above, passes a 1 in a callback, and can pass a single test even if it asserts a result of 2:

    This is because Jest is finished running funcA, not waiting for the setTimeout callback, and therefore not performing expect assertions. Instead, pass in a done argument:

    test('funcA'.(done) = > {
      funcA((data) = > {
        expect(data).toEqual(2);
        done();
      });
    });
    Copy the code

    Explicitly telling JEST that the asynchronous function is finished after the callback is executed, jest will wait until done() is executed to get the expected result.

  2. Promise the asynchronous

    In addition to the callback function, another common asynchronous scenario is Promise. For Promise asynchrony, it is not as complicated as the above, just need to return the Promise at the end of the test case:

    export const funcB = (): Promise<number> = > {return new Promise<number> ((resolve) = > {
        setTimeout(() = > {
          resolve(1);
        }, 1000);
      });
    };
    Copy the code
    test('funcB'.() = > {
      return funcB().then((data) = > expect(data).toEqual(1));
    });
    Copy the code

    If async/await syntax is used, it is even more concise. Promises do not need to be returned and can be written as if testing synchronous code:

    test('funcB'.async() = > {const data = await funcB();
      expect(data).toEqual(1);
    });
    Copy the code

    For exceptions thrown by Promises, the test is similar:

    // A method that throws an exception
    export const funcC = (): Promise<number> = > {return new Promise<number> ((resolve, reject) = > {
        setTimeout(() = > {
          reject('something wrong');
        }, 1000);
      });
    };
    Copy the code
    test('funcC promise'.() = > {
      return funcC().catch((error) = > expect(error).toEqual('something wrong'));
    });
    // or
    test('funcC await'.async() = > {try {
        await funcC();
      } catch (error) {
        expect(error).toEqual('something wrong'); }});Copy the code

Xi. Non-executionjest.spyOnA function of the mock

Jest. Fn and jest. SpyOn can both be used to mock a function, but jest. Fn will not be executed, while jest. So is there any way to stop the jest. SpyOn mock function from executing? In the “Getter for a Mock object”, mock a getter with jest. SpyOn and mock a return value with mockReturnValue. The original function is not executed.

In addition, mockImplementation has the same effect:

mockFn.mockImplementation(() = > {});
Copy the code

This concludes with functions that do not execute jest. SpyOnmock using mockReturnValue and mockImplementation.

As an additional tip, if you can use jest. Fn, try not to use jest. SpyOn, because jest.

Xii. Usetest.each

Sometimes we have to write a large number of single test cases, but each case has the same or similar structure with slight differences, such as testing a format function to return a different string, or calling different member methods of a class that return similar results (e.g., both throw an error or return NULL). For these cases, we can sometimes write an array in a single test and iterate over it, but jest already provides a way to handle this, test.each, to give a few examples:

// each.ts
export const checkString = (str: string) :boolean= > {
  if (str.length <= 0) {
    throw new Error('mockError 1');
  } else if (str.length > 5) {
    throw new Error('mockError 2');
  } else {
    return true; }};// each.test.ts
describe('each. Ts Unit test '.() = > {
  test.each<{ param: string; expectRes: boolean | string[{} > (param: ' '.expectRes: 'mockError 1'}, {param: '123456'.expectRes: 'mockError 2'}, {param: '1234'.expectRes: true,}]) ('checkString'.(data) = > {
    const { param, expectRes } = data;
    try {
      const result = checkString(param);
      expect(result).toEqual(expectRes);
    } catch(error) { expect(error.message).toEqual(expectRes); }}); });Copy the code

Another example is the case where all methods in an object store throw exceptions:

test.each<{
  func: 'get' | 'delete' | 'add' | 'update'; param? :any; [{} > (func: 'get'.param: ['mockKey'] {},func: 'delete'.param: ['mockKey'] {},func: 'add'.param: ['mockKey'.'mockValue'] {},func: 'update'.param: ['mockKey'.'mockValue']},]) ('Call store's method to throw an exception'.(data) = > {
  returnstore[data.func](... data.param).catch((err) = > {
    expect(err).toEqual('mockError');
  });
});
Copy the code

In addition to test.each, there is also describe.each, refer to test.each and describe.each for more details

Xiii. Use.test.js,.test.ts,.test.tsx

Js,.test.ts, and.test.tsx. For example, in the case of utils.ts, it is recommended that the corresponding single test be named utils.test.ts, so that each single test file has clear semantics from the file name alone, i.e. this is a single test file. Rather than a source file with specific features.

Also, files in the list are more visible and easily recognizable when searching for files or global search strings. Furthermore, many IDE file image icon plugins now have different renderings for different file name endings, making it easier to identify:

14. Use togetherJest RunnerThe plug-in

Js,.test.ts, and.test. TSX render several button options:

By clicking Run or Debug, you can only Run or Debug a test or describe. You do not need to re-run the global NPM Run test or execute the file separately with jEST, which greatly improves the efficiency of writing a single test:

This plugin only works with.test.js,.test.ts, and.test. TSX files, so this is one of the reasons why it is recommended to use the.test.js,.test.ts, and.test. TSX names for single test files.

At the same time, the Debug provided by the plug-in, also eliminates the tedious launch.json configuration, can easily Debug breakpoints.