Part 4: 运用Jest测试Vue组件中的继承属性和自定义事件

Test Properties and Custom Events in Vue.js Components with Jest

运用Jest测试Vue组件中的继承属性和自定义事件

There are different ways to test properties, events and custom events.
测试Vue组件中的继承属性、事件和自定义事件有很多种方法。

Properties are custom attributes passed from parent to child components. Custom events solve just the opposite, they send data out to the direct parent via an event. They both combined are the wires of interaction and communication in Vue.js components.
继承属性是由父组件传递进子组件的属性数据。自定义事件恰恰相反,是子组件通过一个事件将属性数据传递给直接父组件。他们都将Vue的组件联系起来,达到数据传递的桥梁作用。

In Unit Testing, testing the in and outs (properties and custom events) means to test how a component behaves when it receives and sends out data in isolation. Let’s get our hands dirty!
在单元测试的范畴,测试数据接收与冒出(继承属性和自定义事件)意味着是测试组件在孤立的情况下,接收到和冒出数据时如何表现。

Properties

继承属性

When we are testing component properties, we can test how the component behave when we pass them certain properties. But before going on, an important note:
当我们测试组件的继承属性时,我们可以模拟给组件传递一个确定的数据属性,测试组件的行为表现。开工之前,给您一个重要提示:

To pass properties to components, use propsData, and not props. The last one is to define properties, not to pass them data.
给组件传递数据,可以用propsData,而不是正常组件中用的props,后者是用来定义属性的,而不是用来给属性传递数据。

First create a Message.test.js file and add the following code:
首先创建一个Message.test.js文件,代码如下:

describe('Message.test.js', () => {
  let cmp

  describe('Properties', () => {
    // @TODO
  })
})

We group test cases within a describe expression, and they can be nested. So we can use this strategy to group the tests for properties and events separately.
我们可以将测试用例归集到一个describe表达式里,且describe还可以嵌套describe。如此一来,我们就可以使用这个语法规则,将测试继承属性和自定义事件两组用例分隔成组。

Then we’ll create a helper factory function to create a message component, give some properties
然后我们来创建一个函数来帮助我们按需生成组件实例,并且传入一些属性。

const createCmp = propsData => mount(Message, { propsData })

Testing property existence

测试属性的存在与否

Two obvious things we can test is that a property exists, or it doesn’t. Remember that the Message.vue component has a message property, so let’s assert that it receives correctly that property. vue-test-utils comes with a hasProp(prop, value) function, which is very handy for this case:
很明显,我们是可以测试一个数据属性存在与否。Message.vue中有一个message属性,现在我们就可以断言以下是否直接收到了传递进来的属性值。vue-test-utils有一个api是hasProp(prop, value)函数,可以帮助我们方便进行检测并断言。

it('has a message property', () => {
  cmp = createCmp({ message: 'hey' })
  expect(cmp.hasProp('message', 'hey')).toBeTruthy()
})

The properties behave in a way that they will be received only if they’re declared in the component. Meaning that if we pass a property that is not defined, it won’t be received. So to check for the no existence of a property, use a non-existing property:
一个属性只有在组件中被声明了,才能被接受到传进的值,如果没声明,即使传入,那也接收不到。所以我们可以测试一下不存在是属性是否收到了传入的值:

it('has no cat property', () => {
  cmp = createCmp({ cat: 'hey' })
  expect(cmp.hasProp('cat', 'hey')).toBeFalsy()
})

However, in this case that test will fail because Vue has non-props attributes which sets it to the root of the Message component, thus being recognized as a prop and then the test will return true. Changing it to toBeTruty will make it pass for this example:
然而,在这个用例中测试会失败,因为Vue有non-props attributes特性,这个特性会将props中没声明的属性设置在根组件中,所以检测如果将message断言为props并检测其值,那么案例就会被通过。所以这个用例中可以修改如下,将成功通过测试。

it('has no cat property', () => {
  cmp = createCmp({ cat: 'hey' });
  expect(cmp.hasProp('cat', 'hey')).toBeTruthy()
})

We can test the default value as well. Go to Message.vue and change the props as follows:
我们还可以测试props中属性的默认值,代码作如下修改:

props: {
  message: String,
  author: {
    type: String,
    default: 'Paco'
  }
},

Then the test could be:
然后测试用例可以如下面这样写:

it('Paco is the default author', () => {
  cmp = createCmp({ message: 'hey' })
  expect(cmp.hasProp('author', 'Paco')).toBeTruthy()
})

Asserting properties validation

对属性的合法性进行检测

Properties can have validation rules, ensuring that a property is required or it is of a determined type. Let’s write the message property as follows:
继承属性有一个合法性的规则,能保证一个属性是否是必备或符合既定规则。现在我们编辑message的属性规则如下:

props: {
  message: {
    type: String,
    required: true,
    validator: message => message.length > 1
  }
}

Going further, you could use custom constructors types or custom validation rules, as you can see in the docs. Don’t do this right now, I’m just showing it as an example:
进一步的测试我们可以使用自定义构造函数的类型或自定义的验证规则,设置属性类型。以下是例子:

