简单说说,周末花了两天时间学习Vitest框架上手使用

前言

在早期接触前端的时候就对单测有所耳闻,但是从当时一直到现在,我都觉得单测是一个费力不讨好的事情。在公司开发相关业务的时候都已经忙的够呛了,还要花时间去写单测,不如自己手点点点、打打 console 就得了。

但是,这个观点一直到这个周末接触到单测课程,我才发现自己的观点好像是有点井底蛙。因为单测不仅仅是为了保证代码的质量,更是为了提高自己的开发效率。在这个过程中,我学到了很多东西,也发现了自己的很多问题。在这里,我会分享一下我学到的东西,希望对大家有所帮助。

什么是单元测试

单元测试是指对软件中的最小可测试单元进行检查和验证。在面向对象的程序设计中,最小单元是指类或者方法。单元测试是开发人员编写的用于检查其自己的代码是否正确的测试代码。单元测试是自动化的,可以在任何时候运行,而且可以频繁地运行。单元测试是一种白盒测试。

为什么要写单元测试

  1. 提高代码质量
  2. 提高开发效率
  3. 保证代码的正确性
  4. 便于重构

起步

环境准备

假设大家已经会创建 vite 工程,所以这里不在做赘述。如果大家还不会,可以查看 vite 官网

  • Vite
  • Vue3.x
  • TypeScript
  • Pnpm
  • Vitest

这里我们使用 vitest 作为单测工具。该工具基于 vite,具有高性能、零配置、开箱即用等特点。

安装

bash 复制代码
pnpm add vitest -D

配置

这里不做赘述,直接点击查看 配置流程

测试用例

文件结构

bash 复制代码
.
├── src
│   ├── sum.ts
├── __test__
│   ├── sum.spec.ts

src 目录下新建 sum.ts 文件,用于编写我们的业务代码。在 __test__ 目录下新建 sum.spec.ts 文件,用于编写我们的测试用例。

sum.ts

ts 复制代码
export function sum(a: number, b: number): number {
  return a + b
}

sum.spec.ts

ts 复制代码
import { sum } from '../src/sum'
import { describe, it, expect } from 'vitest'

describe('sum', () => {
  it('sum(1, 2) 结果应该为 3', () => {
    expect(sum(1, 2)).toBe(3)
  })
})

运行测试

bash 复制代码
pnpm test

查看结果

bash 复制代码
 PASS  __test__/sum.spec.ts
  sum
    ✓ sum(1, 2) 结果应该为 3 (2ms)

深入

上述开胃菜,我相信大家看看官网的相关文档都能写出来,并且写这篇文章的目的也并非是水文。接下来,我们将深入一些内容,让大家更好的理解单测。

封装公用方法

__test__ 目录下新建 utils.ts 文件夹,用于封装公用方法。文件结构应该如下:

bash 复制代码
.
├── __test__
│   ├── utils
│       ├── renderHook.ts
│       ├── createRefElement.tsx

@vue/test-utils 库是 Vue.js 官方提供的一个用于测试 Vue 组件的工具库。它提供了一些用于测试 Vue 组件的 API,可以让我们更方便的测试 Vue 组件。

bash 复制代码
pnpm add @vue/test-utils -D

封装 createRefElement.tsx

tsx 复制代码
import { defineComponent, ref } from 'vue'
import { mount } from '@vue/test-utils'

const createRefElement = (slots?: Record<string, Function>) => {
  const wrapper = mount(
    defineComponent({
      setup() {
        const domRef = ref<HTMLElement>()

        return {
          domRef,
        }
      },
      render() {
        return <div ref="domRef">{{ ...slots }}</div>
      },
    }),
  )

  return wrapper
}

export default createRefElement

封装 renderHook.ts

ts 复制代码
import { createApp, defineComponent } from 'vue'

import type { App } from 'vue'

export default function renderHook<R = any>(
  renderFC: () => R,
): [
  R,
  App<Element>,
  {
    act?: (fn: () => void) => void
  },
] {
  let result: any
  let act: ((fn: () => void) => void) | undefined
  const app = createApp(
    defineComponent({
      setup() {
        result = renderFC()

        act = (fn: () => void) => {
          fn()
        }

        return () => {}
      },
    }),
  )

  app.mount(document.createElement('div'))

  return [result, app, { act }]
}

模拟事件

浏览器窗口尺寸变化

假设我们有一个 useWindowSizehook,用于获取浏览器窗口的宽高。

bash 复制代码
pnpm add @vueuse/core
bash 复制代码
.
├── src
│   ├── useWindowSize.ts
├── __test__
│   ├── useWindowSize.spec.ts

useWindowSize.ts

这里我们就不自己写了,可以直接使用 @vueuse/core 库提供的 useWindowSize

useWindowSize.spec.ts

ts 复制代码
import { useWindowSize } from '@vueuse/core'

describe('useWindowSize', () => {
  it('当浏览器尺寸发生变化后,应该更新 width 与 height 的值', async () => {
    const { width, height } = useDevice()

    window.innerWidth = 300
    window.innerHeight = 400

    window.dispatchEvent(new Event('resize'))

    await nextTick()

    expect(width.value).toBe(300)
    expect(height.value).toBe(400)
  })
})

点击事件

假设我们有一个 useClickhook,用于监听点击事件。

useClick.ts

这里的 unrefElement 方法是用于获取 refdom 元素;onoff 方法是用于监听与移除事件;BasicTarget 是描述待绑定元素的类型。由于篇幅原因,这里不做展开,有兴趣可以去百度一下如何写。

ts 复制代码
import { ref, onMounted, onBeforeUnmount } from 'vue'

