CLOVER🍀

That was when it all began.

VitestでTypeScript × Node.js(ECMAScript Modules)のテストを書く

これは、なにをしたくて書いたもの?

前に、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なのか?というページはこちら。

Why Vitest | Guide | Vitest

ドキュメントを読む前提として、Viteに詳しくなっていることが挙げられていますが…。

This guide assumes that you are familiar with Vite.

Viteはさまざまな機能を持ちますが単体テストについては明確な方針を持っておらず、テスティングフレームワークとしてはすでにJestが
ありますがJestとViteにはなんの関係もないため重複があったりします。

Viteを使ってテスト中にファイルを変換すると、アプリケーションと同じ構成(vite.config.ts)を使ってテストランナーを作成できます。
これを実現したのがVitestで、すでに大規模に採用されているJestと互換性のあるAPIを提供しているのでJestの代替としても利用できると
されています。

主な機能については、こちらに書かれています。

Features | Guide | Vitest

ざっとこんなところでしょうか。

  • 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.jsontypeフィールドの値は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.jsonscriptsはこんな感じにしておきました。

  "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と違ってexpecttestimportしないと解決できません。
※設定で書かないようにもできます

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.jsonscriptsはこんな感じにしました。

  "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: { }
});

設定ファイルのリファレンスはこちらです。

Configuring Vitest | Vitest

特に明示していませんが、テスト対象となるファイルパターンは['**/*.{test,spec}.?(c|m)[jt]s?(x)']のようです。

Configuring Vitest / include

ここで、globalstrueにすると

export default defineConfig({
  test: {
    globals: true
  }
});

テストコードからexpecttestimportを削除できます。

// import { expect, test } from 'vitest';

ちなみに、この方法だとtscの方では解決できなくなるので、その場合は以下のようにvitest/globalstypesに追加します。

  "compilerOptions": {
    ...

    "types": ["vitest/globals"]
  },

Vitestのドキュメントを見ていると、importを明示的に書かせるスタイルのようなので今後はglobalsは使わない方向にしようかなと思います。

export default defineConfig({
  test: {
    // globals: true
  }
});

テストレポート

テストレポートは、さまざまな形式で見ることができるようです。

Reporters | Guide | Vitest

今回は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>

今回の設定だとデフォルトのレポートも表示されるので冗長感がありますが、ファイルに出力するレポートと併用する場合はこうやって
組み合わせるとよいのかなと思います。

カバレッジ

最後にカバレッジを見てみましょう。

Coverage | Guide | Vitest

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.enabledtrueにします。

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から切り替えていこうかなと思います。