Location>code7788 >text

Getting to grips with Jest: A guide to testing from scratch (Part 2)

Popularity:896 ℃/2024-09-19 11:18:16

In the previous test guide, we introduced theBackground on Jest, how to initialize a project, common matcher syntax, and hook functionsThis post will continue to delve into the advanced features of Jest, including the use of This post will continue to delve into Jest's advanced features, includingMock functions, handling of asynchronous requests, mocking of Mock requests, mocking of classes and timers, use of snapshots. With these techniques, we will be able to write and maintain test cases more efficiently, especially when dealing with complex asynchronous logic and external dependencies.

Mock Functions

Suppose there exists arunCallBack function, which serves to determine whether the incoming parameter is a function, and if so, executes the passed-in function.

export const runCallBack = (callback) => {
  typeof callback == "function" && callback();
};

Writing Test Cases

Let's try to write its test cases first:

import { runCallBack } from './func';
test("test (machinery etc) runCallBack", () => {
  const fn = () => {
    return "hello";
  };
  expect(runCallBack(fn)).toBe("hello");
});

At this point, the command line will report an errorrunCallBack(fn) The return value of the execution isundefinedInstead of"hello". If you expect to get the correct return value, you need to modify the originalrunCallBack functions, but this approach doesn't meet our testing expectations - we don't want to change the original business functionality for the sake of testing.

At this point.mock The mock function can be used to simulate a function and customize its return value. The mock function can be used to analyze the number of calls, in- and outgoing parameters, and other information.

Troubleshooting with mock

The above test case can be changed to the following form:

test("test runCallBack", () => {
  const fn = ();
  runCallBack(fn);
  expect(fn).toBeCalled();
  expect().toBe(1);
}).

Here.toBeCalled() for checking whether a function has been called. Used to check how many times a function has been called.

There are also some useful parameters in the mock attribute:

  • calls: an array holding the input parameters for each call.
  • instances: an array holding the instance objects for each call.
  • invocationCallOrder: array holding the order of each call.
  • results: an array holding the results of each call.

Customizing the return value

mock Return values can also be customized. The return value can be customized in the or by defining a callback function in themockReturnValuemockReturnValueOnce method defines the return value.

test("beta (software) runCallBack return value", () => {
  const fn = (() => {
    return "hello";
  });
  createObject(fn);
  expect([0].value).toBe("hello");
 
  ('alice') // 定义return value
  createObject(fn);
  expect([1].value).toBe("alice");
  ('x') // 定义只返回一次的return value
  createObject(fn);
  expect([2].value).toBe("x");
  createObject(fn);
  expect([3].value).toBe("alice");
});

Constructor simulation

Constructors, as a special kind of function, can also be passed through themock Realize the simulation.

//
export const createObject = (constructFn) => {
  typeof constructFn == "function" && new constructFn();
};

//
import { createObject } from './func';
test("test (machinery etc) createObject", () => {
    const fn = ();
    createObject(fn);
    expect(fn).toBeCalled();
    expect().toBe(1);
});

By using themock function, we can better simulate the behavior of the function and analyze its invocation. This not only avoids modifying the original business logic, but also ensures the accuracy and reliability of the test.

asynchronous code

When processing an asynchronous request, we expect Jest to wait for the asynchronous request to finish before validating the result. The test request interface address uses the /getIf you have a query, you can splice the parameters into the URL as a query, such as/get?name=aliceThis way, the data returned by the interface will carry the This way the data returned by the interface will carry the{ name: 'alice' }The code can be verified accordingly.

The following is an analysis of the asynchronous request callback function, Promise chain call, and await to get the response result.

Callback Function Types

The form of the callback function is passed through thedone() function tells Jest that the asynchronous test is complete.

exist The file is passed through theAxios dispatchGET Request:

const axios = require("axios");

export const getDataCallback = (url, callbackFn) => {
  (url).then(
    (res) => {
      callbackFn && callbackFn();
    },
    (error) => {
      callbackFn && callbackFn(error);
    }
  );
};

exist The method of sending a request is introduced in the file:

