持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第18天,点击查看活动详情
单线程的执行机制使得异步代码在JavsScript中是经常出现的,因此对异步代码的测试也是很常见的一个需求。当运行异步代码时,Jest 需要知道它当前测试的异步代码什么时候完成,之后才可以运行下一个测试。针对不同的异步场景,Jest 有多种办法来处理这种情况。
我们都知道在JavaScript中典型的异步模式有回调函数、Promise、Async/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");
});
});
测试用例通过了,但这样其实是有问题的。
我们定时器里设置的是2s后执行回调,这里用例的执行时间是8ms,说明真正的测试用例在回调之前就已经结束了!事实上我们在测试用例中写任何东西都会通过。Jest中解决这个问题的方法是从 test 的第二个参数中传入一个参数 done ,告诉它,这个传入的done()函数执行完了,你这个test才算执行完,这样就可以保证我们的回调被执行了。
import { asyncFunction } from "@/utils/data";
test("setTimeout callback", (done) => {
asyncFunction((msg) => {
expect(msg).toBe("A Message");
done();
});
});
实际开发中,我们遇到最多的异步代码应该就是axios了,下面再多演示一个用axios发送网络请求的测试用例,也为后面的测试预热。使用的API来自JSONPlaceholder,内容如下:
// 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的方式友好很多。只需要在测试中返回一个Promise,Jest会等待Promise完成再进行校验,如果Promise被reject,则测试失败。
下面我们在被测函数中直接返回一个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);
});
});
可以看到运行测试用例之后是通过的,因为返回的状态码就是404,如果我们将断言的内容改为expect(err.response.status).toBe(100),测试用例便不会通过。
但上面的测试用例中其实存在一个隐藏的问题:还是上面的测试用例,这次我们把接口改回正确的,重复的代码就省略了,下面只是作改动的部分:
export const fetchDataPromise = () => {
return axios.get('https://jsonholder.typicode.com/posts/1');
};
竟然还是通过?!结合第一部分回调函数的测试,想来你也知道是什么原因了:测试用例使用了catch方法,也就是说只有Promise被reject时才会走这个方法,而现在因为接口是正常的,所以返回的是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);
});
});
如我们所愿,这时候测试用例就无法正常通过测试了,我们需要将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);
}
});
相关资料: