Part 6: 用Jest测试Vue中的Methods中的方法和Mock依赖

Test Methods and Mock Dependencies in Vue.js with Jest

用Jest测试Vue中的Methods中的方法和Mock依赖

Learn how to test methods and cope with mocking module dependencies.
学习如何测试方法并处理模拟模块的依赖关系。

What should we test in methods? That’s a question that we had when we started doing unit tests. Everything comes down to test what that method do, and just that. This means we need to avoid calls to any dependency, so we’ll need to mock them.
哪些是我们在methods中应该测试的呢?在我们刚着手单测的时候是个问题。其实测试方法无非就是测它做了什么。这意味着我们需要避免调用其它依赖,所以我们需要模拟出来。

Let’s add a onSubmit event to the form in the Form.vue component that we created in the last article:
我们在上文中创建的Form组件中为表单添加一个提交事件:

...
...

The .prevent modifier is just a convenient way to call event.preventDefault() in order to don’t reload the page. Now make some modifications to call an api and store the result, by adding a results array to the data and a onSubmit method:
.prevent是调用event.preventDefault()的语法糖,为的就是不重载页面。现在我们可以做些修改,在data中添加一个数组,methods中添加一个onSubmit方法来调用一个api接口,然后将返回结果赋给数组。

data: () => ({
  inputValue: '',
  results: []
}),
methods: {
  onSubmit(value) {
    axios.get('https://jsonplaceholder.typicode.com/posts?q=' + value).then(results => {
      this.results = results.data
    })
  }
},
...

The method is using axios to perform an HTTP call to the “posts” endpoint of jsonplaceholder, which is just a RESTful API for this kind of examples, and with the q query parameter we can search for posts, using the value provided as parameter.
onSubmit方法运用了axios工具,来对https://jsonplaceholder.typicode.com/posts进行了一次HTTP请求,这只是个api的测试示例,通过url拼接参数,来取得对应的返回结果。

For testing the onSubmit method:

对于测试onSubmit方法来说:

  • We don’t wanna call axios.get actual method

    • 我们不想真正调用axios的get方法
    • We wanna check it is calling axios (but not the real one) and it returns a promise
      • 我们只是想验证onSubmit方法调用了axios,而且axios返回了一个promise对象。
  • That promise callback should set this.results to the promise result

    • 这个promise的回调函数应该将promise的返回结果赋值给this.results
    • This is probably one of the hardest things to test, when you have external dependencies plus those return promises that do things inside. What we need to do is to mock the external dependencies.
      • 这次应该是我们测试用例中最难的一个了,我们有外部环境依赖时可以依据能返回的promise对象们在组件内做很多事。我们现在需要做的就是要模拟这些外部依赖。

Mock External Module Dependencies

模拟外部模块的依赖

Jest provides a really great mocking system that allows you to mock everything in a quite convenient way. You don’t need any extra libraries for that. We have seen already jest.spyOn and jest.fn for spying and creating stub functions, although that’s not enough for this case.
Jest提供给我们一套超级棒的mock系统,可以让我们轻松方便地模拟任何事物。从而不再需要引入其他类库来做这种事情。我们已经见识到了运jest.spyOn和jest.fn方法来监测并创建stub函数,然而这些对我们的测试用例来所还不够。

We need to mock the whole axios module. Here’s where jest.mock comes into the stage. It allow us to easily mock module dependencies by writing at the top of you file:
我们需要模拟整个axios依赖模块。这里我们就能看到jest.mock大放光彩了!它可以让我们轻易模拟依赖的模块,只需要再文件头部写如下代码:
jest.mock('dependency-path', implementationFunction)

You must know that jest.mock is hoisted, which means it will be placed at the top. So:
有一点要注意,jest.mock需要写在文件顶部。

jest.mock('something', jest.fn)
import foo from 'bar'
...

Is equivalent to:
以上写法等同于:

import foo from 'bar'
jest.mock('something', jest.fn) // this will end up above all imports and everything
...

By the date of writing, I still haven’t seen much info about how to do in Jest what we’re gonna do here on the internet. Lucky you don’t have to go through the same struggle.
行文时为止,我始终没有见过网络上有我们接下来要讲的知识的相关文章。你们不必向我当初那样纠结了。

Let’s write the mock for axios at the top of the Form.test.js test file, and the corresponding test case:
现在在文件顶部开始模拟axios模块,并准备相应的用例:

