给后端开发者的 Vue.js 组件测试指南

Vue.js

作为一个研究 Vue 开发领域的后端开发者,我很快意识到单元测试对于个人产品的成功异常重要。就像 UI 开发者总喜欢打地鼠一样,修复一个问题后之前的问题又重现了。

一开始,我曾认为组件测试意味着我必须把 DOM 输出与 属性、槽、触发器、数据源等形成的组合进行比较。不过在编写用于组件 HTML 校验测试的代码时,发现这样的测试会比较脆弱和笨拙,并且不利于代码库的快速迭代。于是,我把目标从整个 HTML 输出检测转移到了基于组件的行为输入检测上,这个结果更具针对性。

我的设置

您不一定需要通过了解如下工具进而理解文章中的概念,不过对我来说,了解辅助细节有助于大局的理解。

一个超简单的例子

让我们从一个 Vue 组件开始,该组件向用户展示一条信息:

message1.vue

<template>
  <div>{{ message }}</div>
</template>
<script>
export default {
   name: 'MyComponent',
   props: ['message']
}
</script>

理解下这个例子,看上去需要验证的是信息被准确的渲染在模板上,所以测试用例写出来可能像这样:

test1.vue

it('should display the message', () => {
  const message = 'foo'
  cmp = mount(MyComponent, {
    propsData: {message}
  })
  expect(cmp.text()).to.equal(message)
})

不过要是模板变为如下所示的内容,改怎么办呢?

<template>
  <div style="background-color: green">
    <p>{{ message }}</p>
  </div>
</template>

即便模板已修改,测试的基本理念也不变。message 字段的意义,就是在组件内部准确的展示,并且该目的在此版本和之前版本之间并未真正改变。

不过 background-color: green 怎么办呢?是否应该使用组件测试来验证呢?在此特定情况下,不会,因为这个值是模板内的静态配置。组件内也没有正式定义更改此值的方法,因此这对组件本身的有效性来说并不重要。如果删除了多余的代码,所剩结果如下:

Vue.js

这与之前的组件描述相同。

带同级元素的组件

之前的组件是为用户显示一个消息,但是如果组件稍微复杂一些呢?

message2.vue

<template>
<div>
  <p>This is my demo!</p>
  <p>{{ message }}</p>
</div>
</template>

由于根组件的内部现在变成 'This is my demo!'+ {{ some whitespace }} + message ,所以必须修改测试。像我之前提到的,静态数据与 prop 的行为没有关系。缺乏经验的人会这样重写测试:

it('should display the message', () => {
  const message = 'foo'
  cmp = mount(MyComponent, {
    propsData: {message}
  })
  const ps = cmp.findAll('p')
  expect(ps.at(1).text()).to.equal(message)
})

尽管这个测试可以工作,但是太过于依赖组件的细节。如果 message 的标签修改为 div 或者 span 或者它不是首个索引,你就需要重写测试。一个可以改进的地方是,为组件分配一个标识符(像 ref 或者 id),然后在测试里面引用标识。这样一来,你的模板修改成:

<template>
<div>
  <p>This is my demo!</p>
  <p ref="messageField">{{ message }}</p>
</div>
</template>

测试修改成这样:

it('should display the message', () => {
  const message = 'foo'
  cmp = mount(MyComponent, {
    propsData: {message}
  })
  const field = cmp.findComponent({ref: 'messageField'})
  expect(field.text()).to.equal(message)
})

这样,不管模板如何迭代, messageField 标签就会总是指向 message prop 的值。

组件样式的修改

如果背景颜色是根据组件的一个行为改变呢?

message4.vue

<template>
  <div :style="success?'background-color: green':''">
    <p>{{ message }}</p>
  </div>
</template>
<script>
export default {
  name: 'MyComponent',
  props: ['message', 'success']
}
</script>

success prop 的真假决定 background-color 的值。因此,需要为这个具体的行为写更多测试进行验证。

test4.vue

it('should set the color if success is set', () => {
  cmp = mount(MyComponent, {
    propsData: { success: true }
  })
  expect(cmp.element.style.backgroundColor).to.equal('green')
})
it('should not set the color if success is not set', () => {
  cmp = mount(MyComponent, {
    propsData: {}
  })
  expect(cmp.element.style.backgroundColor).to.be.undefined
})

我们注意到,尽管组件中有一个 message prop ,但在这些测试中并没有描述和提及到。为什么?因为组件颜色有没有修改都不会影响到 message 。换个思路想,作为一个 Vue 组件,在类中是如何描述成一个对象的:

class MyComponentTheClass {
  constructor(message, success) {
    this.value = message
    this.color = success?'green':''
  }
  get message() {
    return this.message
  }
  set message(v) {
    this.message = message
  }
  get success() {
    return this.color
  }
  set success(success) {
    this.color = success?'green':''
  }
}

