Skip to content
成为赞助商

测试

基础概念

单元测试

  • 优势:快速反馈及时发现bug、改善代码质量及可维护性、check他人代码、活文档
  • 最佳写测试时机:
    • 通过单测替代手动验证
    • 先写测试再实现业务逻辑,TDD敏捷开发
js
// 入门体验
import { expect, it, describe, beforeEach, test } from "vitest";
import { useTodoStore } from './todo';
import { createPinia, setActivePinia } from 'pinia';

describe("todo", () => {

  test("新增一个todo", () => {
    // 1、准备数据
    setActivePinia(createPinia())
    const todoStore = useTodoStore()
    const title = "吃饭"
    // 2、调用
    todoStore.addTodo(title)
    // 3、验证
    expect(todoStore.todos[0].title).toBe(title)
    // 4、重置
    todoStore.reset()
  })

  test("reverse Todo的内容", () => {
    // 1、准备数据
    setActivePinia(createPinia())
    const todoStore = useTodoStore()
    const title = "reverse:HeiHei"
    // 2、调用
    todoStore.addTodo(title)
    // 3、验证
    expect(todoStore.todos[0].title).toBe('ieHieH')
    // 4、重置
    todoStore.reset()
  })
});

Vitest

命令行

bash
vitest # 在当前目录中启动 Vitest,开发环境默认为监听模式,CI 环境会自动进入运行(run)模式
vitest xxx # xxx替换为路径名,将仅运行路径中包含 xxx 的测试文件
vitest run # 执行单次运行
vitest watch # 运行所有测试套件,监听变化并在变化时重新运行测试【别名:vitest dev】

vitest related # ???
vitest bench # ???
vitest init # ???

测试条件

指定超时阈值

  • 以毫秒为单位,作为第三个参数传递给测试。默认值为 5 秒
ts
import { test,beforeAll  } from 'vitest'

test('name', async () => {
  /* ... */
}, 1000)

beforeAll(async () => {
  /* ... */
}, 1000)


// vitest.config.js
export default defineConfig({
  test: {
    exclude: [],
    testTimeout:5000, // 超时阈值 默认值为 5 秒
  },
})

skip跳过测试

  • .skip 跳过/避免 运行某些测试套件或测试
    • skip修饰的测试不会被执行
  • .skipIf(xxx)('remark 注释',()=>{...}) 当xxx为true时,跳过测试内容
  • .runIf(xxx)('remark 注释',()=>{...}) 仅当xxx为true时,执行测试内容,与skipIf 相反
ts
import { assert, describe, it } from 'vitest'

describe.skip('skipped suite', () => {
  it('test', () => {
    // 已跳过此测试套件,无错误
    assert.equal(Math.sqrt(4), 3)
  })
})

describe('suite', () => {
  it.skip('skipped test', () => {
    // 已跳过此测试,无错误
    assert.equal(Math.sqrt(4), 3)
  })
})

only选择测试

  • .only 仅运行某些测试套件或测试
  • 存在only修饰后,未被修饰的测试不会被执行
ts
import { assert, describe, it } from 'vitest'

// 仅运行此测试套件(以及标记为 Only 的其他测试套件)
describe.only('suite', () => {
  it('test', () => {
    assert.equal(Math.sqrt(4), 3)
  })
})

describe('another suite', () => {
  it('skipped test', () => {
    // 已跳过测试,因为测试在 Only 模式下运行
    assert.equal(Math.sqrt(4), 3)
  })

  it.only('test', () => {
    // 仅运行此测试(以及标记为 Only 的其他测试)
    assert.equal(Math.sqrt(4), 2)
  })
})

concurrent并发测试

  • 会将所有测试标记为并发测试,包含深层级
  • 快照和断言必须使用本地测试上下文中的 expect ,以确保检测到正确的测试
ts
import { describe, test } from 'vitest'

// 此测试套件中的所有测试套件和测试将并行运行。
describe.concurrent('suite', () => {
  test('concurrent test 1', async () => {
    /* ... */
  })
  describe('concurrent suite 2', async () => {
    test('concurrent test inner 1', async () => {
      /* ... */
    })
    test('concurrent test inner 2', async () => {
      /* ... */
    })
  })
  test.concurrent('concurrent test 3', async () => {
    /* ... */
  })
})

sequential顺序测试

  • 将每个测试标记为顺序测试,在concurrent并发测试中非常有用
ts
import { describe, test } from 'vitest'

describe.concurrent('suite', () => {
  test('concurrent test 1', async () => {
    /* ... */
  })
  test('concurrent test 2', async () => {
    /* ... */
  })

  describe.sequential('', () => {
    test('sequential test 1', async () => {
      /* ... */
    })
    test('sequential test 2', async () => {
      /* ... */
    })
  })
})

shuffle随机测试

  • 以随机顺序运行所有测试的方法
  • 配置选项 / 方法
ts
import { describe, test } from 'vitest'

