一个Vue3组件单元测试引发的思考

95 阅读3分钟

笔者的公司对单元测试覆盖率比较高,要求写的代码必须写单元测试

最近遇到一个需求,用Vue3实现固定高度的虚拟列表组件,由于组件中使用到某些属性导致写单元测试时一直遇到问题,经过笔者耐心调试后,找到导致问题的原因并给出解决方法,由此记录在本篇文章中

固定高度的虚拟列表组件

固定高度的虚拟列表组件是指在前端开发中,展示大量数据时的一种优化方案,旨在通过“虚拟化”技术来提高性能,尤其是在处理大量数据时,减少渲染和 DOM 节点数量,从而提高页面的渲染速度和用户体验。

请看笔者写的示例代码

<script setup>
import { ref, onMounted, watch, defineProps } from "vue";

const props = defineProps({
  itemHeight: {
    type: Number,
    default: 50,
  },
  items: {
    type: Array,
  },
});

const listContainer = ref(null);
const visibleItems = ref([]);
const totalHeight = ref(0);
const startIndex = ref(0);
const endIndex = ref(0);
const containerHeight = ref(0);

// 计算总高度
const getTotalHeight = () => {
  return props.items.length * props.itemHeight;
};

// 获取每个列表项的样式
const getItemStyle = (index) => {
  return {
    position: "absolute",
    top: `${index * props.itemHeight}px`,
    height: `${props.itemHeight}px`,
    width: "100%",
  };
};

// 更新可见项
const updateVisibleItems = () => {
  const container = listContainer.value;

  const scrollTop = container.scrollTop;
  const visibleCount = Math.ceil(container.clientHeight / props.itemHeight);

  const start = Math.max(0, Math.floor(scrollTop / props.itemHeight));
  const end = Math.min(
    props.items.length - 1,
    Math.ceil((scrollTop + container.clientHeight) / props.itemHeight)
  );

  visibleItems.value = props.items.slice(start, end + 1);

  startIndex.value = start;
  endIndex.value = end;
};

// 滚动事件处理
const onScroll = () => {
  updateVisibleItems();
};

watch(
  () => listContainer.value?.clientHeight,
  () => {
    containerHeight.value = listContainer.value.clientHeight;
    totalHeight.value = getTotalHeight();
    updateVisibleItems();
  }
);

// 监听 items 的变化
watch(
  () => props.items,
  () => {
    updateVisibleItems();
  }
);
</script>

<template>
  <div
    class="virtual-list"
    ref="listContainer"
    @scroll="onScroll"
    data-testid="virtual-list"
  >
    <div class="list" :style="{ height: totalHeight + 'px' }">
      <div
        v-for="(item, index) in visibleItems"
        :key="item.id"
        :style="getItemStyle(startIndex + index)"
        class="list-item"
        data-testid="virtual-list-item"
      >
        <slot :item="item"></slot>
      </div>
    </div>
  </div>
</template>

<style scoped>
.virtual-list {
  position: relative;
  overflow-y: auto;
  border: 1px solid #ccc;
  width: 300px;
  height: 400px;
}

.list {
  position: relative;
  width: 100%;
}

.list-item {
  position: absolute;
  width: 100%;
}

.item {
  padding: 10px;
  border-bottom: 1px solid #ccc;
}
</style>

写完组件后,笔者就去写该组件的单元测试了,单元测试也很快写好了,请看下面的示例代码

it("render list item correctly", async () => {
    const wrapper = mount(VirtualList, {
      props: {
        items,
        itemHeight: 50,
      },
      slots: {
        default: ({ item }) => `<div>${item.name}</div>`,
      },
    });
    
    await wrapper.vm.$nextTick();

    expect(wrapper.text()).toContain("Item 1");
    expect(wrapper.text()).toContain("Item 2");
  });

可是运行时直接报错了,错误信息如下所示

image.png

错误信息显示只有Item1没有Item2, 太奇怪了。最起码会渲染很多Item,绝不是一个Item。然后笔者耐心调试

调试

来看看组件中的几处代码

 const container = listContainer.value;

  const scrollTop = container.scrollTop;
  const visibleCount = Math.ceil(container.clientHeight / props.itemHeight);

  const start = Math.max(0, Math.floor(scrollTop / props.itemHeight));
  const end = Math.min(
    props.items.length - 1,
    Math.ceil((scrollTop + container.clientHeight) / props.itemHeight)
  );

  visibleItems.value = props.items.slice(start, end + 1);

  startIndex.value = start;
  endIndex.value = end;

container 是虚拟列表容器的DOM引用,经过调试发现容器的 clientHeightscrollTop一直是0,没有取到实际的值

一开始笔者以为是@vue/test-utils导致的,当笔者换了@testing-library/vue后,依旧报错,笔者就感觉不是测试框架的问题

经过一番排查,发现了如下有用的信息

image.png

根本原因是jsdom不支持,导致测试环境中无法提供一些DOM节点属性,造成测试无法正常执行

解决方案

如何解决jsdom带来的这个问题呢?可以通过mock需要使用的属性,方法如下

  it("render list item correctly", async () => {
    const wrapper = mount(VirtualList, {
      props: {
        items,
        itemHeight: 50,
      },
      slots: {
        default: ({ item }) => `<div>${item.name}</div>`,
      },
    });

    // 解决方案如下
    const clientHeightMock = 200;

    const listContainer = wrapper.find(".virtual-list").element;
    Object.defineProperty(listContainer, "clientHeight", {
      value: clientHeightMock,
    });

    await wrapper.vm.$nextTick();

    expect(wrapper.text()).toContain("Item 1");
    expect(wrapper.text()).toContain("Item 2");
    expect(wrapper.text()).toContain("Item 3");
    expect(wrapper.text()).toContain("Item 4");
  });

image.png

再运行测试就正常通过了