class Message {}
...
props: {
  message: {
    type: Message, // It's compared using instance of
    ...
    }
  }
}

Whenever a validation rule is not fulfilled, Vue shows a console.error. For example, for createCmp({ message: 1 }), the next error will be shown:
无论何时,一个验证规则如果不被满足,Vue就会显示一个打印错误。下面是错误信息示例。

[Vue warn]: Invalid prop: type check failed for prop "message". Expected String, got Number.
(found in <Root>)

By the date of writing, vue-test-utils doesn’t have any utility to test this. We could use jest.spyOn to test it:
迄今为止,vue-test-utils还没有相关工具可以测试这个错误,我们可以用jest.spyOn来搞定。

it('message is of type string', () => {
  let spy = jest.spyOn(console, 'error')

  cmp = createCmp({ message: 1 })

  expect(spy).toBeCalledWith(expect.stringContaining('[Vue warn]: Invalid prop'))

  spy.mockReset() // or mockRestore() to completely remove the mock
  // 或者用 mockRestore() 来完全移除mock数据
})

Here we’re spying on the console.error function, and checking that it shows a message containing a specific string. This is not an ideal way to check it, since we’re spying on global objects and relying on side effects.
上述代码暗中监视了console.error函数,检测了显示的信息中是否包含指定的字符串。这个不是一个理想的检测方法,因为我们用的这个方法监视了一个全局对象,并且对本想避免的副作用比较依赖。

Fortunately, there is an easier way to do it, which is by checking vm.options. Here’s where Vue stores the component options “expanded”. With expanded I mean: you can define your properties in a different ways: 幸运的是,我们还有一个更简单的方法 ——检测`vm.options对象实现测试目的。vm.$options`对象存储着组件的扩展选项。扩展选项的意思是你可以用一种另类的方式定义你的继承属性。

props: ['message']

// or

props: {
  message: String
}

// or

props: {
  message: {
    type: String
  }
}

But they all will end up in the most expanded object form (like the last one). So if we check the cmp.vm.option.props.message, for the first case, they all will be in the { type: X } format (although for the first example it will be { type: null}) 以上是三种定义类型的表达方式,但是他们在大部分扩展对象中都会已最后一种格式表示。我们索引到`cmp.vm.option.props.message时,会发现他们都会被格式化为{ type: X }的形式,不过第一种的结果是{ type: null}`。

With this in mind, we could write a test suite to test that asserts that the message property has the expected validation rules:
如此一来,所以我们就可以写个测试系列案例来断言信息的属性是否符合预期验证规则。

describe('Message.test.js', () => {
//   ...
  describe('Properties', () => {
    // ...
    describe('Validation', () => {
      const message = createCmp().vm.$options.props.message

      it('message is of type string', () => {
        expect(message.type).toBe(String)
      })

      it('message is required', () => {
        expect(message.required).toBeTruthy()
      })

      it('message has at least length 2', () => {
        expect(message.validator && message.validator('a')).toBeFalsy()
        expect(message.validator && message.validator('aa')).toBeTruthy()
      })
    })
  })
})

Custom Events

自定义事件

We can test at least two things in Custom Events:
我们至少可以测试自定义事件的两类情况:

  • Asserting that after an action an event gets triggered
    • 在一个action后断言事件被触发
  • Checking what an event listener calls when it gets triggered
    • 检测事件被触发时调用了什么方法

Which in the case of the MessageList.vue and Message.vue components example, that gets translated to:
对于MessageList.vueMessage.vue两个组件,以上两类相当于:

  • Assert that Message components triggers a message-clicked when a message gets clicked

    • 对Message组件断言:如果一项message被点击,会触发message-clicked事件
  • Check in MessageList that when a message-clicked happens, a handleMessageClick function is called

    • 检测MessageList组件中,当一项message被点击,handleMessageClick方法会被调用

First, go to Message.vue and use emit to trigger that custom event: 首先我们在Message组件中用emit来触发一个自定义事件

<template>
    <li
      style="margin-top: 10px"
      class="message"
      @click="handleClick">
        {{message}}
    </li>
</template>

<script>
  export default {
    name: 'Message',
    props: ['message'],
    methods: {
      handleClick() {
        this.$emit('message-clicked', this.message)
      }
    }
  }
</script>

And in MessageList.vue, handle the event using @message-clicked:
在父组件MessageList.vue中,我们用@message-clicked绑定handleMessageClick方法,完成闭环

<template>
    <ul>
        <Message
          @message-clicked="handleMessageClick"
          :message="message"
          v-for="message in messages"
          :key="message"/>
    </ul>
</template>

<script>
import Message from './Message'

export default {
  name: 'MessageList',
  props: ['messages'],
  methods: {
    handleMessageClick(message) {
      console.log(message)
    }
  },
  components: {
    Message
  }
}
</script>

