Vitest 单元测试方案

发表于:2024-3-26 09:38

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:江雾    来源:稀土掘金

  简介
  Vitest 是一个面向 Vite 的极快的单元测试框架。它利用了 Vite 的优势,提供了一种全新的测试体验。本文将介绍如何在项目中集成和使用 Vitest 进行单元测试。
  安装 Vitest
  npm install -D vitest
  配置 Vitest
  在项目根目录下创建 vitest.config.js 文件,用于配置 Vitest。
  更多详细配置 cn.vitest.dev/config/
  test: {
      globals: true,
      environment: 'happy-dom',
      restoreMocks: true,
      include: ['**/*.{test,spec,type-test}.{js,mjs,cjs,ts,tsx,jsx}'],
      coverage: {
        exclude: ['tests/**', '.eslintrc.cjs'],
        reporter: ['cobertura', 'text', 'html', 'clover', 'json'],
      },
      setupFiles: `${path.resolve(__dirname, 'tests/setup.ts')}`,
      snapshotFormat: {
        printBasicPrototype: true,
      },
      alias: []
    },
  配置 Vitest公共文件
  在项目根目录下创建setup-test.ts文件,用于统一处理定时器,原生事件,以及在vitest环境下无法模拟的事件
  import { vi, describe, it, expect } from 'vitest';
  import '@testing-library/jest-dom/vitest';
  import MockAdapter from 'axios-mock-adapter';
  import axios from 'axios';
  import { configure } from '.';
  export const mock = new MockAdapter(axios);
  configure({
    renderMode: 'global',
    enableMultipleSelection: false,
  });
  beforeEach(() => {
    mock.reset();
    console.error = vi.fn();
    console.warn = vi.fn();
    vi.useFakeTimers();
    window.requestIdleCallback = vi.fn();
    document.execCommand = vi.fn();
    window.onresize = vi.fn();
    vi.setSystemTime(new Date(1680278400000));
  });
  afterEach(() => {
    mock.reset();
    vi.useRealTimers();
  });
  window.vi = vi;
  window.describe = describe;
  window.it = it;
  window.expect = expect;
  ·全局暴露 vi describe it expect 方法,如果以后更换测试框架,单元测试内部代码无需改动
  · 全局暴露mock,并在beforeEach 与 afterEach 清空相关的mock接口
  · 初始化单元测试环境内时间
  单元测试分类
  · 单元测试:主要是对项目里的最小 组件 或模块进行测试,一般是在非浏览器环境下的测试
  · 集成测试:一般是对单元测试下的多个最小单元组成的较大 组件 或模块进行小型的组合测试,一般是在非浏览器环境下的测试。
  · 端到端 测试 (E2E) : 从用户的视角出发,基于整个页面或者应用,模拟用户操作测试,一般是在浏览器环境下的测试
  如何编写 单元测试
  在项目src目录平级的位置,新建tests文件,tests文件夹下新建mocks文件夹
  · mocks下主要存放对应单元测试模拟数据的文件
  · tests下主要存放对应的单元测试文件
  如何模拟接口请求
  在项目中引入axios-mock-adapter第三方工具库,在进行测试的test文件下新建Mock文件夹,然后新建 test-demo-mock.ts文件。
  详细配置 github.com/ctimmerm/ax…
  import { mock } from '../../../src/setup-test';
  export const TestDemoMock = {
    getUsersTitle: (status: number) => {
      mock.onGet('/users/title').reply(status, { title: '江雾' });
    },
  }; 
  编写测试用例
  新建一个test-demo.tsx文件,编写React组件
  import axios from 'axios';
  import { useEffect, useState } from 'react';
  const TextDemo: React.FC = () => {
    const [title, setTitle] = useState('测试');
    const getTitleValue = async () => {
      const response = await axios.get('/users/title');
      const { status, data } = response;
      if (status === 200) {
        setTitle(data.title);
      } else {
        // error
        setTitle('接口异常');
      }
    };
    const changeBtn = () => {
      setTitle('江雾');
    };
    return (
      <div className="test-dome-content">
        <span className="title"> {title} </span>
        <button className="refresh-button" onClick={getTitleValue}>
          refresh
        </button>
        <button onClick={changeBtn} className="change-btn">
          change
        </button>
      </div>
    );
  };
  export default TextDemo;
  新建test-demo.test.ts单元测试文件
  import { fireEvent, render } from '@testing-library/react';
  import TextDemo from '../../src/test-demo';
  import { TestDemoMock } from './Mock/test-demo-mock';
  describe('getUser', () => {
    it('mount title', async () => {
      const { container } = render(<TextDemo />);
      // 初始化期望 页面标题为测试
      const title = document.body.querySelector('.test-dome-content .title');
      expect(title?.innerHTML).toBe('测试');
    });
    it('change title', async () => {
      await TestDemoMock.getUsersTitle(200);
      render(<TextDemo />);
      // 初始化期望 页面标题为测试
      const title = document.body.querySelector('.test-dome-content .title');
      expect(title?.innerHTML).toBe('测试');
      // 点击按钮
      const btn = document.body.querySelector(
        '.test-dome-content .refresh-button',
      );
      fireEvent.click(btn!);
      await vi.waitFor(() => {
        const targetTitle = document.body.querySelector(
          '.test-dome-content .title',
        );
        expect(targetTitle?.innerHTML).toBe('江雾');
      });
    });
    it('change title error', async () => {
      // 模拟接口请求
      await TestDemoMock.getUsersTitle(201);
      render(<TextDemo />);
      // 初始化期望 页面标题为测试
      const title = document.body.querySelector('.test-dome-content .title');
      expect(title?.innerHTML).toBe('测试');
      // 点击按钮
      const btn = document.body.querySelector(
        '.test-dome-content .refresh-button',
      );
      fireEvent.click(btn!);
      // 期望接口失败内容为接口异常
      await vi.waitFor(() => {
        const title = document.body.querySelector('.test-dome-content .title');
        expect(title?.innerHTML).toBe('接口异常');
      });
    });
  });
  常见的命令
  ·pnpm run test : 执行所有单元测试
  · pnpm test xxx.test.ts : 执行某个单元测试文件
  · pnpm run coverage: 覆盖率报告
  代码覆盖率报告
  执行 pnpm run coverage 会生成coverage 文件夹,将其中的index.html 拖入浏览器中,查看具体的单元测试代码覆盖详情。
  E2E快速补齐单测方案
  自动化录制解决方案
  ·Chrome 插件 DeploySentinel Recorder
  · Chrome 插件 Headless Recorder
  · Playwrighg 官方的 CLI Playwright CLI Codegen
  · eTest 插件
  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号