在 TDD 期间模拟 Vue 实例上的方法

Mocking methods on a Vue instance during TDD

我在构建我的 Vue 应用程序的同时学习 TDD,并努力遵守严格的法律,即只编写足够的生产代码来满足失败的单元测试。我真的很喜欢这种方法,但是我 运行 在向 Vue 实例添加方法以及测试当事件从模板中的元素触发时是否调用它们方面遇到了障碍。

我找不到关于如何模拟 Vue 方法的任何建议,因为如果我模拟代理方法,它最终不会被调用(我正在使用 Jest 和 Vue Test Utils)。

我也在使用 Cypress,所以我可以在 e2e 中填写这个测试,但我希望能够尽可能多地覆盖单元测试。

我拥有 Edd Yerburgh 的《测试 Vue.js 应用程序》一书,但在有关测试组件方法的部分中,他简单地陈述了以下内容:

Often, components use methods internally. For example, to log to the console when a button is clicked [...] You can think of these as private methods—they aren’t intended to be used outside of the component. Private methods are implementation details, so you don’t write tests for them directly.

这种方法显然不允许遵循更严格的 TDD 法则,那么 TDD 纯粹主义者如何处理这个问题?

ButtonComponent.vue

<template>
    <button @click="method">Click me</button>
</template>

<script>
    export default: {
        methods: {
            method () {
                // Have I been called?
            }
        }
    }
</script>

ButtonComponent.spec.js

it('will call the method when clicked', () => {
    const wrapper = shallowMount(ButtonComponent)
    const mockMethod = jest.fn()
    wrapper.vm.method = mockMethod

    const button = wrapper.find('button')
    button.vm.$emit('click')

    expect(mockMethod).toHaveBeenCalled()
    // Expected mock function to have been called, but it was not called
})

解决方案 1:jest.spyOn(Component.methods, 'METHOD_NAME')

你可以在挂载前使用jest.spyOn模拟组件方法:

import MyComponent from '@/components/MyComponent.vue'

describe('MyComponent', () => {
  it('click does something', async () => {
    const mockMethod = jest.spyOn(MyComponent.methods, 'doSomething')
    await shallowMount(MyComponent).find('button').trigger('click')
    expect(mockMethod).toHaveBeenCalled()
  })
})

解决方案 2:将方法移动到可以模拟的单独文件中

官方recommendation is to "abstract the hard parts away", and use Jest's various mocking mechanisms模拟被测组件调用的抽象模块

例如,要验证调用了 click-处理程序:

  1. click-handler 的正文移动到共享的 JavaScript 文件中。
  2. 将共享模块导入被测组件和您的测试中(确保在这两种情况下使用相同的导入路径)。
  3. 调用jest.mock()模拟共享模块的导出函数。
  4. 重置测试套件 beforeEach() 中的模拟。这可能只有在套件中有多个测试时才有必要。
// @/components/MyComponent/utils.js
export function doSomething() { /*...*/ } //1️⃣

// @/components/MyComponent/MyComponent.vue (<script>)
import { doSomething } from '@/components/MyComponent/utils' //2️⃣

export default {
  methods: {
    onClick() {
      doSomething() //1️⃣
    }
  }
}

// @/test/MyComponent.spec.js
import { doSomething } from '@/components/MyComponent/utils' //2️⃣
jest.mock('@/components/MyComponent/utils') //3️⃣

describe('MyComponent', () => {
  beforeEach(() => doSomething.mockClear()) //4️⃣

  it('click does something', async () => {
    await shallowMount(MyComponent).find('button').trigger('click')
    expect(doSomething).toHaveBeenCalled()
  })
})

解决方案 3:setMethods()(v1.0 之前)

使用setMethods()deprecated as of v1.0)覆盖组件方法:

describe('MyComponent', () => {
  it('click does something', async () => {
    // Option A:
    const mockMethod = jest.fn()
    const wrapper = shallowMount(MyComponent)
    wrapper.setMethods({ doSomething: mockMethod })
    await wrapper.find('button').trigger('click')
    expect(mockMethod).toHaveBeenCalled()

    // Option B:
    const mockMethod = jest.fn()
    const wrapper = shallowMount(MyComponent, {
      methods: {
        doSomething: mockMethod
      }
    })
    await wrapper.find('button').trigger('click')
    expect(mockMethod).toHaveBeenCalled()
  })
})

demo

正如 tony19 提到的那样,使用 spyOn 方法对您有用。我还发现我需要在模板中的方法中添加括号 () 以便它被提取。我得到了通过以下文件的测试:

ButtonComponent.vue

<template>
  <button @click="method()">Click me</button>
</template>

<script>
export default {
  methods: {
    method() {
      // Have I been called?
    }
  }
}
</script>

ButtonComponent.spec.js

import ButtonComponent from '@/components/ButtonComponent'
import { shallowMount } from '@vue/test-utils'

it('will call the method when clicked', () => {
  const wrapper = shallowMount(ButtonComponent)
  const mockMethod = jest.spyOn(wrapper.vm, 'method')

  const button = wrapper.find('button')
  button.trigger('click')

  expect(mockMethod).toHaveBeenCalled()
  // passes
})