单元测试-vitest笔记

发布于:2024-12-22 ⋅ 阅读:(43) ⋅ 点赞:(0)

一、关于测试

1、测试分类

  • 单元测试:unit test
  • 集成测试
  • 端对端测试:e2e

2、通用测试框架:

  • jest
  • vitest

只能做js、ts测试。运行环境是node,没有dom和window

如果需要测试dom,需要安装jsdom,用于在node环境中模拟dom

二、vitest

1、基本使用

  1. 安装
npm i vitest -D
  1. 配置启动命令
"scripts": {
  "test": "vitest"
}
  1. 新建以.spec.ts.test.ts结尾的文件
import { expect, test } from 'vitest'

test('测试通用匹配', () => {
    const name = 'button'
    // expect 用于创建断言,toBe 可用于断言基元是否相等或对象共享相同的引用。 
    expect(name).toBe('button')
    expect(2 + 2).toBe(4)
    expect(2 + 2).not.toBe(5)
})

2、测试不同类型

2.1 测试boolean值

test('测试true或false', () => {
    expect(true).toBeTruthy() // 断言值在转换为布尔值时为 true
    expect(false).toBeFalsy() // 断言值在转换为布尔值时为 false
})

2.2 测试null或undefined

test('测试null或undefined', () => {
    expect(null).toBeNull()
    expect(undefined).toBeUndefined()
})

2.3 测试number大小

test('测试number', () => {
    expect(4).toBeGreaterThan(3) // 4大于3
    expect(4).toBeGreaterThanOrEqual(4) // 4大于等于4
    expect(4).toBeLessThan(5) // 4小于5
})
  • 测试对象

⚠️注意:测试对象时需要注意,使用toBe会测试不通过,因为对象是引用类型

使用toEqual:用于比较两个值是否相等,支持深层对象和数组的比较。

test('测试对象', () => {
    // 正确写法
    // toEqual:用于比较两个值是否相等,支持深层对象和数组的比较。
    expect({ name: 'button' }).toEqual({ name: 'button' })
    // 错误写法
    // expect({ name: 'button' }).toBe({ name: 'button' })
})

2.4 测试回调函数

使用vi.fn:创建函数的监视程序,返回值callback(一个在需要地方调用的函数)

  • toHaveBeenCalled:已经被调用
  • toHaveBeenCalledWith:被调用时传入的参数
import { expect, test, describe, vi } from 'vitest'
import { testFn } from './utils'

// describe:将测试用例分组
describe('test function', () => {
    test('创建模拟函数', () => {
        // vi.fn():创建函数的监视程序
        const callback = vi.fn() // 监听callback函数
        testFn(11, callback)
        expect(callback).toHaveBeenCalled() // toHaveBeenCalled:已经被调用
        expect(callback).toHaveBeenCalledWith(11) // toHaveBeenCalledWith:被调用时传入的参数
    })
})
// utils.ts
import axios from 'axios'

export function testFn(number: number, callback: Function) {
    if (number > 10) {
        callback(number)
    }
}

2.5 监控对象上的方法

使用vi.spyOn,创建与 vi.fn()类似的对象的方法或 getter/setter 的监听(spy) 。它会返回一个 mock 函数 。

  • toHaveReturnedWith:返回值
  • toHaveBeenCalledTimes:被调用次数
import { expect, test, describe, vi } from 'vitest'

describe('test obj.fn', () => {
    test('监控对象上的方法', () => {
        const obj = {
            getName: () => 1
        }
        // 创建监听obj.getName方法
        const spy = vi.spyOn(obj, 'getName')
        obj.getName()
        expect(spy).toHaveBeenCalled()
        expect(spy).toHaveReturnedWith(1) // toHaveReturnedWith:返回值
        obj.getName()
        expect(spy).toHaveBeenCalledTimes(2) // toHaveBeenCalledTimes:被调用次数
    })
})

2.6 模拟第三方模块(以axios为例)

  1. 需要在测试的异步方法中使用axios
// utils.ts
import axios from 'axios'

export async function testAxios() {
    const { data } = await axios.get('https://xxx.com')
    return data
}
  1. 在测试文件中导入axios,将axios替换一个带ts类型的模拟axios
  • mockImplementation:模拟axios成功的结果,手动一个promise对象
  • mockResolvedValue:模拟axios成功的结果,返回一个promise对象
