我们应该在方法上测试什么?这是我们开始进行单元测试时遇到的一个问题。一切都归结为测试该方法的功能,只有。这意味着我们需要避免调用任何依赖项,因此我们需要模拟它们。
让我们在上一章中创建的Form.vue
组件的表单中添加一个submit
事件:
<form @submit.prevent="onSubmit(inputValue)"></form>
.prevent
修饰符只是调用event.preventDefault()
的一种方便方式,这样它就不会重新加载页面。现在,进行一些修改以调用 API,然后通过向数据中添加一个results
数组和一个onSubmit
方法来存储结果:
export default {
data: () => ({
inputValue: "",
results: []
}),
methods: {
onSubmit(value) {
axios
.get("https://jsonplaceholder.typicode.com/posts?q=" + value)
.then(results => {
this.results = results.data;
});
}
}
};
这里,该方法是使用axios
对jsonplaceholder
的posts端点执行 HTTP 调用,这只是此类示例的 RESTful API。此外,通过q
查询参数,我们可以使用提供的value
作为参数来搜索帖子。
对于onSubmit
方法的测试:
- 我们不想调用
axios.get
实际方法。 - 我们想检查它是否正在调用 axios(但不是真正的 axios),以及它是否返回了一个
promise
。 promise
回调应该将this.results
设置为承诺的结果。
当您有外部依赖项,再加上那些在内部做事情的返回承诺时,这可能是最难测试的事情之一。我们需要做的是模拟外部依赖关系。
Jest 提供了一个非常好的模拟系统,它允许您以非常方便的方式模拟所有内容。事实上,您不需要任何额外的库来完成这项工作。我们已经看到了jest.spyOn
和jest.fn
用于监视和创建存根函数,尽管这对于本例来说还不够。
在这里,我们需要模拟整个axios
模块。这就是jest.mock
发挥作用的地方。通过在文件顶部写入以下内容,我们可以轻松模拟模块依赖关系:
jest.mock("dependency-path", implementationFunction);
您必须知道jest.mock
被提升,这意味着它将被放置在顶部:
jest.mock("something", jest.fn);
import foo from "bar";
// ...
因此,前面的代码相当于:
import foo from "bar";
jest.mock("something", jest.fn); // this will end up above all imports and everything
// ...
在写这篇文章的时候,我还没有找到很多关于如何在互联网上开玩笑的信息。幸运的是,你不必经历同样的挣扎。
让我们在Form.test.js
测试文件的顶部为axios
编写模拟,并编写相应的测试用例:
jest.mock("axios", () => ({
get: jest.fn()
}));
import { shallowMount } from "@vue/test-utils";
import Form from "../src/components/Form";
import axios from "axios"; // axios here is the mock from above!
// ...
it("Calls axios.get", () => {
cmp.vm.onSubmit("an");
expect(axios.get).toBeCalledWith(
"https://jsonplaceholder.typicode.com/posts?q=an"
);
});
这太棒了。我们确实在模仿axios
,所以没有调用原始 axios,也没有调用任何 HTTP。我们甚至通过使用toBeCalledWith
来检查它是否使用了正确的参数调用。然而,我们仍然缺少一些东西:我们没有检查它是否返回promise
。
首先,我们需要使 mockaxios.get
方法返回一个promise
。jest.fn
接受工厂函数作为参数,我们可以用它来定义它的实现:
jest.mock("axios", () => ({
get: jest.fn(() => Promise.resolve({ data: 3 }))
}));
但是,我们仍然无法访问promise
,因为我们不会返回它。在测试中,在可能的情况下从函数返回某些内容是一种很好的做法,因为这会使测试更加容易。那么,现在让我们在Form.vue
组件的onSubmit
方法中执行此操作:
export default {
methods: {
// ...
onSubmit(value) {
const getPromise = axios.get(
"https://jsonplaceholder.typicode.com/posts?q=" + value
);
getPromise.then(results => {
this.results = results.data;
});
return getPromise;
}
}
};
然后,我们可以在测试中使用非常干净的 ES2017async/await
语法来检查承诺结果:
it("Calls axios.get and checks promise result", async () => {
const result = await cmp.vm.onSubmit("an");
expect(result).toEqual({ data: [3] });
expect(cmp.vm.results).toEqual([3]);
expect(axios.get).toBeCalledWith(
"https://jsonplaceholder.typicode.com/posts?q=an"
);
});
在这里,您可以看到,我们不仅检查承诺的结果,而且还按照预期,通过执行expect(cmp.vm.results).toEqual([3])
更新组件的results
内部状态。
Jest 允许我们通过将所有模拟放在__mocks__
文件夹下并保持测试尽可能干净,将它们分离到自己的 JavaScript 文件中。
因此,我们可以将Form.test.js
文件顶部的jest.mock...
块取出到它自己的文件中:
// test/__mocks__/axios.js
module.exports = {
get: jest.fn(() => Promise.resolve({ data: [3] }))
};
就像这样,Jest 不需要额外的努力就可以在我们所有的测试中自动应用模拟,这样我们就不需要做任何额外的事情,或者在每个测试中手动模拟它。请注意,模块名称必须与文件名匹配。如果再次运行测试,它们仍应通过。
请记住,模块注册表和模拟状态保持原样,因此,如果您在之后编写另一个测试,可能会得到不希望的结果:
it("Calls axios.get", async () => {
const result = await cmp.vm.onSubmit("an");
expect(result).toEqual({ data: [3] });
expect(cmp.vm.results).toEqual([3]);
expect(axios.get).toBeCalledWith(
"https://jsonplaceholder.typicode.com/posts?q=an"
);
});
it("Axios should not be called here", () => {
expect(axios.get).toBeCalledWith(
"https://jsonplaceholder.typicode.com/posts?q=an"
);
});
第二次测试应该失败,但是没有。那是因为axios.get
之前在测试中被调用过。
出于这个原因,清理模块注册表和 mock 是一个很好的实践,因为它们是由 Jest 操纵的,以便进行 mock。为此,您可以添加您的beforeEach
:
beforeEach(() => {
cmp = shallowMount(Form);
jest.resetModules();
jest.clearAllMocks();
});
这将确保每个测试都从干净的模拟和模块开始,就像在单元测试中一样。
Jest 的模拟功能和快照测试是我最喜欢的两件事。这是因为它们使通常很难测试的东西变得非常容易,允许您专注于编写更快、更好的独立测试,并保持代码库的防弹性。