// 或 `describe('suite', { shuffle: true }, ...)`
describe.shuffle('suite', () => {
  test('random test 1', async () => {
    /* ... */
  })
  test('random test 2', async () => {
    /* ... */
  })
  test('random test 3', async () => {
    /* ... */
  })

  // `shuffle` 是继承的
  describe('still random', () => {
    test('random 4.1', async () => {
      /* ... */
    })
    test('random 4.2', async () => {
      /* ... */
    })
  })

  // 禁用内部的 shuffle
  describe('not random', { shuffle: false }, () => {
    test('in order 5.1', async () => {
      /* ... */
    })
    test('in order 5.2', async () => {
      /* ... */
    })
  })
})
// 顺序取决于配置中的 `sequence.seed` 选项(默认为 `Date.now()`)

for/each 循环测试

js
import { describe, expect, test } from 'vitest'
import { urlParamsToObj } from '../index.ts'

describe("urlParamsToObj", () => {
  test.each([
    { i: '?foo=bar&baz=qux', o: { foo: "bar", baz: "qux" } },
    { i: '', o: {} },
    { i: '?foo=bar', o: { foo: "bar" } }
  ])('$i', ({ i, o }) => {
    expect(urlParamsToObj(i)).toEqual(o);
  });
})

todo未实现测试

  • .todo 留存将要实施的测试套件和测试的待办事项
    • 在测试过程输出测试过程,TODO的提示
ts
import { describe, it } from 'vitest'

// 此测试套件的报告中将显示一个条目
describe.todo('unimplemented suite')

// 此测试的报告中将显示一个条目
describe('suite', () => {
  it.todo('unimplemented test')
})

API

test

定义了一组相关的期望,接收测试名称和保存测试期望的函数

  • 类型: (name: string | Function, fn: TestFunction, timeout?: number | TestOptions) => void
  • 可提供超时(毫秒)时间。 默认5s,可以通过 testTimeout 进行全局配置
js
import { expect, test } from 'vitest'

test('should work as expected', () => {
  expect(Math.sqrt(4)).toBe(2)
})

describe

在当前上下文中定义一个新的测试套件,作为一组相关测试或基准以及其他嵌套测试套件,包裹多项测试内容,使呈现的结果更有层次

ts
import { describe, expect, test } from 'vitest'

function numberToCurrency(value: number | string) {
  if (typeof value !== 'number') {
    throw new TypeError('Value must be a number')
  }

  return value
    .toFixed(2)
    .toString()
    .replace(/\B(?=(\d{3})+(?!\d))/g, ',')
}

describe('numberToCurrency', () => {
  describe('given an invalid number', () => {
    test('composed of non-numbers to throw error', () => {
      expect(() => numberToCurrency('abc')).toThrowError()
    })
  })

  describe('given a valid number', () => {
    test('returns the correct currency format', () => {
      expect(numberToCurrency(10000)).toBe('10,000.00')
    })
  })
})

expect

用于创建断言,期待什么得到什么

  • soft 断言失败时,继续运行并将失败标记为测试失败,直到测试完成
  • poll 重新运行直到成功为止,可设置 intervaltimeout 选项来配置 重新运行的次数
  • toBe 表示全等,若是对象函数等,应表示它们的引用一致
  • toEqual 用于比较对象的内容是否一致,而不考虑它们的引用指针
  • toBeFalsy 判断内容是否转化为false,而不关心具体的值时
    • 除了 falsenullundefinedNaN0-00n""document.all 以外,JavaScript 中的一切都是true
  • toContain 判断数组/字符串是否包含指定的内容
  • toThrowError 在被调用时是否会抛出对应的错误
ts
import { expect, test } from 'vitest'

// soft 失败时继续运行并标记为失败
test('expect.soft test', () => {
  expect.soft(1 + 1).toBe(3) // mark the test as fail and continue
  expect(1 + 2).toBe(4) // failed and terminate the test, all previous errors will be output
  expect.soft(1 + 3).toBe(5) // do not run
})

// toBeFalsy 判断返回值是否为false 
import { Stocks } from './stocks.js'
const stocks = new Stocks()
test('if Bill stock hasn\'t failed, sell apples to him', () => {
  stocks.syncStocks('Bill')
  expect(stocks.stockFailed('Bill')).toBeFalsy()
})

// toContain 是否包含指定的内容
import { getAllFruits } from './stocks.js'
test('the fruit list contains orange', () => {
  expect(getAllFruits()).toContain('orange')

  const element = document.querySelector('#el')
  // element has a class
  expect(element.classList).toContain('flex')
  // element is inside another one
  expect(document.querySelector('#wrapper')).toContain(element)
})


// toThrow 判断函数内部是否抛出对应的错误
test("toThrow", () => {
  function getName(name) {
    if (typeof name !== "string") {
      throw new Error("错误的name");
    }
    return "hei";
  }

  expect(() => { getName(111); }).toThrow("错误的name");
});

生命周期

  • 执行顺序: beforeAll >beforeEach > test > afterEach > afterAll
访客总数 总访问量统计始于2024.10.29