Skip to content

存根和浅挂载

¥Stubs and Shallow Mount

Vue Test Utils 提供了一些用于存根组件和指令的高级功能。存根是你将自定义组件或指令的现有实现替换为根本不执行任何操作的虚拟组件或指令的地方,这可以简化原本复杂的测试。让我们看一个例子。

¥Vue Test Utils provides some advanced features for stubbing components and directives. A stub is where you replace an existing implementation of a custom component or directive with a dummy one that doesn't do anything at all, which can simplify an otherwise complex test. Let's see an example.

存根单个子组件

¥Stubbing a single child component

一个常见的例子是,当你想要测试组件层次结构中非常高的组件中的某些内容时。

¥A common example is when you would like to test something in a component that appears very high in the component hierarchy.

在此示例中,我们有一个用于渲染消息的 <App> 组件,以及一个用于进行 API 调用并渲染其结果的 FetchDataFromApi 组件。

¥In this example, we have an <App> that renders a message, as well as a FetchDataFromApi component that makes an API call and renders its result.

js
const FetchDataFromApi = {
  name: 'FetchDataFromApi',
  template: `
    <div>{{ result }}</div>
  `,
  async mounted() {
    const res = await axios.get('/api/info')
    this.result = res.data
  },
  data() {
    return {
      result: ''
    }
  }
}

const App = {
  components: {
    FetchDataFromApi
  },
  template: `
    <h1>Welcome to Vue.js 3</h1>
    <fetch-data-from-api />
  `
}

我们不想在此特定测试中进行 API 调用,我们只想断言消息已渲染。在这种情况下,我们可以使用 stubs,它出现在 global 挂载选项中。

¥We do not want to make the API call in this particular test, we just want to assert the message is rendered. In this case, we could use the stubs, which appears in the global mounting option.

js
test('stubs component with custom template', () => {
  const wrapper = mount(App, {
    global: {
      stubs: {
        FetchDataFromApi: {
          template: '<span />'
        }
      }
    }
  })

  console.log(wrapper.html())
  // <h1>Welcome to Vue.js 3</h1><span></span>

  expect(wrapper.html()).toContain('Welcome to Vue.js 3')
})

请注意,模板显示 <span></span> 位于 <fetch-data-from-api /> 的位置?我们用存根替换它 - 在本例中,我们通过传入 template 来提供我们自己的实现。

¥Notice that the template is showing <span></span> where <fetch-data-from-api /> was? We replaced it with a stub - in this case, we provided our own implementation by passing in a template.

你还可以获得默认存根,而不是提供你自己的:

¥You can also get a default stub, instead of providing your own:

js
test('stubs component', () => {
  const wrapper = mount(App, {
    global: {
      stubs: {
        FetchDataFromApi: true
      }
    }
  })

  console.log(wrapper.html())
  /*
    <h1>Welcome to Vue.js 3</h1>
    <fetch-data-from-api-stub></fetch-data-from-api-stub>
  */

  expect(wrapper.html()).toContain('Welcome to Vue.js 3')
})

这将消除整个渲染树中的所有 <FetchDataFromApi /> 组件,无论它们出现在哪个级别。这就是它位于 global 挂载选项中的原因。

¥This will stub out all the <FetchDataFromApi /> components in the entire render tree, regardless of what level they appear at. That's why it is in the global mounting option.

提示

要存根,你可以使用 components 中的键或组件的名称。如果两者都在 global.stubs 中给出,则将首先使用密钥。

¥To stub out you can either use the key in components or the name of your component. If both are given in global.stubs the key will be used first.

存根所有子组件

¥Stubbing all children components

有时你可能想要删除所有自定义组件。例如,你可能有这样的组件:

¥Sometimes you might want to stub out all the custom components. For example you might have a component like this:

js
const ComplexComponent = {
  components: { ComplexA, ComplexB, ComplexC },
  template: `
    <h1>Welcome to Vue.js 3</h1>
    <ComplexA />
    <ComplexB />
    <ComplexC />
  `
}

想象一下,每个 <Complex> 都做了一些复杂的事情,而你只对测试 <h1> 是否渲染正确的问候语感兴趣。你可以这样做:

¥Imagine each of the <Complex> does something complicated, and you are only interested in testing that the <h1> is rendering the correct greeting. You could do something like:

js
const wrapper = mount(ComplexComponent, {
  global: {
    stubs: {
      ComplexA: true,
      ComplexB: true,
      ComplexC: true
    }
  }
})

但这是很多样板文件。VTU 有一个 shallow 挂载选项,可以自动删除所有子组件:

¥But that's a lot of boilerplate. VTU has a shallow mounting option that will automatically stub out all the child components:

js
test('shallow stubs out all child components', () => {
  const wrapper = mount(ComplexComponent, {
    shallow: true
  })

  console.log(wrapper.html())
  /*
    <h1>Welcome to Vue.js 3</h1>
    <complex-a-stub></complex-a-stub>
    <complex-b-stub></complex-b-stub>
    <complex-c-stub></complex-c-stub>
  */
})

提示

如果你使用过 VTU V1,你可能会记得它是 shallowMount。这个方法还是可以用的 - 和写 shallow: true 一样。

¥If you used VTU V1, you may remember this as shallowMount. That method is still available, too - it's the same as writing shallow: true.

存根所有子组件(但有例外)

¥Stubbing all children components with exceptions

有时你想要删除除特定组件之外的所有自定义组件。让我们考虑一个例子:

¥Sometimes you want to stub out all the custom components, except specific one. Let's consider an example:

js
const ComplexA = {
  template: '<h2>Hello from real component!</h2>'
}

const ComplexComponent = {
  components: { ComplexA, ComplexB, ComplexC },
  template: `
    <h1>Welcome to Vue.js 3</h1>
    <ComplexA />
    <ComplexB />
    <ComplexC />
  `
}

通过使用 shallow 挂载选项,将自动删除所有子组件。如果我们想明确选择退出存根特定组件,我们可以在 stubs 中提供其名称,并将值设置为 false

¥By using shallow mounting option that will automatically stub out all the child components. If we want to explicitly opt-out of stubbing specific component, we could provide its name in stubs with value set to false

js
test('shallow allows opt-out of stubbing specific component', () => {
  const wrapper = mount(ComplexComponent, {
    shallow: true,
    global: {
      stubs: { ComplexA: false }
    }
  })

  console.log(wrapper.html())
  /*
    <h1>Welcome to Vue.js 3</h1>
    <h2>Hello from real component!</h2>
    <complex-b-stub></complex-b-stub>
    <complex-c-stub></complex-c-stub>
  */
})

存根异步组件

¥Stubbing an async component

如果你想删除异步组件,则有两种行为。例如,你可能有这样的组件:

¥In case you want to stub out an async component, then there are two behaviours. For example, you might have components like this:

js
// AsyncComponent.js
export default defineComponent({
  name: 'AsyncComponent',
  template: '<span>AsyncComponent</span>'
})

// App.js
const App = defineComponent({
  components: {
    MyComponent: defineAsyncComponent(() => import('./AsyncComponent'))
  },
  template: '<MyComponent/>'
})

第一个行为是使用在加载异步组件的组件中定义的键。在此示例中,我们使用键 "MyComponent"。在测试用例中不需要使用 async/await,因为该组件在解析之前已被删除。

¥The first behaviour is using the key defined in your component which loads the async component. In this example we used to key "MyComponent". It is not required to use async/await in the test case, because the component has been stubbed out before resolving.

js
test('stubs async component without resolving', () => {
  const wrapper = mount(App, {
    global: {
      stubs: {
        MyComponent: true
      }
    }
  })

  expect(wrapper.html()).toBe('<my-component-stub></my-component-stub>')
})

第二个行为是使用异步组件的名称。在本例中,我们使用 "AsyncComponent" 命名。现在需要使用 async/await,因为需要解析异步组件,然后可以通过异步组件中定义的名称将其存根。

¥The second behaviour is using the name of the async component. In this example we used to name "AsyncComponent". Now it is required to use async/await, because the async component needs to be resolved and then can be stubbed out by the name defined in the async component.

确保你在异步组件中定义了一个名称!

¥Make sure you define a name in your async component!