如果你要测试 success 的返回值,就会去设置和调用 .success 。设置和取得 .message 的值并不会影响到 .success 的结果,要单独对它进行验证。

更进一步,除了要把元素的背景设置为绿色,还要让 message 的文本添加下划线。模板如下:

class MyComponentTheClass {
  constructor(message, success) {
    this.value = message
    this.color = success?'green':''
  }
  get message() {
    return this.message
  }
  set message(v) {
    this.message = message
  }
  get success() {
    return this.color
  }
  set success(success) {
    this.color = success?'green':''
  }
}

有可能写测试验证 div 元素的样式然后验证 p 元素的样式。但如果有更多的样式进行添加和修改呢?这会变得麻烦,按顺序测试会让测试变得脆弱。总的来说,组件测试并不关注组件看起来如何,而是关注它如何工作。因此,从功能上看,本实现方式等同于如下代码。

background-color.vue

<template>
  <div :class="success:'success-me':''">
    <p>{{ message }}</p>
  </div>
</template>
<style scoped>
.success-me {
  background-color: green;
}
.success-me p {
  text-decoration: underline;
}
</style>

然后测试会是这样:

test5.vue

it('should render success if set', () => {
  cmp = mount(MyComponent, {
    propsData: { success: true }
  })
  expect(cmp.classes()).to.include('success-me')
})
it('should not render success if not set', () => {
  cmp = mount(MyComponent, {
    propsData: {}
  })
  expect(cmp.classes()).to.not.include('success-me')
})

如果你对样式表和我一样不熟悉,甚至可以这样写模板:

message6.vue

<template>
  <div 
    v-if="success"
    class="success-me"
    style="background-color: green"
  >
    <p :style="text-decoration: underline">
      {{ message }}
    </p>
  </div>
  <div v-else>
    <p>{{ message }}</p>
  </div>
</template>

测试仍然会正常工作,因为 success prop 的设置主要影响的是 success-me CSS 类。

组件内部的组件

在测试导入了其他组建的组件时,经验法则是假设子组件一定会完美的运行。父组件并不需要对子组件进行测试,子组件的测试应该在其单元测试中完成了。父组件的目标是验证是否将正确的信息传递给子组件,以及是否合适的处理子组件触发的事件。

another-template.vue

<template>
  <div>
    <p>
      There are <span>{{ count }}</span> whatevers going on now.
    </p>
    <MyCoolWhatever :blah="message" @whatevs="count += 1" />
  </div>
</template>
<script>
export default {
  name: 'MyCoolParent',
  props: ['message'],
  data: () => ({ count: 0 })
}
</script>

这里并没有描述 MyCoolWhatever 代表了什么,因为这在 MyCoolParent 组件的上下文中无关紧要。重要的是 MyCoolParent 如何与本组件交互以渲染其模板。

MyCoolParent.vue

it('should provide the message', () => {
  const message = 'foo'
  cmp = mount(MyCoolParent, {propsData: {message}})
  const whatev = cmp.findComponent({name: 'MyCoolWhatever'})
  expect(whatev.props().blah).to.equal(message)
})
it('should increment the count', async () => {
  cmp = mount(MyCoolParent, {})
  const whatev = cmp.findComponent({name: 'MyCoolWhatever'})
  const count = cmp.find('span')
  expect(count.text()).to.equal('0')
  await whatev.vm.$emit('whatevs')
  expect(count.text()).to.equal('1')
})

如果子组件是静态渲染的,则之前的规则也适用于此。如果 blash 属性设置为硬编码的值,或者触发器并未绑定该组件,则无需编写单元测试去验证该部分模板。

组件的分解

现在我们需要知道应该在什么时候把组件分解为更小,更易于管理的块儿。解决此问题的一个方法是,统计下描述这个组件时使用 这个词数量。如果超过两次,则可能需要你考虑下分解组件了。

  • 用一句话描述这个组件的功能。如果不使用 一词就无法描述其目的,则每一个字句都代表一个潜在的子组件。

该组件的目的是展示一个工具栏(1)和左侧导航栏(2)和主要内容(3)。

  • 下一步是描述组件如何实现其目的。同样的,如果不使用 一词就无法描述其实现,则每一个字句都代表一个潜在的子组件。

该组件显示每周的日历。通过显示星期几(1)和一天中的小时(2)和计划的事件(3)以及假期(4)

通过使组件更小,可以减少测试面,从而限制了潜在的复杂错误并通过更少的测试用例数量提升代码覆盖率。

组件单元测试与输入匹配 HTML 无关。也不涉及验证独立方法调用或内部状态改变。这是针对每个组件的预期行为做出的针对性决策,用以提升灵活性、稳定性以及最重要的是,节约时间。

感谢阅读!

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://medium.com/better-programming/a-...

译文地址:https://learnku.com/vuejs/t/52914

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!