Jest 中的异步测试

576 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第18天,点击查看活动详情

单线程的执行机制使得异步代码在JavsScript中是经常出现的,因此对异步代码的测试也是很常见的一个需求。当运行异步代码时,Jest 需要知道它当前测试的异步代码什么时候完成,之后才可以运行下一个测试。针对不同的异步场景,Jest 有多种办法来处理这种情况。

我们都知道在JavaScript中典型的异步模式有回调函数PromiseAsync/Await等,下面针对这三种类型我们来看一下如何分别使用Jest来测试你的异步函数

回调函数

如下代码,在定时器中放置了一个callback回调函数,默认2s后执行

export const asyncFunction = (callback, ms = 2000) => {
  setTimeout(() => {
    callback("A Message");
  }, ms);
};

按照我们正常书写单元测试的思路,测试用例应该是这样:

import { asyncFunction, fetchData } from "@/utils/data";

test("setTimeout callback", () => {
  asyncFunction((msg) => {
    expect(msg).toBe("A Message");
  });
});

image.png

测试用例通过了,但这样其实是有问题的。

我们定时器里设置的是2s后执行回调,这里用例的执行时间是8ms,说明真正的测试用例在回调之前就已经结束了!事实上我们在测试用例中写任何东西都会通过。Jest中解决这个问题的方法是从 test 的第二个参数中传入一个参数 done ,告诉它,这个传入的done()函数执行完了,你这个test才算执行完,这样就可以保证我们的回调被执行了。

import { asyncFunction } from "@/utils/data";

test("setTimeout callback", (done) => {
  asyncFunction((msg) => {
    expect(msg).toBe("A Message");
    done();
  });
});

image.png

实际开发中,我们遇到最多的异步代码应该就是axios了,下面再多演示一个用axios发送网络请求的测试用例,也为后面的测试预热。使用的API来自JSONPlaceholder,内容如下:

image.png

// src/utils/data.js
export const fetchData = fn => {
    axios.get('https://jsonplaceholder.typicode.com/posts/1').then(res => {
        fn(res.data);
    });
};

// tests/unit/data.spec.js
test("axios callback", (done) => {
  fetchData((data) => {
    expect(data.id).toBe(1);
    done();
  });
});

上面的代码中,我们定义了一个fetchData函数用于发送GET请求,它接受一个回调函数用于处理返回的数据;测试用例中则对返回数据的id属性进行了断言。

Promise

相比于回调函数,Jest处理Promise的方式友好很多。只需要在测试中返回一个PromiseJest会等待Promise完成再进行校验,如果Promisereject,则测试失败。

下面我们在被测函数中直接返回一个Promise,这样的形式在工作中也是经常使用的。

export const fetchDataPromise = () => {
    return axios.get('https://jsonplaceholder.typicode.com/posts/1');
};

然后就可以在测试用例中直接使用Promise对象了

import { fetchDataPromise } from "@/utils/data";

test('return promise', () => {
    return fetchDataPromise().then(res => {
        expect(res.data.id).toBe(1);
    });
});

注意:一定要使用return一个Promise,否则测试会在异步的回调发起之后立刻结束

如果Promise的结果是reject(比如404了),就需要在.catch方法中进行断言,这里为了演示,我们将API稍作改动,改成没有这个接口

// src/utils/data.js
export const fetchDataPromise = () => {
    return axios.get('https://jsonholder.typicode.com/posts/200');
};

// tests/unit/data.spec.js
import { fetchDataPromise } from "@/utils/data";

test("return rejected-promise", () => {
  return fetchDataPromise().catch((err) => {
    expect(err.response.status).toBe(404);
  });
});

image.png

可以看到运行测试用例之后是通过的,因为返回的状态码就是404,如果我们将断言的内容改为expect(err.response.status).toBe(100),测试用例便不会通过。

但上面的测试用例中其实存在一个隐藏的问题:还是上面的测试用例,这次我们把接口改回正确的,重复的代码就省略了,下面只是作改动的部分:

export const fetchDataPromise = () => {
    return axios.get('https://jsonholder.typicode.com/posts/1');
};

image.png

竟然还是通过?!结合第一部分回调函数的测试,想来你也知道是什么原因了:测试用例使用了catch方法,也就是说只有Promisereject时才会走这个方法,而现在因为接口是正常的,所以返回的是resolve状态的Promise,也就不会走这个测试方法,因此expect()压根就没有执行,Jest默认这个用例通过了测试。但我们并不希望如此。

解决的方法是使用expect.assertions(1),它表示的意思是必须执行一次expect方法才可以通过测试,在这里也就是说catch那里没有被执行,所以正确的测试用例应该是:

test("return rejected-promise", () => {
  expect.assertions(1);
  return fetchDataPromise().catch((err) => {
    expect(err.response.status).toBe(404);
  });
});

image.png

如我们所愿,这时候测试用例就无法正常通过测试了,我们需要将API改成错误的地址,才能通过测试。

上面写异步测试用例时我们使用的是return的形式,还有另一种方法,我们可以使用 async 将整个测试用例包成一个Promise,在其中就能实现异步方法的同步化:

// src/utils/data.js
export const fetchDataSucc = () => {
  return axios.get("https://jsonplaceholder.typicode.com/posts/1");
};

export const fetchDataFail = () => {
  return axios.get("https://jsonplaceholder.typicode.com/posts/200"); // 接口不存在
};

// tests/unit/data.spec.js
import { fetchDataSucc, fetchDataFail } from "@/utils/data";

test("test fetchDataSucc", async () => {
  const res = await fetchDataSucc();
  expect(res.data.id).toBe(1);
});

test("test fetchDataFail", async () => {
  expect.assertions(1);
  try {
    await fetchDataFail();
  } catch (err) {
    expect(err.response.status).toBe(404);
  }
});

image.png

相关资料:

jestjs.io/docs/asynch…

异步代码的测试方法

www.jianshu.com/p/20fb4199e…