Viteを利用したテストツールVitestの利用を始める

# はじめに
Viteのバージョン4.0の公開をアナウンスする[ブログ](https://vitejs.dev/blog/announcing-vite4.html)で[Vitest](https://vitest.dev/)について言及されていました。
> Vitest adoption is exploding, it will soon represent half of Vite's npm downloads. Nx is also investing in the ecosystem, and officially supports Vite.
Vitestの採用は爆発的に増えており、まもなくViteのnpmダウンロードの半分を占めるようになるでしょう。Nxもエコシステムに投資しており、Viteを公式にサポートしています。(DeepLによる翻訳)

これまではJavaScript、TypeScriptにおけるテストツールとしてはJestという成熟したツールがあるので、Vitestを利用するのは趣味だったり少し先の未来だろうと考えていました。しかし、リリースブログを読んでVite環境においてはVitestの方が使い勝手が良く積極的に利用を始めても良いのではないかと考えを改めVitestを学びました。

この記事ではそんな中で学んだVitestについて紹介していきます。

# Vitestとは
Vitestは公式で`Blazing Fast Unit Test Framework`と謳われています。とんでもなく早いユニットテストフレームワークということです。
Vitestが持つ特徴はいくつもあります。
- Viteのconfig、transformers、resolversとplugins
- アプリと同じ設定
- ViteのHMRのようなwatchモード
- Vue、React、Svelte、Litなどのコンポーネントテストをサポート
- TypeScriptやJSXのサポート
- ESMファースト, top level await
- [Tinypool](https://github.com/tinylibs/tinypool)を搭載したWorkerのマルチスレッド
- [Tinybench](https://github.com/tinylibs/tinybench)によるベンチマークをサポート
- テストスイートやテスト自身のフィルタリング、タイムアウト、並列実行
- Jest互換のスナップショット
- アサーションのための[Chai](https://www.chaijs.com/)搭載、[Jestのexpect](https://jestjs.io/docs/expect)と互換性のあるAPI
- モックのために[Tinyspy](https://github.com/tinylibs/tinyspy)を搭載
- [happy-dom](https://github.com/capricorn86/happy-dom)や[jsdom](https://github.com/jsdom/jsdom)を使ったDOMのモック
- [c8](https://github.com/bcoe/c8)か[istanbul](https://istanbul.js.org/)によるCoverage
- Rustのような実装コード内におけるテスト
- [expect-type](https://github.com/mmkal/expect-type)を用いた型のテスト

特にViteと同じ構成を利用してテストを実行できること、Jestと互換があること、watchモードが高いパフォーマンスを持つこと、ESMとTypeScriptとJSXがサポートされていることが大きな特徴を持ちます。

# Vitestの導入
VitestをVite環境に導入していきます。Vite環境は以下のコマンドで作れます。
```sh
# npm
npm create vite@latest
# yarn
yarn create vite@latest
# pnpm
pnpm create vite@latest
```
それに対してVitestは
```sh
npm i -D vitest
yarn add -D vitest
pnpm add -D vitest
```
でインストールします。今後は`pnpm`を使うとして進めていくので好みのパッケージマネージャーに読み変えてください。
次にVitestの設定を行います。最初に特徴として紹介した通りViteの設定を共有して行うことが出来ます。
Viteの設定ファイル`vite.config.ts`はデフォルトで以下のようになっています。
```ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
})
```
ここに加えていきます。設定を共有するにはまず、冒頭に`/// <reference types="vitest" />`を追加してファイルに型情報を与えます。これによって`test`をキーとしたVitestの設定を記述することができるようになります。例えばテストを実行するファイルを絞るような設定をする場合は以下のように書きます。
```ts
/// <reference types="vitest" />
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  test: {
    include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}']
  }
})
```
これで基本機能を共有したVitestの設定が完了します。`vite.config.ts`の他にも`vitest.config.ts`で設定を行うことも出来ます(`vite.config.ts`より優先度が高く反映されます)。
最後に実行コマンドの登録です。Vitestの実行は`pnpm vitest`で行うことができます(npmの`npx vitest`と同義です)。Vitestがおすすめするpackage.jsonにおけるスクリプトの設定は以下の通りです。
```json
{
  "scripts": {
    "test": "vitest",
    "coverage": "vitest run --coverage"
  }
}
```
これによって`pnpm test`でwatchモードのテストを実行することが出来ます。また、`@vitest/coverage-c8`をインストールすることで、`pnpm coverage`と実行して下のようなcoverageを出力することが出来ます。その他の設定は[こちら](https://vitest.dev/config/)を参照してください。
![スクリーンショット 2022-12-24 10.11.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/7108bb4c-b901-3959-e035-7ab5f093d1ae.png)
また、VSCodeでは[プラグイン](https://marketplace.visualstudio.com/items?itemName=ZixuanChen.vitest-explorer)を導入することで下のように便利にテストを実行できます(画像は公式より引用)。
![202203292020.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/7189db5b-80be-c521-be72-cdb7c2a70d5b.gif)
ここまでの設定でテストが実行できることを確かめるために、引数を全て足す関数とそのテストを作成して実行してみます。追加する前ではテストを実行しても件数が0なのでエラーが出て実行できないことを確認してください。
関数は以下のように定義します。
```ts:src/utils/add.ts
export const add = (...args: number[]) => args.reduce((a, b) => a + b, 0)
```
テストは下のように書けます(記法がjestと同じですね)。
```ts:src/utils/add.test.ts
import { expect, it } from 'vitest';
import { add } from "../add"

it('add', () => {
  expect(add()).toBe(0)
  expect(add(1)).toBe(1)
  expect(add(1, 2, 3)).toBe(6)
})
```
この状況で`pnmp test`と`pnpm coverage`が実行できたら基本の設定完了です。

# インポートなしで利用する
Vitestが提供するAPIをJestのようにグローバルで利用するには設定を変更する必要があります。Vitestの設定ファイルに以下のように追加します。
```ts
test: {
  global: true
}
```
TypeScriptを利用の場合はグローバルで利用するAPIの型を与えるために以下のように設定を追加します。
```json:tsconfig.json
{
  "compilerOptions": {
    "types": ["vitest/globals"]
  }
}
```
これによって先ほど記述したテスト
```ts:src/utils/add.test.ts
import { expect, it } from 'vitest';
import { add } from "../add"

it('add', () => {
  expect(add()).toBe(0)
  expect(add(1)).toBe(1)
  expect(add(1, 2, 3)).toBe(6)
})
```
は以下のように書くことが出来ます。
```ts:src/utils/add.test.ts
import { add } from "../add"

it('add', () => {
  expect(add()).toBe(0)
  expect(add(1)).toBe(1)
  expect(add(1, 2, 3)).toBe(6)
})
```

# コマンド
`pnmp vitest`のように実行できるコマンドを紹介します。
## vitest
`pnpm vitest`は置かれたディレクトリでテストを行うコマンドです。基本的にはwatchモードですが、CI環境では自動で一度きりの実行に切り替わります。また、`pnpm vitest add`のようにして、ファイル名に追加した文字列を含むテストのみを実行することも可能です。この例だと`add`を含むファイルのテストが実行されます。
## vitest watch
`pnpm vitest watch`はwatchモードで実行します。全てのテストを実行し、実行後はテストファイルの監視を行います。テストファイルの変更を検知すると、そのテストだけが再実行されます。`pnpm vitest dev`でも全く同じ挙動で実行することが出来ます。
## vitest run
`pnpm vitest run`は全てのテストを一度だけ実行するコマンドです。CI環境における`pnpm vitest`と同じ挙動をします。わかりやすさのためにCIを書くときはこちらのコマンドで記述した方が良いと考えています。
## vitest related
`vitest related`は引数に取ったテストファイルのみを実行するコマンドです。先ほどの例だと`pnpm vitest related ./src/utils/add.test.ts`のように行うことが出来ます。このコマンドは他のコマンドと合わせて使うことが出来ます。watchモードにしたい場合は`pnpm vitest watch related ./src/utils/add.test.ts`のようにすることで`watch`モードになります(コマンドラインからは元々watchモードで実行されるので動作は変わりません)。1度だけ実行したい場合は`pnpm vitest run related ./src/utils/add.test.ts`とすることで出来ます。このコマンドは[husky](https://typicode.github.io/husky/#/)と[lint-staged](https://github.com/okonet/lint-staged)を用いてコミット前に検査させるときなどで役に立ちます。試しにhuskyとlint-stagedを使ってセットアップしてみます。
まずhuskyとlint-stagedの基本的な用意を行ないます。
```shell
pnpm dlx husky-init && pnpm install
pnpm add -D lint-staged
```
次にlintstagedの設定ファイルを用意します。
```js:.lintstagedrc.cjs
const path = require('path');

const buildCommand = (filenames) => {
  const files = filenames
    .map((f) => path.relative(process.cwd(), f))
    .join(' ');

  return [
    `prettier --write ${files}`,
    `eslint --max-warnings=0 --fix ${files}`,
    `vitest run related ${files}`
  ];
}

module.exports = {
  'src/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}': buildCommand,
};
```
prettierとeslintの設定も書きましたがこれはおまけなのでVitestしか行わない場合は消してください。最後に`pnpm dlx husky-init`によって生成されたprecommitファイルを以下のように変更します。
```shell:.husky/precommit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

pnpm lint-staged
```
このように設定することでテストファイルをコミットした時にそのファイルのテストが実行される仕組みを作ることが出来ます。jsに類するファイルすべてを検査するようにしたのはVitestではソース内コードでテストが可能だからです。
横道にそれましたが、これらがVitestの持つコマンドです。

## オプション
これらのコマンドに渡せるオプションも様々なものがあります。量が多いので抜粋して消化します。
よく使うコマンドとして紹介するのが`--coverage`です。このコマンドは導入でも利用しましたが、`@vitest/coverage-c8`を使ってcoverageを表示してくれるようなコマンドです。
![スクリーンショット 2022-12-24 10.11.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/7108bb4c-b901-3959-e035-7ab5f093d1ae.png)
興味深いコマンドとして`--ui`を紹介します。`@vitest/ui`と合わせて使うオプションです。
ローカルポートを立ててブラウザ上にテストの状況やモジュールのグラフを表示してくれます。`pnpm vitest --ui`のように実行することが出来ます。
![スクリーンショット 2022-12-24 16.21.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/591069/aad36f7c-e901-bcba-70ee-c87abe1b0e74.png)
わかりやすいUIなのでテストを書くモチベーションが上がりますね。

この記事で紹介するオプションは以上です。他のオプションを知りたい場合は[こちら](https://vitest.dev/guide/cli.html#options)を参照するか、`pnpm vitest --help`で参照してください。


# テストの書き方
Vitestのテストの書き方を紹介します。ここで紹介するサンプルコードではimportを省いて書きます。
## テストの定義
テストスイートは`describe`によって定義することができ、テスト自体は`it`で定義することが出来ます。
```ts
describe('suite', () => {
  it('test', () => {
    // ここにテスト内容を書く
  }, 1000)
})
```
`it`の代わりに`test`を使うことが出来ます。`it`や`test`の第三引数にはでテストにかける最大の時間`timeout`を与えることが出来ます。
さらに`describe`と`it`や`test`には`.skip`や`.only`、`.todo`を使うことで特定のスイートやテストのスキップ、実行、todo化を行うことが出来ます。
```ts
describe.todo.only('todo and only suite', () => {
  it.skip('skip test', () => {
    //
  })
  it('normal test', () => {
    //
  })
  it.only('only test', () => {
    //
  })
})
```
他にも`.each`を使うことで簡単に異なる変数で同じテストを実行させることが出来ます。
```ts
it.each([
  [1, 2, 3],
  [1, 3, 2],
  [2, 1, 3],
  [2, 3, 1],
  [3, 1, 2],
  [3, 2, 1],
])('it pattern(%i, %i, %i)' (a, b, c) => {
  //
})
```
ここまでの機能はJestにも同等のものがありましたが、Vitestでは他に条件に当てはまった場合のみスキップする`.skipIf`、条件に当てはまった時のみ実行する`.runIf`、並列してテストを実行させる`.concurrent`、テストが失敗した時だけ通過させる`.fails`が`it`、`test`で利用できます。それぞれ以下のように使えます。
```ts
it.skipIf(import.meta.env.DEV).('Dev環境ではスキップされるテスト', () => {
  //
})
it.runIf(import.meta.env.DEV).('Dev環境でだけ実行されるテスト', () => {
  //
})
test.concurrent('test', () => {
  //
})
test.concurrent('testと並列して実行されるテスト', () => {
  //
})
test.fails('失敗すると通過する', () => {
  // 失敗するテストを書く
})
```
環境によって実行させるテストを選択できたり、並行でテストができるのは大変便利なので積極的に利用していきたいです。

`describe`にもVitest固有の機能があります。`it`などにもあった`.concurrent`と中身をランダムな順番で実行させる`.shuffle`です。`.convurrent`はそのスイートのテスト全てが並列実行されます。`.shuffle`で行われるランダムな選択はコマンドオプションや設定で指定させることも出来ます。

## expect
expectはJestの書き方とChaiの書き方の両方を使えます。等しいことを表すコードは以下の二種類を書くことが出来ます。
```ts
// Chai
expect(1).to.eqaul(1);
// Jest
expect(1).toBe(1);
```
expectに関するAPIはたくさん用意されてことに加えて多くがJestやChaiに共通するのでこの記事ではこれ以上紹介しません。気になる場合は[APIリファレンス](https://vitest.dev/api/#expect)を参照してください。

## テスト前後の処理
JestにもあったようにVitestでも`beforeEach`、`beforeAll`、`afterEach`、`afterAll`を利用できます。`beforeEach`には各テストが実行される前に発火させる関数を、`beforeAll`にはスイート内の最初のテストが実行される前に発火させる関数を、`afterEach`には各テストが実行された後に発火させる関数を、`afterAll`にはスイート内の最後のテストが実行された後に発火させる関数を配置します。
```ts
describe('suit', () => {
  beforeEach(() => {
    console.log(1);
  })
  beforeAll(() => {
    console.log(2);
  })
  afterEach(() => {
    console.log(3);
  })
  afterAll(() => {
    console.log(4);
  })
  it('test1', () => {
    //
  });
  it('test1', () => {
    //
  });
})
```
このようなテストがあった場合は2 => 1 => 3 => 1 => 3 => 4の順番で出力されます。

## vi
Vitestにはユーティリティ関数として`vi`が提供されています。これを用いてさまざまなことが出来ます。代表的なのはモックに関わる機能です。
`vi`は`import vi from 'vitest'`で利用することが出来ます。グローバルなAPIなので設定によってはインポートなして利用できます。
`vi`から利用できるAPIは[こちら](https://vitest.dev/api/#vi)に書かれています。

## モック
Vitestには二種類のモックが存在します。`vi.spyOn`と`vi.fn`です。`vi.spyOn`は関数の振る舞いは変えずに、呼び出された回数の計測などを行うのに対して`vi.fn`はテストのための関数を新しく作成します。
```ts
import * as calc from './add';

describe('add', () => {
  afterEach(() => {
    vi.restoreAllMocks()
  })

  it('spyは特定の関数をテスト用の関数に変更するイメージ', () => {
    const spy = vi.spyOn(calc, 'add')
    expect(spy.getMockName()).toEqual('add')

    expect(calc.add()).toEqual(0)
    expect(spy).toHaveBeenCalledTimes(1)

    spy.mockReturnValue(10)
    expect(calc.add()).toEqual(10)
    expect(spy).toHaveBeenCalledTimes(2)
  })

  it('fnはテスト用の関数を新たに作成するイメージ', () => {
    const mock = vi.fn(() => 5);

    expect(mock()).toEqual(5)
    expect(mock).toHaveBeenCalledTimes(1)

    mock.mockReturnValue(10)
    expect(mock()).toEqual(10)
    expect(mock).toHaveBeenCalledTimes(2)
  })
})

```
実際のテストの挙動を見ると`vi.spy`の方はあくまで元の関数があってそれに対する振る舞いの変更や監視を行っているのに対して、`vi.fn`では監視を行うテスト用の関数を新たに作成していることがわかります。`afterEach`でテストが一つ終わるたびにモックを全て元に戻すための機能`vi.restoreAllMocks`を行っています。この処理を行わないとバグに繋がりますので必ず行うようにしてください。
これらの機能が使われる実際のケースに近いものとしては以下のようなものが考えられます。
```ts:calc.ts
import { add } from "./add";

export const calcTwoNumbers = (
  calc: (a: number, b: number) => number,
  a: number,
  b: number,
) => calc(a, b)

export const addTwoNumbers = (
  a: number,
  b: number,
) => add(a, b)
```
```ts:calc.test.ts
import * as calc from './add';
import { addTwoNumbers, calcTwoNumbers } from './calc';

describe('spyとfnそれぞれでcalcのテストを書く', () => {
  beforeEach(() => {
    vi.resetAllMocks()
  })
  it('spy', () => {
    const spy = vi.spyOn(calc, 'add')

    spy.mockReturnValue(3)
    expect(calc.addTwoNumbers(calc.add, 1, 1)).toBe(3)

    expect(spy).toHaveBeenCalledTimes(1);
  })
  it('fn', () => {
    const mock = vi.fn(() => 3)

    expect(calc.calcTwoNumbers(mock, 1, 1)).toBe(3)

    expect(mock).toHaveBeenCalledTimes(1);
  })
})
```
`vi.spy`は内部で使われている関数に対しても監視させることが可能である点が有利です。一方の`vi.fn`は引数に渡す関数をブラックボックス化して他の関数に依存しないテストを作れる点で優れています。
このように`vi.spy`と`vi.fn`はカテゴリの異なる機能ですが、`mockReturnValue`や`mockImplementationOnce`など呼び出すことのできるメソッドは共通しています。呼び出せるメソッドはMockInstanceメソッドと呼ばれていて[ここ](https://vitest.dev/api/#mockinstance-methods)に書かれています。

## スナップショット
過去の結果と同じであることを確かめるためのテストであるスナップショットテストがVitestには用意されています。
```ts
it('snapshot', () => {
  expect(1).toMatchSnapshot()
});
```
このファイルを実行すると同じディレクトリに`__snapshots__`ディレクトリが生成され、そこに`add.test.ts.snap`というファイルが生成されます(実行したファイルは`add.test.ts`です)。実行したときに過去のテストの記録が存在したときは参照して値の変化がないかを確認するテストに変化します。値に変更があった場合はエラーが出るので間違いではなく更新したい場合は実行したコマンドラインで`u`を入力してください。
他のスナップショットテストとして`toMatchInlineShapshot`があります。このAPIは`toMatchSnapshot`のように別ファイルに書くのではなくテストファイルに直接書き込みます。
```ts
it('inline snapshot', () => {
  expect(1).toMatchInlineSnapshot()
});
```
上のテストを実行するとスナップショットが取られ、
```ts
it('inline snapshot', () => {
  expect(1).toMatchInlineSnapshot("1")
});
```
のようになります。`toMatchInlineSnapshot`の引数に過去の値が書き込まれています。引数がある場合は`expect`に渡された引数と比較するテストに変化します。

これらのAPIはコマンドラインで`u`を入力してリセットすることも可能ですが、`pnpm vitest --update`または、`pnpm vitest -u`でもリセットすることが出来ます。

この機能におけるJestとの差分は2点あります。
1点目は`toMatchSnapshot`を行った時に出力されるファイルのコメントが`// Jest Snapshot v1`から`// Vitest Snapshot v1`となっている点です。
もう一点はVitestのデフォルトでは[pretty-format](https://github.com/facebook/jest/tree/main/packages/pretty-format)の記法を利用できない点です。
以下のようなコードは失敗します。
```ts
it('snapshot', () => {
  const bar = [1, 2, 3]

  expect(bar).toMatchInlineSnapshot(`Array [1, 2, 3]`)
})
```
pretty-formatを有効にしたい場合は設定ファイルのtestに下のような設定を追加してください。
```ts
snapshotFormat: {
  printBasicPrototype: true
}
```

## 型テスト
Vitestでは型を利用したテストを実行できます。型テストは`expectTypeOf`と`assertType`の二つのAPIを利用します。
テストは以下のように書けます。
```ts
it('type test', () => {
  expectTypeOf([1, 2, 3]).toEqualTypeOf<number[]>();
  assertType<[1, 2, 3]>([1, 2, 3]);
})
```
`toEquakTypeOf`は型引数の型になっていることを確認します。型に対する検証メソッドは他にもたくさんあります。例えば`toMatchTypeOf`では`expectTypeOf`に渡した型が型引数の型に含まれていることを検査します。他のメソッドについては[こちら](https://vitest.dev/api/#expecttypeof)を参照してください。

型テストは他のテストと区別して実行させることが出来ます。型テストだけを行うテストは`*.test-d.ts`のような形式のファイルに記述します。他の形式でも行いたい場合は設定ファイルの`typecheck.include`を編集してください。
型テストを実行するコマンドは先ほど紹介できませんでしたが`pnpm vitest typecheck`です。これによって実行されるテストは構文解析されるだけなのでより高速に結果を得ることが出来ます。構文の解析しか行われないので`runIf`や`skipIf`などコードの実行が必要となる機能は利用できないことに注意してください。

## ソース内でテスト実行
VitestではRustのように実装コード内でテストを書くことが出来ます。まずは設定を変更する必要があります。
```ts
test: {
  includeSource: ['src/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
}
```
さらにTypeScriptを利用している場合は型情報`vitest/importMeta`を追加します(`import.meta.vitest`の型)。
```ts
{
  "compilerOptions": {
    "types": [
      "vitest/importMeta"
    ]
  }
}
```
下ように設定後は以下のようにソースコード内にテストを埋め込んでVitestのテストを行うことが可能となります。
```ts:add.ts
export const add = (...args: number[]) => args.reduce((a, b) => a + b, 0)

if (import.meta.vitest) {
  const { it, expect } = import.meta.vitest
  it('add', () => {
    expect(add()).toBe(0)
    expect(add(1)).toBe(1)
    expect(add(1, 2, 3)).toBe(6)
  })
}
```
`import.meta.vitest`で分岐させていることで本番ではこの中身は実行されませんが、バンドルファイルには含まれてしまいます。これはパフォーマンスの低下につながるので`vite.config.ts`のViteの設定で以下のようにすることで含まれないようにしましょう(testの配下ではないので注意してください)。
```ts
define: {
  'import.meta.vitest': 'undefined',
},
```

# 環境
Vitestはデフォルトではnode環境で実行できるテストのみが対象となっています。ブラウザで実行できるようなテストを書きたい場合は[jsdom](https://github.com/jsdom/jsdom)または[happy-dom](https://github.com/capricorn86/happy-dom)を利用します。
happy-domはjsdomよりも高速であるのに対して、APIがjsdomほど豊富でないという特徴があります。
利用するには利用したいパッケージをインストールしてvitestの設定に追加します。
```ts
test: {
  environment: 'happy-dom',
}
```
この例ではhappy-domを利用すると仮定して`'happy-dom'`と設定しましたが、`'jsdom'`と設定することでjsdomを利用することが出来ます。
この他にもデフォルトの`node`やvecelが開発する[`'edge-runtime'`](https://edge-runtime.vercel.app/packages/vm)を設定することが可能です。

# おわりに
提供されるAPIの数が多いので全てを説明することはできませんでしたが、テストを書く上で不便ない程度で紹介できたのではないでしょうか。Jestを使っていた人も移行しやすく、高速ですのでViteを利用していて他のテストツールをご利用の場合はVitestの採用を検討してみてはいかがでしょうか。