js
test('stubs async component with resolving', async () => {
  const wrapper = mount(App, {
    global: {
      stubs: {
        AsyncComponent: true
      }
    }
  })

  await flushPromises()

  expect(wrapper.html()).toBe('<async-component-stub></async-component-stub>')
})

存根指令

¥Stubbing a directive

有时指令会执行相当复杂的操作,例如执行大量 DOM 操作,这可能会导致测试错误(由于 JSDOM 与整个 DOM 行为不相似)。一个常见的例子是来自各种库的工具提示指令,它们通常严重依赖于测量 DOM 节点的位置/大小。

¥Sometimes directives do quite complex things, like perform a lot of DOM manipulation which might result in errors in your tests (due to JSDOM not resembling entire DOM behavior). A common example is tooltip directives from various libraries, which usually rely heavily on measuring DOM nodes position/sizes.

在此示例中,我们有另一个 <App>,它渲染带有工具提示的消息

¥In this example, we have another <App> that renders a message with tooltip

js
// tooltip directive declared somewhere, named `Tooltip`

const App = {
  directives: {
    Tooltip
  },
  template: '<h1 v-tooltip title="Welcome tooltip">Welcome to Vue.js 3</h1>'
}

我们不希望在此测试中执行 Tooltip 指令代码,我们只想断言消息已渲染。在这种情况下,我们可以使用 stubs,它出现在 global 挂载选项中,传递 vTooltip

¥We do not want the Tooltip directive code to be executed in this test, we just want to assert the message is rendered. In this case, we could use the stubs, which appears in the global mounting option passing vTooltip.

js
test('stubs component with custom template', () => {
  const wrapper = mount(App, {
    global: {
      stubs: {
        vTooltip: true
      }
    }
  })

  console.log(wrapper.html())
  // <h1>Welcome to Vue.js 3</h1>

  expect(wrapper.html()).toContain('Welcome to Vue.js 3')
})

提示

使用 vCustomDirective 命名方案来区分组件和指令的灵感来自于 <script setup> 中使用的 同样的方法

¥Usage of vCustomDirective naming scheme to differentiate between components and directives is inspired by same approach used in <script setup>

有时,我们需要指令功能的一部分(通常是因为某些代码依赖于它)。假设我们的指令在执行时添加了 with-tooltip CSS 类,这对于我们的代码来说是重要的行为。在这种情况下,我们可以将 true 与我们的模拟指令实现交换

¥Sometimes, we need a part of directive functionality (usually because some code relies on it). Let's assume our directive adds with-tooltip CSS class when executed and this is important behavior for our code. In this case we can swap true with our mock directive implementation

js
test('stubs component with custom template', () => {
  const wrapper = mount(App, {
    global: {
      stubs: {
        vTooltip: {
          beforeMount(el: Element) {
            console.log('directive called')
            el.classList.add('with-tooltip')
          }
        }
      }
    }
  })

  // 'directive called' logged to console

  console.log(wrapper.html())
  // <h1 class="with-tooltip">Welcome to Vue.js 3</h1>

  expect(wrapper.classes('with-tooltip')).toBe(true)
})

我们刚刚用我们自己的指令实现替换了指令实现!

¥We've just swapped our directive implementation with our own one!

警告

由于 withDirectives 函数内部缺少指令名称,存根指令不适用于功能组件或 <script setup>。如果你需要在功能组件中使用模拟指令,请考虑通过测试框架模拟指令模块。有关解锁此类功能的建议,请参阅 https://github.com/vuejs/core/issues/6887

¥Stubbing directives won't work on functional components or <script setup> due to lack of directive name inside of withDirectives function. Consider mocking directive module via your testing framework if you need to mock directive used in functional component. See https://github.com/vuejs/core/issues/6887 for proposal to unlock such functionality

默认插槽和 shallow

¥Default Slots and shallow

由于 shallow 删除了组件的所有内容,因此在使用 shallow 时任何 <slot> 都不会被渲染。虽然这在大多数情况下不是问题,但在某些情况下这并不理想。

¥Since shallow stubs out all the content of a components, any <slot> won't get rendered when using shallow. While this is not a problem in most cases, there are some scenarios where this isn't ideal.

