Vitest测试工具的初步使用

498 阅读14分钟

项目搭建

项目地址: gitee.com/duibaix/vue…

  1. 使用vite搭建vue3+ts项目
npm create vue@latest
  1. 加入vitest测试框架, @testing-library/vue测试工具包, jsdom模拟DOM, 项目开发时依赖
npm install -D vitest @testing-library/vue jsdom
  1. 配置项, 由于vitest与vite的关系可直接在vite.config.ts中配置test信息

    1. 在顶部声明vitest配置ts三斜杠指令
    2. 加入test配置项
 /// <reference types= "vitest/config" />
import { defineConfig } from "vite";
export default defineConfig({
  test: {
    // ... Specify options here.
  },
  ...
});

组件测试

  1. 创建一个简单组件, 展示传入的props元素, my-component.vue 需要传入一个必选参数element元素标签, 两个可选参数isShow是否渲染节点和classname类名, 该组件中还具有一个默认插槽

注: 如果不传isShow参数, js 会将undefined隐式转换为false

<template>
  <div class="home">
    <component :is="element" v-if="isShow" :class="classname">
      <slot></slot>
    </component>
  </div>
</template>

<script setup lang="ts">
defineProps<{
  element: string,
  isShow?: boolean,
  classname?: string,
}>()
</script>

<style scoped>
.my-class {
  border: 1px solid red;
}
</style>
  1. 创建test文件, 测试组件是否渲染正确

    1. 第一个测试用例挂载MyComponent时, 传递所有props参数, 默认插槽传入一段文字, 渲染成功后断言插槽是否正确被替换, 是否有传入的类样式
    2. 第二个测试用例渲染组件时, 传入参数isShow为false, 根据组件逻辑将不会挂载该节点到DOM中, 则后续断言该节点为空, 且在DOM中找不到传入的插槽文字信息
import { render, screen} from '@testing-library/vue'
import { describe, it, expect, assert } from 'vitest'
import MyComponent from '../../components/my-component.vue'

describe("MyComponent Test Unit", () => {
  const testMsg = "Hello, MyCommponent"
  // test case 组件正确渲染
  it("render correctly", async () => {
    render(MyComponent, {
      props: {
        element: "div",
        isShow: true,
        classname: "my-class"
      },
      slots: { default: testMsg}
    })
    const res = await screen.findByText(testMsg)
    // 断言挂载有我传入的slot参数
    expect(res.innerHTML).toBe(testMsg)
    // 断言类样式存在
    assert(res.classList.contains("my-class"))
  })
  //test case 组件display: none
  it("component do not show correctly", async () => {
    render(MyComponent, {
      props: {
        element: "div",
        isShow: false,
      },
      slots: { default: testMsg}
    })
    // 断言组件不在 DOM 中
    const res = screen.queryByText(testMsg);
    expect(res).toBeNull();
  })
})

函数简介:

  • @testing-library/vue中: 相关文档API | Testing Library

    • render渲染一个vue组件实例, 并返回一个包含各种实用方法的对象,
    • screen用于在渲染后的 DOM 中查找元素, 类似于浏览器中的DOM API
  • vitest中: 相关文档Vitest

    • describe函数为一个测试套件, 第一个参数是对其描述信息, 第二个参数接收包含一个或多个测试用例的函数, 官方所描述是为了让组织测试和基准,使报告更加清晰
    • it(test)函数为一个具体的测试用例, 第一个参数为描述信息, 第二个参数接收具体包含测试逻辑的函数。如果里面有异步操作需要声明为异步函数, 为了防止遗漏我在每个函数前都加了async关键字
    • expect, assert都为断言函数, api使用风格不同

模拟网络请求测试

  1. 增加项目依赖 flush-promises 等待之前所有的异步请求, 在@vue/test-utils包中也有此工具
npm install -D flush-promises
  1. 声明ts类型(网络请求的返回值, 主要为下文的mock数据person类型)
 /// <reference types= "vite/client" />
type Person = {
  id: number;
  name: string;
}
type Pageable = {
  page: number;
  size: number;
}
type DataForm<T> = {
  content: T[];
  pageable: Pageable;
  totalElements: number;
}
type Result<T> = {
  code: number;
  msg: string;
  result: DataForm<T> | any;
}
  1. 创建mock数据 person.ts