export function useClick<T extends HTMLElement>(target: BasicTarget<T>) {
  const count = ref(0)

  const click = () => {
    const element = unrefElement(target)

    if (element) {
      count.value += 1
    }
  }

  onMounted(() => {
    on(document, 'click', click)
  })
  onBeforeUnmount(() => {
    off(document, 'click', click)
  })

  return {
    count,
  }
}

useClick.spec.ts

ts 复制代码
import { useClick } from '../src/useClick'
import { nextTick } from 'vue'
import { ref } from 'vue'
import createRefElement from './utils/createRefElement'
import renderHook from './utils/renderHook'

describe('useClick', () => {
  const wrapperRef = createRefElement()
  const [result] = renderHook(() => useClick(wrapperRef.element))

  it('当点击元素后,count 应该加 1', async () => {
    wrapperRef.element.click()

    await nextTick()

    expect(result.count.value).toBe(1)
  })
})

模拟环境

在实际开发中,我们会注入一些 vue 相关的插件,比如 vue-router, pinia, i18n 等。这里简单讲解一下如何在测试环境中起用这些插件。

setupMiniApp

__test__utils 目录下新建 setupMiniApp.ts 文件。

bash 复制代码
.
├── __test__
│   ├── utils
│       ├── setupMiniApp.ts
ts 复制代码
import { setupStore } from '../../src/store'
import { setupRouter } from '../../src/router'
import { setupI18n } from '../../src/locales'
import renderHook from '../utils/renderHook'

/**
 *
 * @description
 * 初始化 mini ray template 应用环境。
 * 该方法会初始化 store、router、i18n 等环境。
 *
 * @example
 * const { app } = await setupMiniApp()
 */
const setupMiniApp = async () => {
  const [_, app] = renderHook(() => {})

  setupStore(app)
  setupRouter(app)
  await setupI18n(app)

  return {
    app,
  }
}

export default setupMiniApp

其中的相关插件 setup 方法这里就不做赘述,其实大概就是初始化插件,并且挂载在 app 上。可以点击查看 Ray Template 相关方法的实现:

更新 pinia 仓库中的值

src 目录下新建一个 store 文件夹,用于存放 pinia 仓库。并且新建一个 demo.ts 文件,用于存放 demo 仓库。

创建对应的 demo.spec.ts 测试文件

bash 复制代码
.
├── src
│   ├── store
│       ├── demo.ts
├── __test__
│   ├── store
│       ├── demo.spec.ts

demo.ts

ts 复制代码
import { defineStore } from 'pinia'

export const useDemoStore = defineStore('demo', () => {
  const count = ref(0)

  const increment = () => {
    count.value += 1
  }

  return {
    count,
    increment,
  }
})

demo.spec.ts

ts 复制代码
import setupMiniApp from '../utils/setupMiniApp'
import { useDemoStore } from '../../src/store/demo'

describe('useDemoStore', async () => {
  await setupMiniApp()

  it('当调用 increment 方法后,count 应该加 1', async () => {
    const store = useDemoStore()

    store.increment()

    expect(store.count.value).toBe(1)
  })
})

使用 vue-router 方法

src 目录下新建一个 router 文件夹,用于存放 vue-router 相关文件。并且新建一个 index.ts 文件,用于存放 router 相关配置。

创建对应的 router.spec.ts 测试文件

bash 复制代码
.
├── src
│   ├── router
│       ├── index.ts
├── __test__
│   ├── router
│       ├── index.spec.ts
ts 复制代码
import setupMiniApp from '../utils/setupMiniApp'
import router from '../../src/router'

describe('router', async () => {
  await setupMiniApp()

  it('当调用 router.push 方法后,路由应该发生变化', async () => {
    const { push, replace } = router

    assert.isFunction(push)
    assert.isFunction(replace)
  })
})

使用 i18n 方法

src 目录下新建一个 locales 文件夹,用于存放 i18n 相关文件。并且新建一个 index.ts 文件,用于存放 i18n 相关配置。

创建对应的 locales.spec.ts 测试文件

bash 复制代码
.
├── src
│   ├── locales
│       ├── index.ts
├── __test__
│   ├── locales
│       ├── index.spec.ts
ts 复制代码
import setupMiniApp from '../utils/setupMiniApp'
import i18n from '../../src/locales'

describe('i18n', async () => {
  await setupMiniApp()

  it('当调用 t 方法后,应该返回对应的翻译文本', async () => {
    const { t } = i18n.global

    assert.isFunction(t)
  })
})

最后🌻

至此,大概讲解了一下单测的相关内容。希望对大家有所帮助。在实际开发中,单测是非常重要的,不仅仅是为了保证代码的质量,更是为了提高自己的开发效率。希望大家都能写出高质量的代码。

文章内有很多内容都是来自于 Ray Template,如果大家有兴趣可以去看看。如果有什么问题,欢迎大家提出来,我会尽力解答。

相关推荐
m51273 分钟前
LinuxC语言
java·服务器·前端
Myli_ing1 小时前
HTML的自动定义倒计时,这个配色存一下
前端·javascript·html
dr李四维1 小时前
iOS构建版本以及Hbuilder打iOS的ipa包全流程
前端·笔记·ios·产品运营·产品经理·xcode
雯0609~2 小时前
网页F12:缓存的使用(设值、取值、删除)
前端·缓存
℘团子এ2 小时前
vue3中如何上传文件到腾讯云的桶(cosbrowser)
前端·javascript·腾讯云
学习前端的小z2 小时前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
彭世瑜2 小时前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
FØund4042 小时前
antd form.setFieldsValue问题总结
前端·react.js·typescript·html
Backstroke fish2 小时前
Token刷新机制
前端·javascript·vue.js·typescript·vue
小五Five2 小时前
TypeScript项目中Axios的封装
开发语言·前端·javascript