You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Vitest与Testing-Library中隔离单测的组件渲染?

Vue3组件Vitest测试中渲染叠加导致元素重复的解决方案

用Vue3开发了一个简单列表组件,使用Vitest和Testing-Library编写自动化测试时,每个test方法渲染的组件会叠加在DOM中,导致getByText查询时抛出TestingLibraryElementError: Found multiple elements with the text: foo错误。

测试代码

import { describe, it, expect, test } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/vue'
import TmpList from '../ui/TmpList.vue'

const listItems = ['foo', 'bar']

describe('TmpList', () => {
    // 测试item-content插槽渲染
    test('renders item-content slot', () => {
        const slotTemplate = `
        <template v-slot:item-content="{ item }">
            <div> {{ item }} </div>
        </template>`;

        render(TmpList, { props: { listItems }, slots: { 'item-content': slotTemplate } });
        listItems.forEach(li => {
            expect(screen.getByText(li)).toBeTruthy();
        })
    })

    // 测试列表项交互逻辑
    test('should select item when clicked and is selectable', async () => {
        const slotTemplate = `
        <template v-slot:item-content="{ item }">
            <div> {{ item }} </div>
        </template>`;

        render(TmpList, { props: { listItems, selectable: true }, slots: { 'item-content': slotTemplate } });
        const firstItem = screen.getByText(listItems[0]);
        await fireEvent.click(firstItem);
        expect(firstItem.classList).toContain('selected-item')
    })
})

组件代码

<template>
    <ul>
        <li v-for="(item, index) in listItems" :key="`list-item-${index}`" @click="onItemClick(index)"
            class="rounded mx-2" :class="{
                'selected-item bg-secondary-600/20 text-secondary':
                    selectedIndex == index,
                'hover:bg-zinc-200/30': selectable,
            }">
            <slot name="item-content" :item="item"></slot>
        </li>
    </ul>
</template>

<script setup lang="ts">
import { computed, ref } from "vue";
export interface Props {
    listItems: any[];
    selectable?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
    selectable: false,
});

const selectedIndex = ref<number>(-1);

const onItemClick = (index: number) => {
    if (props.selectable) {
        selectedIndex.value = index;
    }
};

</script>

终端错误信息

TestingLibraryElementError: Found multiple elements with the text: foo

Here are the matching elements:

Ignored nodes: comments, script, style

foo

Ignored nodes: comments, script, style

foo

(If this is intentional, then use the *AllBy* variant of the query (like queryAllByText, getAllByText, or findAllByText)).

Ignored nodes: comments, script, style

    <li
        class="rounded mx-2"
        data-v-96593be0=""
      >
    
        <div>
          foo
        </div>
    
      </li>
      <li
        class="rounded mx-2"
        data-v-96593be0=""
      >
    
        <div>
          bar
        </div>
    
      </li>
    
    </ul>
    
    <li
        class="rounded mx-2 hover:bg-zinc-200/30"
        data-v-96593be0=""
      >
    
        <div>
          foo
        </div>
    
      </li>
      <li
        class="rounded mx-2 hover:bg-zinc-200/30"
        data-v-96593be0=""
      >
    
        <div>
          bar
        </div>
    
      </li>
    
    </ul>
    
❯ Object.getElementError node_modules/@testing-library/dom/dist/config.js:37:19 ❯ getElementError node_modules/@testing-library/dom/dist/query-helpers.js:20:35 ❯ getMultipleElementsFoundError node_modules/@testing-library/dom/dist/query-helpers.js:23:10 ❯ node_modules/@testing-library/dom/dist/query-helpers.js:55:13 ❯ node_modules/@testing-library/dom/dist/query-helpers.js:95:19 ❯ src/components/__tests__/SUList.spec.ts:54:33 52| 53| render(TmpList, { props: { listItems, selectable: true }, slots: { 'item-content': slotTemplate } }); 54| const firstItem = screen.getByText(listItems[0]); | ^ 55| await fireEvent.click(firstItem); 56| expect(firstItem.classList).toContain('selected-item')

问题原因与解决办法

测试渲染叠加是因为每个test执行后,之前渲染的组件没有被清理出DOM。Testing Library Vue默认会自动清理,但如果未生效,可通过以下方式手动保证测试环境独立:

方法1:手动调用Testing Library的清理函数

在describe块内添加afterEach钩子,每次测试结束后清理DOM:

import { cleanup } from '@testing-library/vue'

describe('TmpList', () => {
  afterEach(() => {
    cleanup()
  })

  // 你的测试用例
})

方法2:直接清空DOM

利用Vitest的afterEach钩子,手动重置document.body内容:

describe('TmpList', () => {
  afterEach(() => {
    document.body.innerHTML = ''
  })

  // 你的测试用例
})

方法3:使用render返回的卸载函数

每个render调用会返回包含unmount方法的对象,测试结束后调用它卸载当前组件:

test('renders item-content slot', () => {
    const slotTemplate = `
    <template v-slot:item-content="{ item }">
        <div> {{ item }} </div>
    </template>`;

    const { unmount } = render(TmpList, { props: { listItems }, slots: { 'item-content': slotTemplate } });
    listItems.forEach(li => {
        expect(screen.getByText(li)).toBeTruthy();
    })
    unmount() // 测试结束后卸载组件
})

以上方法都能确保每个测试用例的渲染环境独立,避免元素叠加导致的查询错误。

内容的提问来源于stack exchange,提问作者Nicola Migliore

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 11:46:02