export const persons: Result<Person> = {
  code: 200,
  msg: "ok",
  result: {
    content: [
      { id: 1, name: "张三1" },
      { id: 2, name: "张三2" },
      { id: 3, name: "张三3" },
      { id: 4, name: "张三4" },
      { id: 5, name: "张三5" },
      { id: 6, name: "张三6" },
      { id: 7, name: "张三7" },
      { id: 8, name: "张三8" },
      { id: 9, name: "张三9" },
      { id: 10, name: "张三10" },
    ],
    pageable: {
      page: 1,
      size: 10,
    },
    totalElements: 99,
  },
};
  1. 封装模拟网络请求获取person.ts中的数据, 返回一个promise对象, 这里为了模拟网络请求失败, 特地加了一个参数
import { persons } from "@/mock/persons";
const person = {
  getPersonList: (failFlag: boolean = false) => new Promise<Result<Person>>((res, rej) => {
    if (failFlag) {
      rej("服务器异常, 请联系管理员");
    }
    res(persons)
  })
}
export default person
  1. 创建一个展示数据的组件person-list.vue接收一个Person数组进行无序列表展示person的姓名, 和一个内容页面my-content.vue, 点击按钮请求数据将数据传入person-list组件中进行展示, 同样这里为了测试异常, 给组件加了一个props参数fetchFail为了标志请求失败或成功, 默认值为false请求成功
<template>
  <div class="box">
    <ul v-if="persons.length > 0">
      <li v-for="person in persons" :key="person.id">
        {{ person.name }}
      </li>
    </ul>
    <h2 v-else></h2>
  </div>
</template>

<script setup lang="ts">
defineProps<{
  persons: Person[]
}>()
</script>
<template>
  <div>
    <button @click="loadingPersonList">loading persons</button>
    <PersonList :persons="personList"></PersonList>
  </div>
</template>

<script setup lang="ts">
import { type Ref, ref } from "vue";
import PersonList from "@/components/person-list.vue";
import fetch from '@/fetch'
const props = withDefaults(defineProps<{
  fetchFail: boolean
}>(), {
  fetchFail: false
})
const personList: Ref<Person[]> = ref([]);
const loadingPersonList = () => {
  fetch.person.getPersonList(props.fetchFail).then((res) => {
    if (res.code === 200) {
      personList.value = res.result.content;
    }
  }).catch((err) => {
    console.error(err);
  });
};
</script>
  1. 创建测试文件, 这里的shallowMount, mount与之前提到的render函数作用一致, 都是渲染组件实例. flushPromises函数则是为了等待之前所有的异步请求结束

    1. 第一个测试用例模拟数据请求成功, 在组件挂载成功之后找到按钮并模拟点击操作. 等待网络请求成功之后获取到Ref变量personList, 查看里面是否有数据并且其中包含一个id为1, name为张三1的对象
    2. 第二个测试用例模拟网络请求失败的情况, 传递参数fetchFail为true, 再次模拟按钮点击事件, 断言会渲染一个h2标签, 且里面内容为' 空 ', 注意这里文字旁边都有一个空格
import { describe, it, expect } from "vitest";
import { shallowMount, mount } from "@vue/test-utils";
import flushPromises from "flush-promises";
import MyContent from "@/page/home/my-content/index.vue";

describe("MyContent组件网络请求测试", () => {
  // case1
  it("加载数据成功", async () => {
    // 挂载组件    
    const myContentWrapper = shallowMount(MyContent)
    // 模拟按钮点击事件, 开始请求数据
    const button = myContentWrapper.find("button")
    button.trigger("click");
    await flushPromises();
    // 等待网络请求, 判断数据是否有值
    const personList = (myContentWrapper.vm as any).personList;
    expect(personList).toBeDefined();
    expect(personList.length).toBeGreaterThan(0);
    expect(personList).toContainEqual({
      id: 1,
      name: '张三1'
    })
    console.log(personList);
  })
  // case2
  it("模拟网络错误", async () => {
    // 传入参数, 模拟请求失败
    const myContentWrapper = mount(MyContent, {
      props: {
        fetchFail: true,
      },
    });
    // 模拟点击事件
    const button = myContentWrapper.find("button");
    button.trigger("click");
    await flushPromises();
    // 断言出现h2元素, 并且显示 ' 空 ' 
    const h2El = myContentWrapper.find("h2");
    expect(h2El.exists()).toBe(true)
    expect(h2El.element.innerHTML).toBe(" 空 ")
  });
})

