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组件中为表单添加一个提交事件:

...
<form action="" @submit.prevent="onSubmit(inputValue)">
...

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.
这样就可以确保每个测试用例都会在开始的时候有一个清洁无污染的模拟依赖环境。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容