import { expect, test, describe, vi, Mocked } from 'vitest'
import { testAxios } from './utils'
import axios from 'axios'
vi.mock('axios') // 模拟axios模块,后续的axios调用其实是替换了模拟的axios模块
const mockAxios = axios as Mocked<typeof axios> // 将axios替换成有ts类型的axios模块

describe('test axios', () => {
    test('模拟第三方模块', async () => {
        // 写法1: mockImplementation:模拟实现,返回一个promise对象
        mockAxios.get.mockImplementation(() => Promise.resolve({ data: 123 }))
        // 写法2: mockResolvedValue:模拟实现,返回一个promise对象
        mockAxios.get.mockResolvedValue({ data: 123 })
        const result = await testAxios() // 相当于调用testAxios返回的data是mockAxios模拟返回的数据
        expect(result).toBe(123)
    })
})

代码讲解:

1、

import axios from 'axios'
vi.mock('axios') // 模拟axios模块,后续的axios调用其实是替换了模拟的axios模块
const mockAxios = axios as Mocked<typeof axios> // 将axios替换成有ts类型的axios模块

这里是导入axios,用vitest的axios去替换真实的axios(类似“狸猫换太子”)。mockAxios是将vitest的axios重新赋值,变为一个带有ts类型的axios

2、

mockAxios.get.mockImplementation(() => Promise.resolve({ data: 123 }))

mockAxios.get:就是axios.get()方法,发送get请求

3、覆盖率测试

  1. 安装@vitest/coverage-v8(覆盖率测试安装包)
npm i @vitest/coverage-v8 -D
  1. 添加脚本
"scripts": {
  "coverage": "vitest run --coverage"
}
  1. 配置vite.config.ts
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    coverage: {
      include: ['src/components/**'], // 哪些文件需要做覆盖率测试
      exclude: ['src/**/schema.ts'] // 排除哪些文件
    }
  }
})