权限测试

页面级权限

  1. 增加路由依赖
npm install -D vue-router
  1. 配置router, 一共五个页面和四个路由有效路由, 默认展示home首页, 如果没有访问没有定义的路由时返回404页面, 在每个路由中增加了meta元信息, 里面包含这个路由可以访问的用户名, 为所有用户都可访问, 在导航守卫中判断该用户是否有权限, 无权限跳转到no-permission页面, 用户角色存放在localStorage中, 这里为了后续代码覆盖率特地封装setNavGuard*函数并导出用在后续测试用例中
import { createRouter, createWebHashHistory, type Router, type RouteRecordRaw } from "vue-router";

declare module "vue-router" {
  interface RouteMeta {
    permisson: Array<string>;
  }
}
const routes: RouteRecordRaw[] = [
  {
    path: "/",
    redirect: "/home",
  },
  {
    path: "/home",
    component: () => import("@/page/home/index.vue"),
    meta: {
      permisson: ["*"],
    },
  },
  {
    path: "/about",
    component: () => import("@/page/about/index.vue"),
    meta: {
      permisson: ["*"],
    },
  },
  {
    path: "/manage",
    component: () => import("@/page/manage/index.vue"),
    meta: {
      permisson: ["admin", "userA"],
    },
  },
  {
    path: "/no-permisson",
    component: () => import("@/page/no-permisson/index.vue"),
    meta: {
      permisson: ["*"],
    },
  },
  {
    path: "/:pathMatch(.*)*", // 捕获所有未匹配的路径
    component: () => import("@/page/not-found/index.vue"),
    meta: {
      permisson: ["*"],
    },
  },
];
const router = createRouter({
  history: createWebHashHistory(),
  routes
})
const setNavGuard = (router: Router) => {
  router.beforeEach((to, from, next) => {
    const { permisson } = to.meta;
    if (permisson.includes("*") || permisson.includes(localStorage.getItem("userRole"))) {
      next()
    } else {
      next("/no-permisson")
    }
  })
}
setNavGuard(router)
export { routes, setNavGuard };
export default router;
  1. 创建测试用例, , 如下为App.vue, 为router-view创建自定义数据属性, 方便后续测试查找到该元素

         <div class="app-content">
           <router-link class="link" to="/">主页</router-link>
           <router-link data-test="about" class="link" to="/about">关于</router-link>
           <router-link data-test="manage" class="link" to="/manage">管理页面</router-link>
           <div>
             <router-view></router-view>
           </div>
         </div>
       ```
    
  • 第一条测试用例, 测试默认路由渲染Home组件, 其他组件不存在
  • 第二条测试用例, 测试访问没有定义的路由路径, 跳转到404
  • 第三条测试用例, App组件渲染成功后点击router-link跳转到About页面
  • 第四条测试用例, App组件渲染成功后点击router-link跳转到Manage页面被拦截重定向到NoPermission
  • 第五条测试用例, 增加全局属性localSorage增加用户角色为admin, App组件渲染成功后点击router-link成功跳转到Manage页面

注意: 在测试vue-router时, 需要每一个test拥有自己的router实例, 详细可查看下方官方文档, 这里使用vitest的beforeEach注册一个回调函数, 在当前上下文中的每个测试运行前调用

expectComponent函数为断言组件是否存在, 需要传入一个VueWrapper对象和一个ComponentTestExist数组, 循环自定义数据判断组件是否渲染正确(其实写完才发现还不如一行一行cv的可读性高)

Vue Test Utils

import { describe, expect, it, beforeEach } from "vitest";
import { mount, type VueWrapper } from "@vue/test-utils";
import { type Component } from "vue";
import flushPromises from "flush-promises";
import { type Router, createRouter, createWebHashHistory } from "vue-router";
import { routes, setNavGuard } from "@/router";
import App from "@/App.vue";
import Home from "@/page/home/index.vue";
import About from "@/page/about/index.vue";
import Manage from "@/page/manage/index.vue";
import NoPermission from "@/page/no-permisson/index.vue";
import NotFound from "@/page/not-found/index.vue";
declare type ComponentTestExist = {
  component: Component;
  isExist: boolean;
};
const expectComponent = (
  appWrapper: VueWrapper,
  list: Array<ComponentTestExist>
) => {
  list.forEach((item) =>
    expect(appWrapper.findComponent(item.component).exists()).toBe(item.isExist)
  );
};
// each test should use it's own instance of the router 
let router: Router;
beforeEach(async () => {
  router = createRouter({
    history: createWebHashHistory(),
    routes,
  });
  setNavGuard(router);
});

describe("页面权限测试", () => {
  it("测试路由不存在, 返回not-found页面", async () => {
    router.push("//aaa////");
    await router.isReady();
    const appWrapper = mount(App, {
      global: {
        plugins: [router],
      },
    });
    // 断言渲染NotFound页面
    expect(appWrapper.html()).toContain("404");
    expect(appWrapper.findComponent(NotFound).exists()).toBe(true);
  });
  it("测试路由渲染", async () => {
    router.replace("/");
    await router.isReady();
    const appWrapper = mount(App, {
      global: {
        plugins: [router],
      },
    });
    // 断言只出现Home组件
    expectComponent(appWrapper, [
      { component: Home, isExist: true },
      { component: About, isExist: false },
      { component: Manage, isExist: false },
      { component: NoPermission, isExist: false },
    ]);
    // 正确渲染Home组件组件中title
    expect(appWrapper.html()).toContain("Hello, HomePage");
  });
  it("测试路由点击跳转成功", async () => {
    router.push("/");
    await router.isReady();
    const appWrapper = mount(App, {
      global: {
        plugins: [router],
      },
    });
    // 模拟点击about路由, 断言渲染About组件成功
    await appWrapper.find("[data-test='about']").trigger("click");
    await flushPromises();
    expect(appWrapper.findComponent(About).exists()).toBe(true);
    expect(appWrapper.html()).toContain("about");
  });
  it("测试没有权限的页面, 加载没有权限", async () => {
    router.push("/");
    await router.isReady();
    const appWrapper = mount(App, {
      global: {
        plugins: [router],
      },
    });
    // 模拟点击manage路由, 断言无权限, 渲染NoPermission组件成功
    await appWrapper.find("[data-test='manage']").trigger("click");
    await flushPromises();
    expect(appWrapper.findComponent(NoPermission).exists()).toBe(true);
    expect(appWrapper.findComponent(Manage).exists()).toBe(false);
    expect(appWrapper.html()).toContain("no permisson");
  });
  it("测试没有权限的页面, 加载成功", async () => {
    // 设置用户角色信息
    localStorage.setItem("userRole", "admin");
    router.push("/");
    await router.isReady();
    const appWrapper = mount(App, {
      global: {
        plugins: [router],
      },
    });
    // 模拟点击manage路由, 断言无权限, 渲染Manage组件成功
    await appWrapper.find("[data-test='manage']").trigger("click");
    await flushPromises();
    expect(appWrapper.findComponent(Manage).exists()).toBe(true);
    expect(appWrapper.html()).toContain("manage");
  });
});

组件级权限

灵感来源:

www.bilibili.com/video/BV11A…

  1. 首先增加权限mock数据, permissons.ts

注: 这里*通配符的含义

  1. 在用户权限中, 拥有*表示可以查看所有组件进行或操作任意组件
  2. 在组件所需权限中, 拥有*表示任意用户都可以进行查看或操作(除当该用户的权限为 null, undefined等外, 例如haveNone用户, 当然这些逻辑可以在后续权限组件中更改)
 // 所有用户
export const userRoles = ["foo", "bar", "admin", "haveNone"];
// 每个用户的权限信息, 必须为数组
export const userRolePermission = {
  foo: ["sys:user:add"],
  bar: ["sys:user:add", "sys:user:update"],
  admin: ["*"], // 表示拥有所有权限
  haveNone: null, // 无权限, 或(-)0, false, "", undefined, NaN
};
// 每个按钮所需的权限, string | string[]
export const btnPermission: Array<BtnPermisson> = [
  { name: "增加用户", permissson: "sys:user:add" },
  { name: "删除用户", permissson: "sys:user:delete" },
  { name: "修改用户", permissson: "sys:user:update" },
  {
    name: "联系用户",
    permissson: ["sys:user:add", "sys:user:update"],
  }, // 表示需要add && update两个权限
  { name: "查看用户", permissson: "*" }, // 表示无权限判断
];
  1. 创建useAuth.ts使用hooks函数模拟用户当前的权限(实际开发业务时应存储在状态管理或localstorage中)
import { ref } from "vue";
// 当前用户, 初始值只有增加用户的权限
export const nowPermissions = ref([
  // "*",
  "sys:user:add",
  // "sys:user:delete",
  // "sys:user:update",
]);
  1. 创建权限组件将需要权限展示的组件或元素包裹起来, 传入permissionNeed参数, 从useAuth中拿到当前用户权限, 通过isShowSlot计算属性用来实现判断权限逻辑(对应前端也就是该组件是否显示)
<template>
  <slot v-if="isShowSlot" :nowPermissions></slot>
</template>

<script setup lang="ts">
import { computed } from "vue";
import { nowPermissions } from "@/hooks/useAuth";

// 按钮需要的权限
const props = defineProps<{
  permissionNeed: string[] | string;
}>();

const isIncludeAll = (list: string[] | string) => list.includes("*");
const isShowSlot = computed((): boolean => {
  const { permissionNeed } = props;
  // 如果用户没有权限,不显示
  if (!nowPermissions.value) {
    return false;
  }
  // 权限为*任意,直接显示
  if (isIncludeAll(nowPermissions.value) || isIncludeAll(permissionNeed)) {
    return true;
  }

  if (Array.isArray(permissionNeed)) {
    // 用户需要包含每一个按钮所需的权限
    return permissionNeed.every((item) => nowPermissions.value.includes(item));
  } else {
    return nowPermissions.value.includes(permissionNeed);
  }
});
</script>

<style scoped></style>
  1. manage页面, 通过切换不同用户展示不同的按钮. 拿到所有mock数据进行展示, 将按钮传递给子组件插槽中(这里不关心权限判断的逻辑, 只需要将参数传递给子组件), 这里默认插槽其实还可以接受一个nowPermissions参数获取当前用户的权限, 可以进一步增强对插槽中的内容做操作(例如按钮的禁用, 颜色等).

注:

  1. 你很快就注意到了我在manage页面也用到了useAuth函数, 其实这里只是为了去切换用户特地引入的(真正业务中, 用户的切换是在登录与登出的逻辑中去操作), 实际上这里不需要关心当前用户的权限值, 只需要将按钮展示所需要的权限传入btn-authority组件.
  2. 这里@change="(e) => selectChange(e)" 有一点react风格, 在react事件处理函数中只能传入函数, 而不能直接调用函数比如@change="selectChange($event)" 这样的写法(react会在渲染页面时调用而不是在点击事件中调用, 当然在react中具体使用jsx语法), 而在vue中这两种写法都可以, 因为vue在内部有做处理(实际上都会以函数的形式传递,详细可查看下面链接), 个人认为传入函数比较好一些, 毕竟框架最后也是要拿到函数使用addEventListener实现监听, 而且在查看代码时也能很清楚地知道这是一个回调函数

你知道vue中@click="onClick"和@click="onClick()"有什么区别么?更详细的了解vue中的 - 掘金

<template>
  <div class="manage">
    <h2>manage</h2>
    <div class="content">
      <label :for="userRoleString">用户:</label>
      <select :id="userRoleString" @change="(e) => selectChange(e)">
        <option
          v-for="(userRole, index) in userRoleOptions"
          :key="index"
          :value="userRole"
        >
          {{ userRole }}
        </option>
      </select>
      <div class="btn-box">
        <template v-for="(item, index) in btns">
          <btn-authority :permissionNeed="item.permissson">
            <template #default>
              <button :key="index" :disabled="false">
                {{ item.name }}
              </button>
            </template>
          </btn-authority>
        </template>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { type Ref, ref } from "vue";
import BtnAuthority from "@/components/btn-authority.vue";
import { nowPermissions } from "@/hooks/useAuth";
import {
  btnPermission,
  userRolePermission,
  userRoles,
} from "@/mock/permissions";

// 用户select和label绑定的id值
const userRoleString = "userRole";
// 用户角色选项数组
const userRoleOptions = ref(userRoles);

// 每个按钮所需权限数组
const btns: Ref<Array<BtnPermisson>> = ref(btnPermission);
// select框change事件, 改变当前用户的权限
const selectChange = (e: Event) => {
  const target = e.target as HTMLSelectElement;
  nowPermissions.value = userRolePermission[target.value];
};
</script>

<style scoped>
.manage {
  width: 50 % ;
  margin: 0 auto;
  text-align: center;
}
.content select {
  width: 50 % ;
}
.content .btn-box {
  margin-top: 20px;
  display: flex;
  flex-flow: row, nowrap;
  justify-content: space-evenly;
}
</style>

  1. manage组件测试,

    1. 第一条测试用例, 挂载manage组件, 断言默认foo角色的内容展示正确(拥有增加和查看权限)
    2. 第二条测试用例, 模拟选择bar用户, 断言其展示的内容正确(没有删除权限)
    3. 第三条测试用例, 模拟选择admin用户, 断言其展示的内容正确(拥有全部权限)
    4. 第四条测试用例, 模拟选择haveNone用户, 只有查看权限
import { describe, it, expect } from  "vitest" ; import { mount } from  "@vue/test-utils" ; import  Manage  from  "@/page/manage/index.vue" ; describe ( "组件权限测试" , () => {  it ( "测试foo权限" , async () => {  const manageWrapper = mount ( Manage );  // 测试默认foo角色   expect (manageWrapper. html ()). contain ( "foo" );  // 断言foo拥有的按钮权限正确展示 => 查看和修改按钮   const btns = manageWrapper. findAll ( "button" );  expect (btns. find ( ( button ) => button. text () === "增加用户" )). toBeDefined ();  expect (btns. find ( ( button ) => button. text () === "查看用户" )). toBeDefined ();  expect (btns. find ( ( button ) => button. text () === "删除用户" )). toBeUndefined ();  expect (btns. find ( ( button ) => button. text () === "联系用户" )). toBeUndefined ();  expect (btns. find ( ( button ) => button. text () === "修改用户" )). toBeUndefined (); });  it ( "测试bar权限" , async () => {  const manageWrapper = mount ( Manage );  // 模拟select选项框, 选择bar角色   await manageWrapper. find ( "select" ). setValue ( "bar" );  expect (manageWrapper. html ()). contain ( "bar" );  // 断言foo拥有的按钮权限正确展示 => 查看,修改,增加和删除按钮   const btns = manageWrapper. findAll ( "button" );  expect (btns. find ( ( button ) => button. text () === "增加用户" )). toBeDefined ();  expect (btns. find ( ( button ) => button. text () === "查看用户" )). toBeDefined ();  expect (btns. find ( ( button ) => button. text () === "删除用户" )). toBeUndefined ();  expect (btns. find ( ( button ) => button. text () === "联系用户" )). toBeDefined ();  expect (btns. find ( ( button ) => button. text () === "修改用户" )). toBeDefined (); });   it ( "测试admin权限" , async () => {  const manageWrapper = mount ( Manage );  // 模拟select选项框, 选择admin角色   await manageWrapper. find ( "select" ). setValue ( "admin" );  expect (manageWrapper. html ()). contain ( "admin" );  // 断言foo拥有的按钮权限正确展示 => 所有权限   const btns = manageWrapper. findAll ( "button" );  expect (btns. find ( ( button ) => button. text () === "增加用户" )). toBeDefined ();  expect (btns. find ( ( button ) => button. text () === "查看用户" )). toBeDefined ();  expect (btns. find ( ( button ) => button. text () === "删除用户" )). toBeDefined ();  expect (btns. find ( ( button ) => button. text () === "联系用户" )). toBeDefined ();  expect (btns. find ( ( button ) => button. text () === "修改用户" )). toBeDefined (); });  it ( "测试用户权限为空" , async () => {  const manageWrapper = mount ( Manage );  // 模拟select选项框, 选择haveNone角色   await manageWrapper. find ( "select" ). setValue ( "haveNone" );  expect (manageWrapper. html ()). contain ( "haveNone" );  // 断言haveNone 只有查看权限   const btns = manageWrapper. findAll ( "button" );  expect (btns. find ( ( button ) => button. text () === "增加用户" )). toBeUndefined ();  expect (btns. find ( ( button ) => button. text () === "查看用户" )). toBeDefined ();  expect (btns. find ( ( button ) => button. text () === "删除用户" )). toBeUndefined ();  expect (btns. find ( ( button ) => button. text () === "联系用户" )). toBeUndefined ();  expect (btns. find ( ( button ) => button. text () === "修改用户" )). toBeUndefined (); }); }); 

代码覆盖率

  1. 增加v8依赖
npm install -D @vitest/coverage-v8

2. 使用vitest run --coverage命令跑覆盖率, 会默认在根目录下生成coverage文件夹, 里面包含具体的代码覆盖率信息

npx vitest run --coverage

3. 测试率结果展示