jest.mock('axios', () => ({
  get: jest.fn()
}))

import { shallow } 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')
})

This is great, we’re indeed mocking axios, so the original axios is not called nor any HTTP call. And, we’re even checking by using toBeCalledWith that it’s been called with the right parameters. But we’re still missing something: we’re not checking that it returns a promise.
这样太棒了,我们确实模拟了axios,所以真实的axios就不会被调用来发起HTTP请求。而且,我们甚至用toBeCalledWith验证了axios会被传入参数并调用。但是我们仍然遗漏了某些事情:我们没有检查返回的promise。

First we need to make our mocked axios.get method to return a promise. jest.fn accepts a factory function as a parameter, so we can use it to define its implementation:
首先,我们需要我们模拟的axios.get方法来返回一个promise对象,jest.fn接受一个工厂函数来作为参数,使得我们可以用它来定义执行条件:

jest.mock('axios', () => ({
  get: jest.fn(() => Promise.resolve({ data: 3 }))
}))

But still, we cannot access the promise, because we’re not returning it. In testing, is a good practice to return something from a function when possible, it makes testing much easier. Let’s do it then in the onSubmit method of the Form.vue component:
但是如此还不行,我们依然拿不到promise对象,因为我们没有返回它。在测试中,我们要尽量做到能让一个函数返回数据,这样可以让测试更简单。接下来我们就实际运用它:

onSubmit(value) {
  const getPromise = axios.get('https://jsonplaceholder.typicode.com/posts?q=' + value)

  getPromise.then(results => {
    this.results = results.data
  })

  return getPromise
}

Then we can use the very clean ES2017 async/await syntax in the test to check the promise result:
这样一来我们就可以用ES6中非常简洁的async/await、syntax方法,在测试中来检测promise的结果:

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')
})

You can see that we don’t only check the promise result, but also that the results internal state of the component is updated as expected, by doing expect(cmp.vm.results).toEqual([3]).
你可以看到我们不只是检查了proise的结果,还验证了结果内部的数据在组件中已经被如期更新,正如断言表达式expect(cmp.vm.results).toEqual([3]).那样。

Keep mocks externalized

保证模拟的模块被隔离

Jest allows us to have all our mocks separated in their own JavaScript file, placing them under a mocks folder, keeping the tests as clean as possible.
Jest允许我们模拟的依赖模块与真实的模块代码隔离,将其放置在mocks文件夹下,保证了依赖的清洁。

So we can take the jest.mock... block from top of the Form.test.js file out to it’s own file:
所以我们可以将 jest.mock... 相关模拟数据放在mocks内axios.js中:

// test/__mocks__/axios.js
module.exports = {
  get: jest.fn(() => Promise.resolve({ data: [3] }))
}

Just like this, with no extra effort, Jest automatically applies the mock in all our tests so we don’t have to do anything extra or mocking it in every test manually. Notice the module name must match the file name. If you run the tests again, they should still pass.
这样依赖,不需要其他的额外模拟操作,Jest自动地就为我们所有的测试应用模拟数据,此后就不用每次手动操作了。注意到我们的模块名依然与文件名匹配。如果你执行测试命令,它们都将完美通过测试。

Keep in mind the modules registry and the mocks state is kept, so if you write another test afterwards, you may get undesired results:
要注意的是模拟的模块和数据的状态会一直保持,所以如果你接下来还要写其他测试用例,那么结果就不是你想要的了:

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')
})

The second test should fail, but it doesn’t! That’s because axios.get was called on the test before.
第二个用例本来应该报错的,却被通过了测试。这是因为axios.get之前测试中被调用过了。

For that reason, it’s a good practice to clean the module registry and the mocks, since they’re manipulated by Jest in order to make mocking happen. For that you can add in your beforeEach:
因为这个原因,我们直接清空被注册的模拟模块就好了,下面的代码可以添加在beforeEach函数里。

beforeEach(() => {
  cmp = shallow(Form)
  jest.resetModules()
  jest.clearAllMocks()
})

That will ensure each test starts with clean mocks and modules, as it should be in unit testing.
这样就可以确保每个测试用例都会在开始的时候有一个清洁无污染的模拟依赖环境。

你可能感兴趣的:(Part 6: 用Jest测试Vue中的Methods中的方法和Mock依赖)