これは、なにをしたくて書いたもの?
前に、ECMAScript Modulesを使うように設定したNode.jsとTypeScriptを扱うエントリーを書きました。
TypeScript × Node.jsでECMAScript Modulesを試す - CLOVER🍀
この時、テストコードを書くのにJestを使ったのですが、ECMAScript Modulesとの組み合わせがなかなか難しい印象を持ったので、
esbuld-jestや@swc/jestではなくVitestを試してみることにしました。
Vitest
VitestのWebサイトはこちら。
Vitest | Next Generation testing framework
Viteネイティブな次世代のテスティングフレームワークで、高速だと謳われています。
ドキュメントはこちら。
Getting Started | Guide | Vitest
なぜVitestなのか?というページはこちら。
ドキュメントを読む前提として、Viteに詳しくなっていることが挙げられていますが…。
This guide assumes that you are familiar with Vite.
Viteはさまざまな機能を持ちますが単体テストについては明確な方針を持っておらず、テスティングフレームワークとしてはすでにJestが
ありますがJestとViteにはなんの関係もないため重複があったりします。
Viteを使ってテスト中にファイルを変換すると、アプリケーションと同じ構成(vite.config.ts
)を使ってテストランナーを作成できます。
これを実現したのがVitestで、すでに大規模に採用されているJestと互換性のあるAPIを提供しているのでJestの代替としても利用できると
されています。
主な機能については、こちらに書かれています。
ざっとこんなところでしょうか。
- Viteの設定やトランスフォーマー、リゾルバー、そしてプラグインが利用可能で、アプリケーションと同じ設定(
vite.config.ts
)でテストが可能 - HMR(Hot Module Replacemen)のようなウォッチモード
- VueやReact、Svelte、Lit、Markoなどのコンポーネントテスト
- TypeScriptやJSXのサポート
- ESM(ECMAScript Modules)ファースト、トップレベルawaitのサポート
- Tinypoolによるワーカーのマルチスレッド化
- Tinybenchによるベンチマークのサポート
- テストスイートのフィルタリング、タイムアウト、並列化
- ワークスペースのサポート
- Jest互換のスナップショット
- Chaiのビルトインアサーション+Jestのexpect互換のAPI
- Tinyspyによるビルトインのモック
- happy-domまたはjsdomによるDOMのモック
- v8またはistanbulによるコードカバレッジ
- Rustのようなインソーステスト
- expect-typeによる型テスト
- シャーディングのサポート
とりあえず、Getting Startedを見ながら試してみることにします。
Getting Started | Guide | Vitest
環境
今回の環境はこちら。
$ node --version v20.16.0 $ npm --version 10.8.1
準備
まずはNode.jsプロジェクトを作成して、TypeScriptとついでにPrettierもインストール。
$ npm init -y $ npm i -D typescript $ npm i -D @types/node@v20 $ npm i -D prettier
package.json
のtype
フィールドの値はmodule
にしてECMAScript Modulesとして扱うようにします。
"type": "module",
この時点での依存関係。
"devDependencies": { "@types/node": "^20.14.8", "prettier": "^3.3.3", "typescript": "^5.5.4" }
ソースコードはsrc
、テストコードはtest
に置くことにしましょう。
$ mkdir src test
TypeScriptの設定。
tsconfig.json
{ "compilerOptions": { "target": "esnext", "module": "nodenext", "moduleResolution": "nodenext", "lib": ["esnext"], "baseUrl": "./src", "outDir": "dist", "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "noImplicitOverride": true, "noImplicitReturns": true, "noPropertyAccessFromIndexSignature": true, "skipLibCheck": true, "esModuleInterop": true }, "include": [ "src/**/*" ] }
こちらはビルド用で、テストコードを含めた型チェック用はこちら。
tsconfig.typecheck.json
{ "extends": "./tsconfig", "compilerOptions": { "baseUrl": "./", "noEmit": true }, "include": [ "src/**/*", "test/**/*" ] }
Prettierの設定。
.prettierrc.json
{ "singleQuote": true, "printWidth": 120 }
この時点で、package.json
のscripts
はこんな感じにしておきました。
"scripts": { "build": "tsc --project .", "build:watch": "tsc --project . --watch", "typecheck": "tsc --project ./tsconfig.typecheck.json", "typecheck:watch": "tsc --project ./tsconfig.typecheck.json --watch", "format": "prettier --write src test" },
テスト対象のコードは簡単なものを用意しておきます。
src/calc.ts
export function plus(a: number, b: number): number { return a + b; } export function minus(a: number, b: number): number { return a - b; }
Vitestを使ってテストを書く
それでは、Vitestをインストールしましょう。
$ npm i -D vitest
依存関係はこうなりました。
"devDependencies": { "@types/node": "^20.14.8", "prettier": "^3.3.3", "typescript": "^5.5.4", "vitest": "^2.0.5" }
テストを書きます。
test/calc.test.ts
import { expect, test } from 'vitest'; import { plus, minus } from '../src/calc.js'; test('plus', () => { expect(plus(1, 2)).toBe(3); }); test('minus', () => { expect(minus(5, 1)).toBe(4); });
Jestと違ってexpect
やtest
はimport
しないと解決できません。
※設定で書かないようにもできます
import { expect, test } from 'vitest';
test
がわかりません、となったりします。
FAIL test/calc.test.ts [ test/calc.test.ts ] ReferenceError: test is not defined ❯ test/calc.test.ts:4:1 2| import { plus, minus } from '../src/calc.js'; 3| 4| test('plus', () => { | ^ 5| expect(plus(1, 2)).toBe(3); 6| });
実行は、vitest run
で。
$ npx vitest run
テストが実行できました。高速です。
$ npx vitest run RUN v2.0.5 /path/to ✓ test/calc.test.ts (2) ✓ plus ✓ minus Test Files 1 passed (1) Tests 2 passed (2) Start at 20:22:04 Duration 368ms (transform 46ms, setup 0ms, collect 39ms, tests 3ms, environment 0ms, prepare 91ms)
ちなみに、vitestコマンドのみだとウォッチモードになり(vitest watch
と同じ)、
DEV v2.0.5 /path/to ✓ test/calc.test.ts (2) ✓ plus ✓ minus Test Files 1 passed (1) Tests 2 passed (2) Start at 20:24:40 Duration 290ms (transform 42ms, setup 0ms, collect 36ms, tests 3ms, environment 0ms, prepare 69ms) PASS Waiting for file changes... press h to show help, press q to quit
テストを実行した後にコマンドが終了せずに、ファイルに変更がある度にテストを再実行します。
Command Line Interface | Guide | Vitest
というわけで、package.json
のscripts
はこんな感じにしました。
"scripts": { "build": "tsc --project .", "build:watch": "tsc --project . --watch", "typecheck": "tsc --project ./tsconfig.typecheck.json", "typecheck:watch": "tsc --project ./tsconfig.typecheck.json --watch", "test": "vitest run", "test:watch": "vitest watch", "format": "prettier --write src test" },
実行例。
$ npm test $ npm run test $ npm run test:watch
ところで、自然に書きましたがJestの時にはけっこう苦労した拡張子の解決ができています。しかも.js
で。
import { plus, minus } from '../src/calc.js';
次に、設定ファイルを作成してみましょう。
Getting Started / Configuring Vitest
Viteの設定ファイルであるvite.config.ts
か、Vitest専用にvitest.config.ts
を作成することもできるようですが特にこだわりがなければ
vite.config.ts
とした方がよさそうな気がします。
なお、拡張子については.js
以外(.mjs
、.cjs
、.ts
、.cts
、.mts
、.json
)をサポートしているようです。
最小の記述はこんな感じでしょうか。
vite.config.ts
/// <reference types="vitest" /> import { defineConfig } from 'vite'; export default defineConfig({ test: { } });
設定ファイルのリファレンスはこちらです。
特に明示していませんが、テスト対象となるファイルパターンは['**/*.{test,spec}.?(c|m)[jt]s?(x)']
のようです。
ここで、globals
をtrue
にすると
export default defineConfig({ test: { globals: true } });
テストコードからexpect
やtest
のimport
を削除できます。
// import { expect, test } from 'vitest';
ちなみに、この方法だとtscの方では解決できなくなるので、その場合は以下のようにvitest/globals
をtypes
に追加します。
"compilerOptions": { ... "types": ["vitest/globals"] },
Vitestのドキュメントを見ていると、import
を明示的に書かせるスタイルのようなので今後はglobals
は使わない方向にしようかなと思います。
export default defineConfig({ test: { // globals: true } });
テストレポート
テストレポートは、さまざまな形式で見ることができるようです。
今回はJUnitレポートを見てみることにします。
Reporters / Built-in Reporters / JUnit Reporter
HTMLレポートを使うには、@vitest/uiというパッケージが必要なようなのでインストールします。
今回は、JUnitレポートとデフォルトのレポートの両方を出力するように構成してみます。default
は、もともと表示されていたものですね。
export default defineConfig({ test: { reporters: ['junit', 'default'], } });
Reporters / Combining Reporters
テストを実行。
$ npm test
すると、最後にJUnitレポートが表示されます。
<testsuites name="vitest tests" tests="2" failures="0" errors="0" time="0.332"> <testsuite name="test/calc.test.ts" timestamp="2024-08-12T11:54:35.307Z" hostname="ikaruga" tests="2" failures="0" errors="0" skipped="0" time="0.003"> <testcase classname="test/calc.test.ts" name="plus" time="0.002"> </testcase> <testcase classname="test/calc.test.ts" name="minus" time="0.001"> </testcase> </testsuite> </testsuites>
今回の設定だとデフォルトのレポートも表示されるので冗長感がありますが、ファイルに出力するレポートと併用する場合はこうやって
組み合わせるとよいのかなと思います。
カバレッジ
最後にカバレッジを見てみましょう。
v8またはistanbulが選択できるようで、デフォルトはv8です。
といっても、npmパッケージのインストールは必要ですが。
カバレッジを取得する際にインストールするかどうか聞かれるのですが、今回は明示的にインストールしておきます。
$ npm i -D @vitest/coverage-v8
バージョン。
"devDependencies": { "@types/node": "^20.14.8", "@vitest/coverage-v8": "^2.0.5", "prettier": "^3.3.3", "typescript": "^5.5.4", "vitest": "^2.0.5" }
カバレッジの設定。
export default defineConfig({ test: { coverage: { provider: 'v8' }, } });
デフォルトを明示したことになります。
カバレッジを取得するには、--coverage
フラグを明示的に渡すか
$ npx vitest run --coverage
coverage.enabled
をtrue
にします。
export default defineConfig({ test: { coverage: { enabled: true, provider: 'v8' }, } });
実行すると、最後にカバレッジレポートが表示されます。
% Coverage report from v8 ----------|---------|----------|---------|---------|------------------- File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s ----------|---------|----------|---------|---------|------------------- All files | 100 | 100 | 100 | 100 | calc.ts | 100 | 100 | 100 | 100 | ----------|---------|----------|---------|---------|-------------------
デフォルトではテキスト、HTML、clover、JSON形式でレポートが出力されます。
ファイルで出力されるものは、coverage
ディレクトリに出力されています。
$ ll coverage 合計 80 drwxrwxr-x 2 xxxxx xxxxx 4096 8月 12 21:03 ./ drwxrwxr-x 6 xxxxx xxxxx 4096 8月 12 21:03 ../ -rw-rw-r-- 1 xxxxx xxxxx 5394 8月 12 21:03 base.css -rw-rw-r-- 1 xxxxx xxxxx 2655 8月 12 21:03 block-navigation.js -rw-rw-r-- 1 xxxxx xxxxx 3754 8月 12 21:03 calc.ts.html -rw-rw-r-- 1 xxxxx xxxxx 972 8月 12 21:03 clover.xml -rw-rw-r-- 1 xxxxx xxxxx 1369 8月 12 21:03 coverage-final.json -rw-rw-r-- 1 xxxxx xxxxx 445 8月 12 21:03 favicon.png -rw-rw-r-- 1 xxxxx xxxxx 4346 8月 12 21:03 index.html -rw-rw-r-- 1 xxxxx xxxxx 676 8月 12 21:03 prettify.css -rw-rw-r-- 1 xxxxx xxxxx 17590 8月 12 21:03 prettify.js -rw-rw-r-- 1 xxxxx xxxxx 138 8月 12 21:03 sort-arrow-sprite.png -rw-rw-r-- 1 xxxxx xxxxx 6181 8月 12 21:03 sorter.js
HTML出力結果の例。
今回はこんなところでしょうか。
おわりに
ECMAScript Modulesを有効にしたNode.jsプロジェクトで、TypeScriptと組み合わせてVitestを使ってテストを書いてみました。
実はとっかかりでやや迷子になったのですが、1度動かせるとあとはは割とすらすらといったのでまあよいかなと。
高速ですし、ECMAScript Modulesへの対応もよさそうなのでJestから切り替えていこうかなと思います。