Small knowledge, big challenge! This article is participating in the creation activity of “Essential Tips for Programmers”.

How to test asynchronous code, hook functions, Snapshot, etc.

Testing asynchronous code

In JavaScript, it is common for code to run asynchronously. When you have code running asynchronously, Jest needs to know when the code it is testing is complete before it can move on to another test. Jest has the following processing methods, let’s look at one by one.

Callbacks

For example, fetchData(callback) is usually written to request data from the server. If the data request is successful, it is passed into the callback, so that we can perform some operation after the data request is successful.

// For example, after 1 second, request the data peanut butter, Callback export const fetchData = (callback) => {setTimeout(() => {callback('peanut ')}, 1000)}Copy the code
test('the data is peanut butter', () => { function callback(data) { expect(data).toBe('peanut butter'); } fetchData(callback); }); // The test will pass successfullyCopy the code

Write two pieces of code, the test will pass, and people will say that’s not right, same as before. Callback (‘peanut Butter111 ‘) is running or tested. FetchData (callback); It will not wait for the result of the request to call the callback. Test takes two arguments. The first is a description, and the second is a test function. This function can accept a done function, which does not end immediately after Jest executes the test code. Change the test code to the following and let’s see what happens:

// fetchData does not change, The test case passes // the callback in fetchData will not pass the test('the data is peanut butter', done => { function callback(data) { try { expect(data).toBe('peanut butter'); done(); } catch (error) { done(error); } } fetchData(callback); });Copy the code

Promises

In addition to using the callback function, we often use Promise to return the result of the request. How to test this, modify the code as follows:

export const fetchData = (callback) => { return new Promise((resolve) => { resolve('peanut butter') }) } test('the data is peanut butter', () => { return fetchData().then(data => { expect(data).toBe('peanut butter'); }); }); // The test passesCopy the code

What if we want to test reject a promise? .then will not execute, we can add.catch to improve the test case,