Now it’s time to write a unit test. Create a nested describe within the test/Message.spec.js file and prepare the barebones of the test case “Assert that Message components triggers a message-clicked when a message gets clicked” that we mentioned before:
现在已经万事俱备,可以写单元测试了。在test/Message.spec.js文件中创建一个嵌套describe语句,并为之前提到的Assert that Message components triggers a message-clicked when a message gets clicked这个测试用例准备准系统:

// ...
describe('Message.test.js', () => {
  // ...
  describe('Events', () => {
    beforeEach(() => {
      cmp = createCmp({ message: 'Cat' })
    })

    it('calls handleClick when click on message', () => {
      // @TODO
    })
  })
})

Testing the Event Click calls a method handler

测试点击事件调用的处理方法

The first thing we can test is that when clicking a message, the handleClick function gets called. For that we can use a trigger of the wrapper component, and a jest spy using spyOn function:
首先,我们先测试点击单项信息后,处理方法被调用。此处我们把jest的spyOn方法赋给message的容器组件来监测触发动作:

it('calls handleClick when click on message', () => {
  const spy = spyOn(cmp.vm, 'handleClick')
  cmp.update() // Forces to re-render, applying changes on template

  const el = cmp.find('.message').trigger('click')
  expect(cmp.vm.handleClick).toBeCalled()
})

See the cmp.update()? When we change things that are used in the template, handleClick in this case, and we want the template to apply the changes, we need to use the update function.
注意到cmp.update()方法了吗?当模板内容有改动时,即处理方法成功被调用后,我们想让模板内容按预期有所改变,我们需要用cmp.update()方法。

Keep in mind that by using a spy the original method handleClick will be called. Probably you intentionally want that, but normally we want to avoid it and just check that on click the methods is indeed called. For that we can use a Jest Mock function:
要注意的是,我们预期只要检测到处理方法被成功调用就ok了,可是通常我们却是要避免如此笼统的检测,而是具体地检查方法是否被直接调用。如此一来,我们需要用到Jest的Mock函数:

it('calls handleClick when click on message', () => {
  cmp.vm.handleClick = jest.fn()
  cmp.update()

  const el = cmp.find('.message').trigger('click')
  expect(cmp.vm.handleClick).toBeCalled()
})

Here we’re totally replacing the handleClick method, accessible on the vm of the wrapper component returned by the mount function.
现在我们将处理方法替换掉,改为检测挂载方法返回的父组件实例上的vm对象。

We can make it even easier by using setMethods helper that the official tools provide us:
甚至我们再简单一些,直接用官方工具中的setMethods函数:

it('calls handleClick when click on message', () => {
  const stub = jest.fn()
  cmp.setMethods({ handleClick: stub })

  const el = cmp.find('.message').trigger('click')
  expect(stub).toBeCalled()
})

Using setMethods is the suggested way to do it, since is an abstraction that official tools give us in case the Vue internals change.
这里推荐使用setMethods方法做这类测试,因为这是官方工具提供给我们的抽象方法,就是用来测试Vue组件内部数据变化的方法。

Testing the Custom Event message-clicked is emitted

测试message-clicked自定义事件被分发

We’ve tested that the click method calls it’s handler, but we haven’t tested that the handler emits the message-clicked event itself. We can call directly the handleClick method, and use a Jest Mock function in combination with the Vue vm on method: 我们已经测试了点击事件后处理方法被调用,但仍然没有测到`message-clicked`被处理方法分发后的状况。我们可以直接调用处理方法,然后用jest的Mock方法结合Vue中vm的on方法来测试。

it('triggers a message-clicked event when a handleClick method is called', () => {
  const stub = jest.fn()
  cmp.vm.$on('message-clicked', stub)
  cmp.vm.handleClick()

  expect(stub).toBeCalledWith('Cat')
})

See that here we’re using toBeCalledWith so we can assert exactly which parameters we expect, making the test even more robust. Not that we’re not using cmp.update() here, since we’re making no changes that need to propagate to the template.
可以看到我们运用toBeCalledWith方法,精确断言了我们期望的参数,使得用例代码十分简洁清爽。之所以不用cmp.update()方法,是因为我们没有做什么需要模板重新渲染的数据变更。

Testing the @message-clicked triggers an event

测试@message-clicked触发的事件

For custom events, we cannot use the trigger method, since it’s just for DOM events. But, we can emit the event ourselves, by getting the Message component and using its vm.emit method. So add the following test to MessageList.test.js: 对于自定义事件,我们不能用触发事件的方法进行测试,因为那是基于DOM的测试方法。但是,我们可以通过Message组件的`vm.emit`方法分发事件。代码更改如下:

it('Calls handleMessageClick when @message-click happens', () => {
  const stub = jest.fn()
  cmp.setMethods({ handleMessageClick: stub })
  cmp.update()

  const el = cmp.find(Message).vm.$emit('message-clicked', 'cat')
  expect(stub).toBeCalledWith('cat')
})

I’ll leave up to you to test what handleMessageClicked does.
对handleMessageClicked的测试就不再一一赘述了。

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

推荐阅读更多精彩内容