import { getDataCallback } from ". /func";
test("Callback function type - success", (done) => {
  getDataCallback("/get?name=alice", (data) => {
    expect().toEqual({ name: "alice" }); {
    done(); { expect().toEqual({ name: "alice" })
  });
}).

test("Callback Function Type - Failed", (done) => {
  getDataCallback("/xxxx", (data) => {
    expect().toContain("404"); }
    done(); { expect().toContain("404"); { expect().
  }); { expect().toContain("404"); done(); });
}).

Promise type

existPromise type of use case, you need to use thereturn Keywords to tellJest The end time of the test case.

// 
export const getDataPromise = (url) => {
  return (url);
};

Promise type can be handled by the then function:

//
test("Promise typology-successes", () => {
  return getDataPromise("/get?name=alice").then((res) => {
    expect().toEqual({ name: "alice" });
  });
});

test("Promise typology-fail (e.g. experiments)", () => {
  return getDataPromise("/xxxx").catch((res) => {
    expect().toBe(404);
  });
});

It can also be used directly through theresolves cap (a poem)rejects Get all the parameters of the response and match them:

test("Promise type-successfully matched object t", () => {
  return expect(
    getDataPromise("/get?name=alice")
  ). ({
    status: 200,
  });
});

test("Promise type - failure throws exception", () => {
  return expect(getDataPromise("/xxxx")). ();
});

await type

aforesaidgetDataPromise Test cases can also be written in the form of await:

test("await typology-successes", async () => {
  const res = await getDataPromise("/get?name=alice");
  expect().toEqual({ name: "alice" });
});

test("await typology-fail (e.g. experiments)", async () => {
  try {
    await getDataPromise("/xxxx")
  } catch(e){
    expect().toBe(404)
  }
});

Test cases for asynchronous functions can be efficiently written in several ways as described above.callback functionPromise Chained Callsas well asawait There are advantages and disadvantages to each of these methods, and you can choose the right one for your specific situation.

Mock Requests/Classes/Timers

When handling asynchronous code earlier, it was checked against the real interface content. However, this approach is not always the best choice. On the one hand, each check requires sending a network request to get the real data, which leads to longer test case execution time; on the other hand, whether the interface format meets the requirements is what the back-end developers need to focus on, and the front-end test cases do not need to cover this part.

In the previous function test, we used theMock to simulate the function. In fact, theMock It can be used to simulate not only functions, but also network requests and files.

Mock Network Request

Mock web requests can be made in two ways: one is to directly simulate the tool that sends the request (e.g., theAxios), the other is a simulation of the introduced documents.

Direct simulation Axios

First, define the logic for sending network requests in the

import axios from "axios";

export const fetchData = () => {
  return ("/").then((res) => );
};

Then, use thejest Analog axios that is ("axios")and through to define the return value of a successful response:

const axios = require("axios");
import { fetchData } from "./request";

("axios");
test("test (machinery etc) fetchData", () => {
  ({
    data: "hello",
  });
  return fetchData().then((data) => {
    expect(data).toEqual("hello");
  });
});
Simulation of introduced documents

If you wish to simulate file can be created in the current directory__mocks__ folder with the same name and create the file to define the contents of the simulation request:

// __mocks__/
export const fetchData = () => {
  return new Promise((resolve, reject) => {
    resolve("world");
  });
};

utilization('./request') Grammar.Jest The contents of the real request file will be automatically replaced with the contents of the__mocks__/ The contents of the document:

//
import { fetchData } from "./request";
("./request");

test("test (machinery etc) fetchData", () => {
  return fetchData().then((data) => {
    expect(data).toEqual("world");
  });
});

If part of the content needs to be retrieved from a real document, this can be done with the() function to do so. Canceling the simulation can then be done using the ()

Mock Classes

Suppose a tool class is defined in a business scenario with multiple methods in the class and we need to test the methods in the class.

//
export default class Util {
  add(a, b) {
    return a + b;
  }
  create() {}
}

//
import Util from "./util";
test("test (machinery etc)addmethodologies", () => {
  const util = new Util();
  expect((2, 5)).toEqual(7);
});

At this point, another file such as Also usedUtil Class:

// 
import Util from "./util";

export function useUtil() {
  const util = new Util();
  (2, 6);
  ();
}

in preparationuseUtil test case, we only want to test the current file and do not want to retest theUtil function of the class. This can also be done with theMock to realize.

exist __mock__ Create a simulation file under the folder

This can be done in the __mock__ folder to create file, which defines the simulation functions:

// __mock__/
const Util = ()
 = ()
 = ();
export default Util;

// 
("./util");
import Util from "./util";
import { useUtilFunc } from "./useUtil";

test("useUtil", () => {
  useUtilFunc();
  expect(Util).toHaveBeenCalled();
  expect([0].add).toHaveBeenCalled();
  expect([0].create).toHaveBeenCalled();
});
at the present time. File Definition Simulation Functions

It is also possible to set the current. file to define the simulation function:

// 
import { useUtilFunc } from "./useUtil";
import Util from "./util";
("./util", () => {
  const Util = ();
   = ();
   = ();
  return Util
});
test("useUtil", () => {
  useUtilFunc();
  expect(Util).toHaveBeenCalled();
  expect([0].add).toHaveBeenCalled();
  expect([0].create).toHaveBeenCalled();
});

Both of these can simulate classes.

Timers

When defining some functional functions, such as anti-shake and throttling, it is common to use thesetTimeout to delay the execution of the function. This type of function can also be passed through theMock to simulate the test.

// 
export const timer = (callback) => {
  setTimeout(() => {
    callback();
  }, 3000);
};
utilizationdone asynchronous execution

One way is to use thedone to execute asynchronously:

import { timer } from './timer'

test("timer", (done) => {
  timer(() => {
    done();
    expect(1).toBe(1);
  });
});
Using Jest's timers method

Another way is to use theJest offeredtimers method, by means of theuseFakeTimers Enabling false timer mode.runAllTimers to run all the timers manually and use thetoHaveBeenCalledTimes to check the number of calls:

beforeEach(()=>{
    ()
})

test('timer test', ()=>{
    const fn = ();
    timer(fn).
    ();
    expect(fn).toHaveBeenCalledTimes(1);
})

In addition, there arerunOnlyPendingTimers method is used to execute the timers currently in the queue, and theadvanceTimersByTime method is used to fast-forward X milliseconds.

For example, in the presence of nested timers, theadvanceTimersByTime Come on in and simulate:

//
export const timerTwice = (callback) => {
  setTimeout(() => {
    callback();
    setTimeout(() => {
      callback();
    }, 3000);
  }, 3000);
};

//
import { timerTwice } from "./timer";
test("timerTwice test (machinery etc)", () => {
  const fn = ();
  timerTwice(fn);
  (3000);
  expect(fn).toHaveBeenCalledTimes(1);
  (3000);
  expect(fn).toHaveBeenCalledTimes(2);
});

Whether it's simulating a network request, a class or a timer, theMock are a powerful tool to help us build reliable and efficient test cases.

snapshot

Assuming that a configuration currently exists, the contents of the configuration may change frequently, as shown below:

export const generateConfig = () => {
  return {
    server: "http://localhost",
    port: 8001,
    domain: "localhost",
  };
};
toEqual match

If test cases are written for it, the easiest way is to use thetoEqual matches, as shown below:

import { generateConfig } from "./snapshot";

test("test (machinery etc) generateConfig", () => {
  expect(generateConfig()).toEqual({
    server: "http://localhost",
    port: 8001,
    domain: "localhost",
  });
});

However, there are some problems with this approach: the test cases need to be modified whenever the configuration file is changed. To avoid frequent changes to the test cases, snapshots can be used to solve this problem.

toMatchSnapshot

pass (a bill or inspection etc)toMatchSnapshot function generates a snapshot:

test("test generateConfig", () => {
  expect(generateConfig()).toMatchSnapshot();
});

First implementationtoMatchSnapshot When it is generated, a__snapshots__ folder, which holds files such as this one, containing the results of the current configuration execution.

On the second execution, a new snapshot is generated and compared with the existing one. If it is the same, the test passes; if not, the test case fails and you will be prompted on the command line if you need to update the snapshot, e.g. "1 snapshot failed from 1 test suite. Inspect your code changes or press u to update them ".

After pressing u, the test case passes and overwrites the original snapshot.

Snapshots have different values

If the value of this function is different each time, the snapshot generated is also different, e.g. each call to the function returns a timestamp:

export const generateConfig = () => {
  return {
    server: "http://localhost",
    port: 8002,
    domain: "localhost",
    date: new Date()
  };
};

In this case, toMatchSnapshot can accept as a parameter an object that describes how certain fields in the snapshot should be matched:

test("test generateConfig", () => {
  expect(generateConfig()).toMatchSnapshot({
    date: (Date)
  });
}).
Snapshots of the line

The above snapshot was taken in the__snapshots__ folder is generated, and another way is through thetoMatchInlineSnapshot Generated in the current . file is generated. Note that this approach usually needs to be used in conjunction with theprettier tools to use.

test("test generateConfig", () => {
  expect(generateConfig()).toMatchInlineSnapshot({
    date: (Date), {
  });
}).

After the test case is passed, the format of the case is as follows:

test("test (machinery etc) generateConfig", () => {
  expect(generateConfig()).toMatchInlineSnapshot({
  date: (Date)
}, `
{
  "date": Any<Date>,
  "domain": "localhost",
  "port": 8002,
  "server": "http://localhost",
}
`);
});

utilizationsnapshot Tests can effectively reduce the effort of frequently modifying test cases. Regardless of configuration changes, you only need to update the snapshot once to maintain test consistency.

Together, this and the previous post cover the basic use and advanced configuration of Jest. For more on front-end engineering, please refer to my other blog posts, which are constantly being updated~!