js
const CustomButton = {
  template: `
    <button>
      <slot />
    </button>
  `
}

你可以这样使用它:

¥And you might use it like this:

js
const App = {
  props: ['authenticated'],
  components: { CustomButton },
  template: `
    <custom-button>
      <div v-if="authenticated">Log out</div>
      <div v-else>Log in</div>
    </custom-button>
  `
}

如果你使用的是 shallow,则该插槽将不会被渲染,因为 <custom-button /> 中的渲染函数已被删除。这意味着你将无法验证渲染的文本是否正确!

¥If you are using shallow, the slot will not be rendered, since the render function in <custom-button /> is stubbed out. That means you won't be able to verify the correct text is rendered!

对于此用例,你可以使用 config.renderStubDefaultSlot,即使使用 shallow,它也将渲染默认槽内容:

¥For this use case, you can use config.renderStubDefaultSlot, which will render the default slot content, even when using shallow:

js
import { config, mount } from '@vue/test-utils'

beforeAll(() => {
  config.global.renderStubDefaultSlot = true
})

afterAll(() => {
  config.global.renderStubDefaultSlot = false
})

test('shallow with stubs', () => {
  const wrapper = mount(AnotherApp, {
    props: {
      authenticated: true
    },
    shallow: true
  })

  expect(wrapper.html()).toContain('Log out')
})

由于此行为是全局的,而不是以 mount by mount 为基础,因此你需要记住在每次测试之前和之后启用/禁用它。

¥Since this behavior is global, not on a mount by mount basis, you need to remember to enable/disable it before and after each test.

提示

你还可以通过在测试设置文件中导入 config 并将 renderStubDefaultSlot 设置为 true 来全局启用此功能。不幸的是,由于技术限制,此行为并未扩展到默认插槽以外的插槽。

¥You can also enable this globally by importing config in your test setup file, and setting renderStubDefaultSlot to true. Unfortunately, due to technical limitations, this behavior is not extended to slots other than the default slot.

mountshallowstubs:哪一个以及何时?

¥mount, shallow and stubs: which one and when?

根据经验,你的测试越接近软件的使用方式,它们就越能给你带来信心。

¥As a rule of thumb, the more your tests resemble the way your software is used, the more confidence they can give you.

使用 mount 的测试将渲染整个组件层次结构,这更接近用户在真实浏览器中的体验。

¥Tests that use mount will render the entire component hierarchy, which is closer to what the user will experience in a real browser.

另一方面,使用 shallow 的测试侧重于特定组件。shallow 对于完全隔离地测试高级组件非常有用。如果你只有一两个与你的测试不相关的组件,请考虑将 mountstubs 结合使用,而不是 shallow。存根越多,测试的生产环境就越差。

¥On the other hand, tests using shallow are focused on a specific component. shallow can be useful for testing advanced components in complete isolation. If you just have one or two components that are not relevant to your tests, consider using mount in combination with stubs instead of shallow. The more you stub, the less production-like your test becomes.

请记住,无论你是进行完全挂载还是浅渲染,良好的测试都侧重于输入(props 和用户交互,例如 trigger)和输出(渲染的 DOM 元素和事件),而不是实现细节。

¥Keep in mind that whether you are doing a full mount or a shallow render, good tests focus on inputs (props and user interaction, such as with trigger) and outputs (the DOM elements that are rendered, and events), not implementation details.

因此,无论你选择哪种挂载方法,我们都建议你牢记这些准则。

¥So regardless of which mounting method you choose, we suggest keeping these guidelines in mind.

结论

¥Conclusion

  • 使用 global.stubs 将组件或指令替换为虚拟组件或指令以简化测试

    ¥use global.stubs to replace a component or directive with a dummy one to simplify your tests

  • 使用 shallow: true(或 shallowMount)来删除所有子组件

    ¥use shallow: true (or shallowMount) to stub out all child components

  • 使用 global.renderStubDefaultSlot 渲染存根组件的默认 <slot>

    ¥use global.renderStubDefaultSlot to render the default <slot> for a stubbed component

Vue Test Utils 中文网 - 粤ICP备13048890号