test('the fetch fails with an error', () => { // expect.assertions(1); return fetchData().catch(e => expect(e).toMatch('error')); }); // Test failed, add expect. Assertions (1); Will only beCopy the code

With annotations, our test case will pass even if we still resolve a value in fetchData. Why? As long as the fetchData performed then state changed (resolve, reject) that will be the end of the test cases, not execute catch method, then nature passed, we need to add the expect. Assertions (1); To specify how many times expect is executed and how many times it fails.

.resolves / .rejects

The use of Promise can be tested not only with the. Then and. Catch methods, but also with. Jest waits for the Promise to end and then continues

test('the data is peanut butter', () => {
    return expect(fetchData()).resolves.toBe('peanut butter');
});

test('the fetch fails with an error', () => {
  return expect(fetchData()).rejects.toMatch('error');
});
Copy the code

Async/Await

The async and await methods are also necessary for testing asynchronous code. The same fetchData scenario could look like this:

test('the data is peanut butter', async () => { const data = await fetchData(); expect(data).toBe('peanut butter'); }); test('the fetch fails with an error', async () => { expect.assertions(1); try { await fetchData(); } catch (e) { expect(e).toMatch('error'); }});Copy the code

Hook function

The sample

Jest provides global hook functions to do some preparatory or tidy work before or after a test. Common beforeEach, afterEach, beforeAll, afterAll, and so on. Here’s an example:

class MathFunc {
    constructor() {
        this.num = 0
    }
    add() {
        this.num = this.num + 1
    }
    sub() {
        this.num = this.num - 1
    }
}
const m = new MathFunc()

test('test function MathFunc add methods', () => {
    m.add()
    expect(m.num).toBe(1)
});

test('test function MathFunc add methods', () => {
    m.sub()
    expect(m.num).toBe(0)
});
Copy the code

You define a MathFunc class and then you instantiate it, test it, and you can think about why is it 0 and not -1? Because one instance M is shared and the attribute num is shared, the test is not correct. The two use cases interfere with each other. How to solve the problem? New instantiates side in every use case, okay? Sure, is there a better way? Yes, change it to the following:

let m = null

beforeEach(() => {
    m = new MathFunc()
});

test('test function MathFunc add methods', () => {
    m.add()
    expect(m.num).toBe(1)
});

test('test function MathFunc add methods', () => {
    m.sub()
    expect(m.num).toBe(-1)
});
Copy the code

scope

Scope of this concept is not a stranger, also have the concept in Jest, may test multiple modules more cases, the need for different preparation, directly in the above code executed each case needs to be written, affected or may affect efficiency, can according to different cases in different prepared only for local effect? You can use describe to describe groups that use different hook functions. Global hook functions are written in a global scope

beforeAll(() => console.log('1 - beforeAll'));
afterAll(() => console.log('1 - afterAll'));
beforeEach(() => console.log('1 - beforeEach'));
afterEach(() => console.log('1 - afterEach'));
test('', () => console.log('1 - test'));

describe('Scoped / Nested block', () => {
  beforeAll(() => console.log('2 - beforeAll'));
  afterAll(() => console.log('2 - afterAll'));
  beforeEach(() => console.log('2 - beforeEach'));
  afterEach(() => console.log('2 - afterEach'));
  test('', () => console.log('2 - test'));
});

// 1 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
// 2 - beforeAll
// 1 - beforeEach
// 2 - beforeEach
// 2 - test
// 2 - afterEach
// 1 - afterEach
// 2 - afterAll
// 1 - afterAll
Copy the code

Snapshot Snapshot test

While we’re testing, we’re testing UI components. How do we test them? Or what if we have some configuration files that we want to test? Of course, we can copy the contents of the copied files into a variable and compare them with the method introduced before. Then we have to change the configuration in two places, is it inconvenient? The __snapshots__ folder is generated for the first time, which stores the automatically generated Snapshot files. After the test, compare the Snapshot files to determine whether they pass the test.

Examples are as follows:

export const getConfig = () => { return { server: 'http://localhost', port: 8080, time: '2021.10.25'}} test('toMatchSnapshot', () => {expect(getConfig()). });Copy the code

A snapshot file named index.test.js.snap is generated

// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`toMatchSnapshot 1`] = ` Object { "port": 8080, "server": "http://localhost", "time": "2021.10.25",} ';Copy the code

When we test the getConfig function after modifying one of the items, the test results are as follows:

We can clearly see the difference, the test remind error, at this time the test terminal is not over, if it is really wrong is not what you want to modify the execution again; If it is what you want, press U to update the snapshot and the test case passes.

What if we had a field that changed dynamically? For example, change the getConfig function to the following:

export const getConfig = () => {
    return {
        server: 'http://localhost',
        port: 8080,
        time: new Date()
    }
}
Copy the code

What if the time property value of the returned object is never the same as that in the snapshot after each test? Updated? Updated next run or different? It’s not good, is it? What’s the solution? Pass the second parameter to toMatchSnapshot and process the return value, requiring only that the time attribute value is of the time type, as well as other types. After saving, an error will be reported. After updating by u, the use case will pass when running again

test('toMatchSnapshot', () => {
    expect(getConfig()).toMatchSnapshot({
        time: expect.any(Date)
    });
});
Copy the code

The question is, this is just the most basic case, what if we have multiple snapshots, how do we compare them? The method is the same, but there are some small cases. Here’s an example:

export const getConfig = () => { return { server: 'http://localhost', port: 8080, time: New Date()}} export const getAntherConfig = () => {return {server: '127.0.0.1', port: 8888, time: new Date() } } test('toMatchSnapshot-getConfig', () => { expect(getConfig()).toMatchSnapshot({ time: expect.any(Date) }); }); test('toMatchSnapshot-getAntherConfig', () => { expect(getAntherConfig()).toMatchSnapshot({ time: expect.any(Date) }); });Copy the code

Up at this time also can generate a snapshot files, then we changed some parameters of the two configurations, then run, predictable results, the test is not passed, according to comparing the results, according to u two snapshot will be updated at this time, but we may not see come over, if you more to print too much information, or have a plenty of what we want to update, there isn’t (is wrong), The operation would be completely updated, which is obviously not reasonable; So what to do? There is also an operation key I, press I to enter the comparison display one by one, as before, press U to update. You can try it, you can try other commands.

That’s all for today, and next time we’ll talk about mock data and jsDOM.