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

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

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

固定高度的虚拟列表组件

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

请看笔者写的示例代码

vue 复制代码
<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>

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

js 复制代码
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");
  });

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

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

调试

来看看组件中的几处代码

js 复制代码
 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后,依旧报错,笔者就感觉不是测试框架的问题

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

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

解决方案

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

js 复制代码
  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");
  });

再运行测试就正常通过了

相关推荐
李白的天不白7 小时前
Tree-Shaking
前端
Csvn7 小时前
TypeScript:你以为安全的 `JSON.parse` 其实是颗雷 — 运行时类型安全实战
前端·javascript
橘子星7 小时前
深入理解线性数据结构:栈、队列与链表
前端·javascript
dadaobusi7 小时前
Linux内核完成大量内存/调度/时间子系统初始化的关键阶段
java·linux·前端
用户059540174467 小时前
Redis 缓存过期不一致踩坑实录:一个 bug 让我排查了 3 小时,最终用 Pytest 自动化堵上漏洞
前端·css
东风破_7 小时前
AJAX 异步请求:从回调地狱到 async/await,到底解决了什么?
前端
Larcher7 小时前
JS 数据类型的八重人格与内存真相
前端·javascript
星辰徐哥8 小时前
工具推荐:HTML5+AI开发必备的前端调试工具
前端·人工智能·html5
Full Stack Developme8 小时前
Linux Shell 教程概览
linux·前端·chrome
Maimai108088 小时前
Web3 前端实时通信如何落地:从 SSE 订阅到行情、订单与账户状态更新
前端·javascript·react.js·前端框架·web3·状态模式