Node.js の話や Vitest の話など、色々詰め合わせになっています。
何かの参考用というより、他人が書いた勉強ノート的なものとして見ていただければと思います。
背景
しばらくの間、JavaScript のユニットテストは、テスト用の HTML と JavaScript を書いて、ブラウザで実行してコンソール確認して、という形をとっていました。
ユニットテストフレームワークを避けていたのは、npm とか Node.js 関係の環境構築を食わず嫌いしてきたためです。(フレームワークを使うには Node.js か Deno を導入する他ない)
ただ、さすがにブラウザで手動テストは面倒だしダサいと感じてきたので、大人しく Node.js/npm 周りと和解し、Vitest を使ったテストをすることにしたのでした。
自動で回せるようになれば、GitHub Actions でも push の度にテストを回せますから、常にテストし続けてコードの質を保つことにつながります。
ということで、この記事は Node.js 関係の環境構築、および npm の扱いからスタートして、GitHub Actions で Vitest を動かすまでの手順というか、道のりを記しています。
ローカル上で Vitest を準備して動かすまで
まずはローカル環境上で Vitest を利用できるようにします。
Node.js の準備
Vitest は v18.0.0 以上の Node.js が必要なようです。ところが、apt から入れられる Node.js は古く、この要件を満たせません。(記事執筆時点、Linux Mint 21.3 で v12.22.9)
そこで、apt ではない方法で Node を準備します。Node Version Manager (nvm) を使う方法が良さげです。インストールは下記コマンド。
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
Bash でなく Fish 使いの方はこちらが使えます。インストールは下記コマンド。
fisher install jorgebucaran/nvm.fish
これで nvm
コマンドが使えるようになり、Node.js のインストール/アンインストール、アクティベートなどができるようになります。
v20 の Node が LTS としてリリースされているようなので、ここでは v20 を使う方向でいきます。(本記事執筆時点で最新の LTS)
nvm install 20
そして、下記コマンドで環境がアクティベートされ、node
や npm
などのコマンドが使えるようになります。
nvm use 20
一度端末を閉じるとアクティベートが解除され、node
や npm
などのコマンドが使えなく (見つからなく) なります。必要になったら、再び上の use
コマンドを実行します。
Vitest の準備
この辺は、Vitest 公式ページにある通り進めます。カレントに Vitest パッケージをインストールします。
npm install -D vitest
オプション -D
の意味は、調べれば出てくるので省略。
一言で言うと、開発用途にだけ必要なパッケージ (例えばテストフレームワークとか) には -D
を付けるべきです。
ls
# node_modules package-lock.json package.json
この node_modules
が存在するディレクトリ、およびこれより下位のディレクトリにカレントがある状態で、先ほどインストールしたパッケージが使用できます。試しに Vitest のバージョンを表示させてみます。
npx vitest -v
# vitest/1.6.0 linux-x64 node-v20.13.0
ちょっと npm の設定
ここで package.json
を少し編集します。
{
+ "scripts": {
+ "test": "vitest run"
+ },
+ "type": "module",
"devDependencies": {
"vitest": "^1.6.0"
}
}
バージョン ^1.6.0
は、そのときのリリースバージョンによって変わります。
"scripts"
これにより、npm test
とコマンドを実行すると、Vitest が実行されるようになります。エイリアス登録的な感じです。
これを登録しておくことで、もしテストに使うオプションが変わったとしても、GitHub Actions などのスクリプトを変えることなく、package.json
だけ変えればよいことになります。以降はテストをする際に npm test
と実行します。
"scripts"
に登録されているコマンドは、npm run <name>
というコマンドで呼び出せます。
"test"
は npm run test
でも実行できますが、よく使われるからちょっと特別?で、npm test
だけで OK。
"type"
モジュールの読み込み方式の指定です。試しにこれを未設定の状態でテストをしてみると、以下のような Warning が出てきます。
The CJS build of Vite's Node API is deprecated. See https://vitejs.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated for more details.
「CJS (CommonJS) を廃止するから、ESM (ECMAScript Modules) にしてね」との案内です。先の "type": "module"
を指定してやることで ESM になり、これで Warning が消えます。
JavaScript のモジュールの仕組みは、CommonJS と ECMAScript Modules の 2つの仕様があります。仕様が 2つ存在する経緯とかを記すとこの記事の範囲を超えてしまうので、詳細は別途お調べください・・・。
以降 vitest の準備は
ところで、Vitest をインストールしたとき、package.json
と package-lock.json
が生成されました。この 2ファイルには、インストールしたパッケージの情報が記されています。
node_modules
にはパッケージの実体が格納されており、先ほどのパッケージ情報ファイルを基に生成されます。
そのため、node_modules
は Git などのソース管理で追跡せず、代わりに package.json
と package-lock.json
をソース管理することで、同じパッケージ構成を他環境で作れるようにするのが一般的です。
package-lock.json
を基にパッケージを再度インストールするには下記コマンド。
npm ci
package-lock.json
には、依存パッケージ含め、インストールしたすべてのパッケージ情報が記されています。つまり、npm ci
により、パッケージ構成は (バージョンなど含めて) 完全に再現されることになります。
npm install
と npm ci
は一見すると同じようなコマンドに見えます。ですが、GitHub Actions などの CI 環境においては、npm ci
が推奨されています。
詳しくは、こちらの記事の解説をどうぞ。(丸投げ)
Vitest の実行
Vitest の準備ができたところで、テストを記述します。公式チュートリアルの通りファイルを準備するとこんな感じ。
ls
# node_modules package-lock.json package.json sum.js sum.test.js
export function sum(a, b) {
return a + b
}
import { expect, test } from 'vitest'
import { sum } from './sum'
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3)
})
テスト実行
npm test
正しくできていれば、Vitest が passed で結果を表示してくれます。
これで、まずはローカル環境上で Vitest を動かすことができるようになりました。
GitHub Actions で動かす
これまでの手順も踏まえ、テストを GitHub Actions 上で動かします。
さて、先の手順では nvm をやりくりしていましたが、GitHub Actions ではその必要はありません。Node.js を準備してくれる action を呼ぶだけで OK です。また、バージョン指定も可能です。
- name: Set up Node 20
uses: actions/setup-node@v4
with:
node-version: 20
あとは、npm ci
とかで Vitest の準備をし、実行するのみ。
完成品がこちら。
name: Run all JS unittests by vitest
on:
push
jobs:
js-vitest:
runs-on: ubuntu-latest
steps:
- name: Source checkout
uses: actions/checkout@v4
- name: Set up Node 20
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install node dependencies
run: npm ci
- name: Run all tests
run: npm test
ちなみに、Vitest のテストファイルがある場所、および Vitest をインストールする場所がリポジトリのルート直下でない場合は、defailts:
を以下のように追加することでカレントをセットできます。
(略)
jobs:
js-vitest:
runs-on: ubuntu-latest
defaults:
run:
working-directory: path/to/vitest-files/dir
(略)
ちなみに - Vitest の採用理由
- Vitest が比較的最近できたものらしい (記事執筆時点)
- Jest と API の互換性がある
- Jest は数ある JavaScript テストフレームワークの中でもだいぶ人気らしい
- もし Vitest が微妙だったら Jest にしよう、という算段
- 場合によっては Jest より早いらしい? (要出典)
機能面で特別な理由はありません。フレームワーク固有の機能を使えば、もっといい感じなテスト (語彙力ェ) ができるかもしれませんが、慣れてないうちは、複雑な機能は避けることにします。
おまけ - テストコード before - after
以前やっていたテストコードの書き方と、Vitest を使用したテストコードを比べてみます。例外 throw のテストをしたかったので、sum()
のサンプルでなく div()
(割り算) のサンプルを作ってみます。
export function div(a, b) {
if(b === 0) {
throw new Error("Zero division");
}
return a / b;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script type="module">
import { div } from '/static/div.js'
function test_div() {
console.log("-- test_div");
const expected = 2 / 3;
const actual = div(2, 3);
if(expected === actual) {
console.log("--- OK");
} else {
console.log("Expected: " + expected);
console.log("Actual: " + actual);
console.log("--- NG");
}
}
function err_zero_div() {
console.log("-- err_zero_div");
try {
div(1, 0);
console.log("--- NG - No error thrown");
} catch(e) {
if(e.toString() === "Error: Zero division") {
console.log("--- OK");
} else {
console.log(e);
console.log("--- NG - Un expected error thrown");
}
}
}
document.getElementById("test-run-button").addEventListener("click", () => {
test_div();
err_zero_div();
});
</script>
</head>
<body>
<button id="test-run-button">Run test</button>
</body>
</html>
ESM の import
export
を使ったスクリプトの場合、HTML をローカルファイルとして開くと動いてくれません。HTTP 通信経由で開く必要があります。
FastAPI の例
え、HTTP サーバも Node.js でやらんのかって?FastAPI しかまだ知らんのです・・・。
ファイル構成:
./
|- main.py
|- pages
| |- before.html
|
|- static
|- div.js
# coding: UTF-8
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from uvicorn import run
templates = Jinja2Templates("./pages")
app = FastAPI()
app.mount("/static", StaticFiles(directory="./static"))
@app.get("/")
def index(request: Request):
return templates.TemplateResponse("before.html", {"request": request})
if __name__ == "__main__":
run("main:app", reload=True)
import { div } from 'div.js'
import { test, expect } from 'vitest'
test("Division", () => {
expect(div(2, 3)).toBe(2 / 3);
});
test("Zero division", () => {
expect(() => div(1, 0)).toThrowError(new Error("Zero division"));
});
この通り、コード量が格段に減りました (そもそも HTML を書く必要が無くなった)。テスト結果を出力するコードを自力で書く必要が無くなるので、だいぶ楽です。
(before.html
あえて共通部分のモジュール化せず遠回しに書いてる?と思われるかもしれません。でもこの非スマートな書き方し続けてたのはマジです。)
テスト書くのを面倒臭がって、ノーテストのコードが量産される前にフレームワーク導入してよかったです。いや、マジで。