三、vue、react框架测试工具

  • vue-test-utils:测试vue(https://test-utils.vuejs.org/zh/guide/)
  • @testing-library:测试vue、react(https://cn.vitest.dev/guide/browser/examples.html#examples)

1、vue-test-utils

vitest只能做js、ts测试,vue-test-utils能帮助解决测试vue组件

1.1 基本使用(包括配置vite.config.ts)

  1. 安装
npm i @vue/test-utils -D
  1. 使用

调用mount方法,渲染组件。参数1:组件实例,参数2:值

import { describe, test, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from './src/button.vue'

describe('测试 Button.vue', () => {
  test('render', () => {
    // 渲染组件
    const wrapper = mount(Button, {
        props: {
            type: 'primary'
        },
        slots: {
            default: '按钮' // 默认插槽
        }
    })
    console.log(wrapper.html());
  })
})

但是,这里运行命令会报错:

FAIL  button/button.test.ts > 测试 Button.vue > render
ReferenceError: document is not defined
 ❯ Proxy.mount ../../node_modules/.pnpm/@vue+test-utils@2.4.6/node_modules/@vue/test-utils/dist/vue-test-utils.cjs.js:8401:14
 ❯ button/button.test.ts:8:21
      6|   test('render', () => {
      7|     // 渲染组件
      8|     const wrapper = mount(Button, {
       |                     ^
      9|         props: {
     10|             type: 'primary'

ReferenceError: document is not defined意思是:没有dom环境,vitest默认环境是node,需要添加dom环境,安装jsdom

npm i jsdom -D

同时,配置vite.config.ts

/// <reference types="vitest" />
import { defineConfig  } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'

export default defineConfig({
    plugins: [
        vue(),
        vueJsx() as any,
    ],
    test: {
        /**
         * 在 Vitest 中,globals 配置项用于控制是否启用全局 API。
         *  - globals: false(默认情况):
                行为:Vitest 不会自动提供 Jest 的全局 API(如 describe、it、expect 等)。
                使用方式:你需要显式地导入这些 API。
            - globals: true
                行为:Vitest 会自动提供 Jest 的全局 API,你不需要显式导入这些 API。
                使用方式:可以直接使用这些全局 API。
         */
        globals: true, // 启用全局模式
        environment: 'jsdom', // 模拟浏览器环境
    }
})

vitest和vite可以共用同一个配置文件vite.config.ts,vitest也可以新建自己的vitest.config.ts

1.2 测试Button组件内容、类名

  • get:获取元素,找不到就报错
  • find:获取元素,找不到就返回 null
import { describe, test, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from './src/button.vue'
import { Accessibility } from '@vicons/ionicons5'

describe('测试 Button.vue', () => {
  test('basic button', () => {
    // 渲染组件
    const wrapper = mount(Button, {
        props: {
            type: 'primary'
        },
        slots: {
            default: '按钮' // 默认插槽
        }
    })
    expect(wrapper.classes()).toContain('gy-button--primary') // 测试类名是否存在
    // slot内容:可以用 get 和 find 方法
    /**
     * get 和 find 区别:
     * get 是获取元素,找不到就报错,find 是获取元素,找不到就返回 null
     */
    expect(wrapper.get('.gy-button').text()).toBe('按钮') // 测试文本内容(slot)
    expect(wrapper.find('.gy-button').exists()).toBe(true) // exists:判断元素是否存在
  })
})

1.3 测试Button组件事件

  • trigger('click'):触发事件
  • emitted():返回一个对象
  • toHaveProperty(key):测试对象上是否有某个属性
import { describe, test, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from './src/button.vue'
import Icon from '../icon/src/icon.vue'
import { Accessibility } from '@vicons/ionicons5'

describe('测试 Button.vue', () => {
  test('basic button', () => {
    // 渲染组件
    const wrapper = mount(Button, {
        props: {
            type: 'primary'
        },
        slots: {
            default: '按钮' // 默认插槽
        }
    })
    // 测试事件
    // wrapper.trigger('click')
    wrapper.get('button').trigger('click')
    console.log(wrapper.emitted()); // 返回一个对象:{ click: [ [ [MouseEvent] ] ] }
    expect(wrapper.emitted()).toHaveProperty('click'); // 测试对象上是否有某个属性
  })
})

1.4 测试Button组件的disabled属性

方法:

  • wrapper.attributes('disabled'):通过attributes获取属性
  • wrapper.find('button').element.disabled:获取到真实的DOM元素,再获取属性
  • toBeDefined():测试属性是否存在

要点:

  • 看能否获取到组件的disabled属性
  • disabled后,点击事件不会被触发
test('test button disabled', () => {
    const wrapper = mount(Button, {
        props: {
            disabled: true
        },
        slots: {
            default: '按钮'
        }
    })
    // 方法1:attributes获取属性
    expect(wrapper.attributes('disabled')).toBeDefined() // 测试属性是否存在
    // 方法2:获取到真实的DOM元素
    expect(wrapper.find('button').element.disabled).toBeDefined()

    // 判断点击后是否会发送事件(disabled不会发送事件)
    wrapper.get('button').trigger('click')
    console.log(wrapper.emitted()); // {}
    expect(wrapper.emitted()).not.toHaveProperty('click') // 测试对象上是否有某个属性
})

1.5 测试Button组件嵌套的Icon组件

**不模拟Icon组件(不忽略Icon组件测试):**也就是在slots.icon插槽中填入icon

import Icon from '../icon/src/icon.vue'
import { Accessibility } from '@vicons/ionicons5' // 第三方图标

test('test button icon测试1:不忽略icon组件', () => {
    const icon = mount(Icon, {
        slots: {
            default: Accessibility
        },
    })
    const wrapper = mount(Button, {
        props: {
            iconPlacement: 'left'
        },
        slots: {
            default: '按钮',
            icon: icon
        }
    })
    console.log(wrapper.html());
    // icon组件是否有图标
    const iconElement = icon.findComponent(Accessibility) // 获取到组件实例
    expect(iconElement.exists()).toBeTruthy()
    // button组件是否包含icon
    const buttonElement = icon.findComponent(Icon)
    expect(buttonElement.exists()).toBeTruthy()
})

模拟Icon组件(忽略Icon组件测试)

在icon组件中,如果要引入图标,测试起来会慢一些,使用stubs将该组件忽略掉,就不会对Icon组件进行测试

test('test button icon测试2:忽略icon组件', () => {
    const wrapper = mount(Button, {
        props: {
            loading: true,
            iconPlacement: 'left'
        },
        slots: {
            default: '按钮',
            icon: 'icon'
        },
        global: {
           stubs: ['Icon'] // 忽略icon组件(当icon是依赖第三方的,可以使用stubs忽略)
        }
    })
    console.log(wrapper.html());
    // button组件是否包含icon
    const buttonElement = wrapper.findComponent(Icon)
    expect(buttonElement.exists()).toBeTruthy()
})

1.6 测试tsx

下面是一个tsx写的组件:

在tsx中使用插槽,需要用useSlots()这个hook

// VNode.tsx
import { defineComponent, useSlots } from 'vue'

export default defineComponent({
  props: {
    title: {
      type: String,
      default: 'world'
    }
  },
  setup(props) {
    const slots = useSlots()
    return () => (
      <div>
        <h1>hello + {props.title}</h1>
        {slots.default?.() as any}
      </div>
    )
  }
})

测试代码:

有两种写法:

  1. 使用h渲染函数
  2. tsx写法

1、使用h渲染函数写法:

h函数使用方法:

function h(
  type: string | Component,
  props?: object | null,
  children?: Children | Slot | Slots
): VNode

h(<div>hello</div>, props, 内容)
import { describe, expect, test } from 'vitest'
import { h } from 'vue'
import { mount } from '@vue/test-utils'
import VNode from './src/VNode'
import Button from './src/button.vue'

test('测试tsx 写法1', () => {
  const wrapper = mount(VNode, {
    props: {
      title: '标题'
    },
    slots: {
      default: () => h(Button, { type: 'primary' }, { default: () => '按钮' }) // 渲染button组件
    }
  })
  console.log(wrapper.html())
})

2、使用tsx写法:

test('测试tsx 写法2', () => {
  const wrapper = mount(() => (
    <VNode title={'标题'}>
      <Button type="primary">按钮</Button>
    </VNode>
  ), {
    props: {
      title: '标题'
    },
    slots: {
      default: () => h(Button, { type: 'primary' }, { default: () => '按钮' }) // 渲染button组件
    }
  })
  console.log(wrapper.html())
})

2、@testing-library

@testing-library有可以测试vue和react,具体用法看官网:

  • react:https://testing-library.com/docs/react-testing-library/intro/
  • vue:https://testing-library.com/docs/vue-testing-library/intro

⚠️注意:下面内容以react为例

2.1 安装依赖

"vitest": "^2.1.8",
"@testing-library/react": "^16.1.0", // 组件测试,用于渲染组件
"@testing-library/dom": "^10.4.0", // 组件测试,用于测试 dom 元素
"@testing-library/jest-dom": "^6.6.3", // 扩展测试函数
"jsdom": "^25.0.1", // 模拟 dom 环境

2.2 配置vite.config.ts

import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    globals: true, // 全局变量
    environment: 'jsdom' // 当前环境
  }
})

2.3 测试Button组件

import React from 'react'
import { describe, test, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import '@testing-library/jest-dom' // 扩展测试函数,例如:toHaveClass

import Button from '.'

describe('Button Component', () => {
  test('should render correctly', async () => {
    // 渲染组件
    const dom = await render(<Button />)
    // 获取组件:screen
    const Btn = screen.getByText('Click me')
    // 是否符合预期
    expect(dom).toBeTruthy()
    expect(Btn).toHaveClass('btn')
  })
})

2.4 测试Button组件事件

模拟函数const handleClick = vi.fn()

import React from 'react'
import { describe, test, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import '@testing-library/jest-dom' // 扩展测试函数,例如:toHaveClass

import Button from '.'

describe('Button Component', () => {
  test('should render correctly', async () => {
    // mock 函数
    const handleClick = vi.fn()
    // 渲染组件
    const dom = await render(<Button onClick={handleClick} />)
    // 获取组件:screen
    const Btn = await screen.getByText('Click me')
    // 点击
    Btn.click()
    // 是否符合预期
    expect(dom).toBeTruthy()
    expect(Btn).toHaveClass('btn')
    expect(handleClick).toBeCalled()
  })
})