Color mode

テスト

Riebeckite では、変更によって既存の動作が壊れていないことを自動テストで確認します。

基本のテスト環境は、

  • Node.js 組み込みの node:test
  • TypeScript を直接実行する tsx

です。

Jest や Vitest などの追加のテストフレームワークは使用しません。

また、HTML や JSON のように手作業では確認しづらい大きな出力には Golden File を使用します。期待する出力そのものをファイルとして保存することで、変更内容を Pull Request の差分として確認できます。

テストの種類

Riebeckite には大きく2種類のテストがあります。

Diagram source
text
flowchart TD
    Test["Riebeckite Tests"]
 
    Test --> Unit["Package Tests<br/>Unit / Integration"]
    Test --> E2E["External Site E2E"]
 
    Unit --> Package["各 package の test/*.test.ts"]
    E2E --> External["tests/external-site"]

このページで主に扱うのは、各 Package に置くテストです。

実際の外部 Site を生成して検証する End-to-End Test は、

text
tests/external-site

にあります。

sh
pnpm test:e2e:external

で実行できます。

External Site E2E を動かす再利用可能な Engine は、

text
@riebeckite/test/e2e

にあります。

一方、

  • Riebeckite repository 固有の Fixture
  • テスト対象 Package の一覧
  • Repository 固有の Assertion

は tests/external-site に置きます。

第三者 Plugin を外部 Package の立場で検証する Fixture は tests/plugin-dx にあります(独立した pnpm workspace)。

sh
pnpm test:plugin-dx            # Public API だけで書いた Plugin の Unit Test
pnpm test:plugin-dx:external   # packed tarball を隔離 Site へ install して検証

test:plugin-dx:external は @riebeckite/core と fixture Plugin を pnpm pack し、monorepo の外の隔離 Site に install して、公開 Package だけで Plugin が動作することを確認します。外部配布する Plugin の回帰検知に利用できます。

テストを実行する

よく使うコマンドは次のとおりです。

コマンド 内容
pnpm test test Script を持つすべての Package をテスト
pnpm --filter <package> test 特定の Package だけテスト
pnpm test:update すべての Golden File を現在の出力で更新
pnpm test:e2e:external External Site E2E を実行
pnpm test:plugin-dx 外部 Package 視点の Plugin Fixture をテスト
pnpm test:plugin-dx:external packed tarball を隔離 Site へ install して Plugin を検証
pnpm test:registry Scaffold の install Contract を npm 公開 artifact に対して実行

Scaffold の install Contract は、既定では workspace を pnpm pack した artifact を install して検証します。そのため、新しい Package を追加した branch でも publish 前に検証できます。pnpm test:registry は同じ Contract を npm からの install に切り替え、公開済み artifact が解決できることを確認します。Release 後に実行してください。

通常は、変更した範囲に近いテストから実行してください。

たとえば TOC Plugin だけを変更した場合は、

sh
pnpm --filter @riebeckite/plugin-toc test

とします。

その後、必要に応じて Repository 全体の、

sh
pnpm test

を実行します。

Golden File を部分的に更新する

1つの Package の Golden File だけを更新したい場合は、UPDATE_GOLDEN=1 を設定してテストします。

PowerShell では、

powershell
$env:UPDATE_GOLDEN=1; pnpm --filter @riebeckite/plugin-toc test

です。

Golden File の更新は単にテストを通すために行うものではありません。

新しい出力が正しいことを確認してから更新してください。

テストの置き場所

各 Package のテストは、

text
test/*.test.ts

に置きます。

たとえば、

text
packages/plugins/example/
├─ src/
├─ test/
│  ├─ example.test.ts
│  └─ __golden__/
│     └─ example.html
├─ package.json
└─ index.ts

のような構成です。

Package 自身が test Script を持ちます。

json
{
  "scripts": {
    "test": "node --import tsx --test \"test/*.test.ts\""
  }
}

test/ とテストファイルは、

  • Type Check
  • Package Build
  • 公開 Package の files

から除外されます。

そのため、Riebeckite の利用者へテストコードが配布されたり、利用側の Build に影響したりしません。

共有テストユーティリティ

複数 Package から利用するテスト用機能は、

text
@riebeckite/test

にあります。

必要な Package の devDependencies に追加し、

ts
import {
  assertGolden,
} from "@riebeckite/test";

のように Public API から利用します。

テスト専用の共通処理を各 Plugin にコピーしないでください。

Plugin のテスト

Plugin のテストは、確認したい範囲に合わせて次の4段階に分けられます。すべてを行う必要はありません。小さい範囲から始めてください。

text
Level 1  Pure logic            通常の test runner でよい
Level 2  Markdown / HTML       Pipeline に Plugin を渡して変換結果を検証
Level 3  Content / lifecycle   ContentManager + In-memory ContentSource
Level 4  外部パッケージ境界     packed tarball を隔離 Site へ install

Level 1: Pure Logic

Option の解決、文字列変換、AST ヘルパーなど、Riebeckite に依存しない処理は node:test などの通常の test runner でテストします。この段階では @riebeckite/test も ContentManager も不要です。

Level 2: Markdown / HTML Transformation

Markdown / HTML の変換は、公開されている Pipeline に Plugin を渡して検証できます。

ts
import { Pipeline } from "@riebeckite/core";
import { tipPlugin } from "../src/index.ts";
 
const pipeline = new Pipeline(new Map(), new Map(), undefined, {
  plugins: [tipPlugin()],
});
 
const { html } = await pipeline.execute(":::tip\nSave often.\n:::");
assert.match(html, /<aside class="rr-tip">/);

Pipeline の第1引数は content index、第2引数は permalink の Map です。単一 Document の変換なら空で構いません。第3引数は embed など他 Content を参照する場合だけ必要です。

ContentManager を使わずに変換だけを確認できるため、Plugin のテストで最もよく使う段階です。

Level 3: Content / lifecycle

Content の読み込み、hook、manifest、body slot、Page Type を検証する場合は ContentManager を使います。後述の「Content を扱うテスト」と同じ In-memory ContentSource の形で、getProcessedContent() は Pipeline と Content hook を通した結果を、getManifest() は解決済み Plugin を含む manifest を返します。

Level 4: 外部パッケージ境界

配布する Package は、pnpm pack した tarball を monorepo の外の隔離 Site へ install して検証します。これによって、公開 Export の不足、@riebeckite/core/src/** への誤った依存、dependencies の宣言漏れ、型定義の欠落を検出できます。

Repository の tests/plugin-dx がこの検証の実例で、前述のとおり pnpm test:plugin-dx と pnpm test:plugin-dx:external で実行します。

@riebeckite/test の役割

@riebeckite/test は、Golden File の比較(assertGolden / assertGoldenJson)など、複数 Package で共有するテスト Helper を提供します。必須ではありません。Level 1〜3 のほとんどは node:test と公開 Core API(Pipeline / ContentManager)だけで書けます。@riebeckite/test/e2e は外部 Site を生成して検証する Repository 向けの Engine で、第三者 Plugin が通常使うものではありません。

基本的なテストの書き方

基本は、対象 Package の処理へ入力を渡して、その結果を検証します。

ts
import assert from "node:assert/strict";
import { test } from "node:test";
 
import { renderBreadcrumbNav } from "../src/render.ts";
 
test("renders nothing for a single root item", () => {
  assert.equal(
    renderBreadcrumbNav([]),
    "",
  );
});

Riebeckite の Plugin Logic には純粋関数として切り出せる処理が多いため、すべてのテストで Build Pipeline 全体を起動する必要はありません。

Diagram source
text
flowchart LR
    Input["Input"]
    Function["対象の処理"]
    Output["Output"]
    Assert["Assertion"]
 
    Input --> Function
    Function --> Output
    Output --> Assert

小さい処理は、可能な限りこの形で直接テストします。

Content を扱うテスト

Content System の動作を確認したい場合は、実際の Filesystem を用意するより、In-memory の ContentSource を利用するのが基本です。

ts
const source = {
  async scan() {
    return [
      {
        path: "notes/index.md",
      },
    ];
  },
 
  async read(entry) {
    return `# ${entry.path}`;
  },
};
 
const config = resolveConfig({
  content: {
    directory: ".",
  },
});
 
const manager = new ContentManager(
  source,
  [],
  { config },
);
 
const manifest =
  await manager.getManifest();

これによって、

Diagram source
text
flowchart LR
    Source["In-memory<br/>ContentSource"]
    Manager["ContentManager"]
    Manifest["Manifest"]
    Assert["Assertion"]
 
    Source --> Manager
    Manager --> Manifest
    Manifest --> Assert

という小さい範囲で Content System を検証できます。

実際の Site や Vite Build が必要な問題でなければ、テストのためだけに Application 全体を組み立てないでください。

Golden File

HTML や大きな JSON のように、個別の Assertion を大量に書くより出力全体を確認した方が分かりやすい場合は Golden File を使用します。

@riebeckite/test には2つの Helper があります。

Helper 用途
assertGolden(actual, goldenUrl) HTML、Markdown、Text、Serialized JSON
assertGoldenJson(value, goldenUrl) Object / JSON

assertGoldenJson() は比較前に、

ts
JSON.stringify(value, null, 2)

で整形します。

Golden File の配置

期待値は、

text
test/__golden__/

に置きます。

たとえば、

text
test/
├─ toc.test.ts
└─ __golden__/
   └─ toc.html

です。

テストからは import.meta.url を基準に URL を渡します。

ts
import { test } from "node:test";
import {
  assertGolden,
} from "@riebeckite/test";
 
test("renders the table of contents", () => {
  assertGolden(
    renderToc(entries),
    new URL(
      "./__golden__/toc.html",
      import.meta.url,
    ),
  );
});

絶対 Path や Current Working Directory に依存させないことが重要です。

Golden File の比較

既定では出力を少し正規化してから比較します。

具体的には、

text
CRLF / CR
    ↓
LF
 
末尾の複数改行
    ↓
1つの改行

へ統一します。

これによって Windows / Linux や Editor の設定による不要な差分を防ぎます。

バイト単位で厳密な比較が必要な場合は、

ts
assertGolden(
  actual,
  goldenUrl,
  {
    normalize: false,
  },
);

とします。

Golden File が一致しない場合

Golden File が、

  • 存在しない
  • 現在の出力と一致しない

場合、テストは失敗します。

出力の変更が意図したものであることを確認したら、

sh
pnpm test:update

で Golden File を更新します。

Diagram source
text
flowchart TD
    Test["Test"]
    Match{"Goldenと一致?"}
 
    Test --> Match
 
    Match -->|Yes| Pass["Pass"]
    Match -->|No| Review["差分を確認"]
 
    Review --> Correct{"新しい出力が正しい?"}
 
    Correct -->|No| Fix["実装を修正"]
    Correct -->|Yes| Update["Goldenを更新"]
 
    Fix --> Test
    Update --> Test

Golden File をコミットするのは意図的です。

出力の変更を、

diff
- 以前の出力
+ 新しい出力

として Pull Request 上でレビューできるようにするためです。

Golden File の更新そのものを修正として扱わず、なぜ出力が変わったのかを必ず確認してください。

Node の Snapshot を使わない理由

Riebeckite では Node.js 組み込みの Snapshot Assertion、

text
--test-update-snapshots

は使用していません。

これは Node.js 22.3 以降を必要とします。

Riebeckite は Node.js 20.19 以降も対象としているため、独自の Golden File Helper を使用します。

Package にテストを追加する

まだテストを持っていない Package に追加する場合は、次の順番で設定します。

1. tsx を追加する

Package の devDependencies に tsx を追加します。

2. test Script を追加する

json
{
  "scripts": {
    "test": "node --import tsx --test \"test/*.test.ts\""
  }
}

3. 必要なら @riebeckite/test を追加する

Golden Helper などを使う場合は、

text
@riebeckite/test

を devDependencies に追加します。

4. Plugin Metadata を更新する

Plugin Package の場合は、

text
scripts/package_metadata.mjs

の hasTests に Package Directory 名を追加します。

5. Package Metadata を検証する

Dependency を変更したら、

sh
pnpm install
pnpm check:packages

を実行します。

scripts/check_packages.mjs は Package の Metadata と scripts/package_metadata.mjs の定義を比較します。

たとえば、

  • test Script が必要なのに存在しない
  • hasTests に追加されていない
  • 期待する Metadata と一致しない

といった問題を検出します。

何をテストするか

テストでは、決定的で再現可能な振る舞いを優先します。

特に次のような処理はテストに向いています。

  • Option の解決
  • Validation
  • Default Value
  • Permalink の解決
  • TOC の構築
  • Metadata の抽出
  • HTML Rendering
  • JSON Generation
  • 空の入力
  • Frontmatter の欠落
  • Slug の重複
  • 不正な Attribute

たとえば純粋関数なら、

text
同じ入力
   ↓
同じ処理
   ↓
常に同じ出力

になるため、安定したテストを書きやすくなります。

避けるべきテスト

次のような外部状態へ直接依存するテストは、可能な限り避けます。

text
Network
Current Time
Random Value
Machine-specific Absolute Path
Current Working Directory

たとえば、

ts
const now = new Date();

を処理の内部で直接取得すると、テストする時刻によって結果が変化します。

代わりに必要な値を注入できるようにします。

ts
function createEntry(now: Date) {
  // ...
}

テストでは固定値を渡せます。

ts
const now =
  new Date("2026-01-01T00:00:00Z");
 
const entry =
  createEntry(now);

乱数なども同様です。

テスト側で実際の時刻や乱数を予測するのではなく、決定に必要な値を外から渡せる設計が重要です。

どの範囲までテストするか

変更した処理に最も近い、小さい範囲からテストします。

Diagram source
text
flowchart TD
    Change["変更"]
 
    Change --> Pure["純粋関数で確認できる?"]
    Pure -->|Yes| Unit["Unit Test"]
    Pure -->|No| Content["ContentManagerが必要?"]
 
    Content -->|Yes| Manager["In-memory ContentSource"]
    Content -->|No| Package["Package Integration Test"]
 
    Package --> Site{"実Siteが必要?"}
    Site -->|Yes| E2E["External Site E2E"]

単純な Renderer の修正を確認するために External Site E2E を使う必要はありません。

逆に、

  • Package の配布形式
  • External Consumer からの Import
  • 実際の Site Build
  • Integration をまたぐ問題

などは Unit Test だけでは十分ではないため、External Site E2E で確認します。

基本方針

Riebeckite のテストでは、

変更した振る舞いを確認できる最小の範囲で、決定的なテストを書く

ことを基本にします。

Diagram source
text
flowchart LR
    Logic["Pure Logic"]
    Unit["Unit Test"]
    Content["Content Behavior"]
    Manager["ContentManager Test"]
    Package["Package Integration"]
    E2E["External Site E2E"]
 
    Logic --> Unit
    Content --> Manager
    Package --> E2E

大きな Build を毎回再現するのではなく、純粋関数や In-memory ContentSource で確認できる処理は小さくテストします。

一方、Package 境界や実際の External Consumer に関係する問題は E2E で確認します。

Golden File は、複雑な出力を「テストを通すための Snapshot」ではなく、レビュー可能な期待出力として扱ってください。

History

1 changesCollapseExpand
1 + # テスト
2 +
3 + Riebeckite では、変更によって既存の動作が壊れていないことを自動テストで確認します。
4 +
5 + 基本のテスト環境は、
6 +
7 + - Node.js 組み込みの `node:test`
8 + - TypeScript を直接実行する `tsx`
9 +
10 + です。
11 +
12 + Jest や Vitest などの追加のテストフレームワークは使用しません。
13 +
14 + また、HTML や JSON のように手作業では確認しづらい大きな出力には **Golden File** を使用します。期待する出力そのものをファイルとして保存することで、変更内容を Pull Request の差分として確認できます。
15 +
16 + # テストの種類
17 +
18 + Riebeckite には大きく2種類のテストがあります。
19 +
20 + ```mermaid
21 + flowchart TD
22 + Test["Riebeckite Tests"]
23 +
24 + Test --> Unit["Package Tests<br/>Unit / Integration"]
25 + Test --> E2E["External Site E2E"]
26 +
27 + Unit --> Package["各 package の test/*.test.ts"]
28 + E2E --> External["tests/external-site"]
29 + ```
30 +
31 + このページで主に扱うのは、**各 Package に置くテスト**です。
32 +
33 + 実際の外部 Site を生成して検証する End-to-End Test は、
34 +
35 + ```text
36 + tests/external-site
37 + ```
38 +
39 + にあります。
40 +
41 + ```sh
42 + pnpm test:e2e:external
43 + ```
44 +
45 + で実行できます。
46 +
47 + External Site E2E を動かす再利用可能な Engine は、
48 +
49 + ```text
50 + @riebeckite/test/e2e
51 + ```
52 +
53 + にあります。
54 +
55 + 一方、
56 +
57 + - Riebeckite repository 固有の Fixture
58 + - テスト対象 Package の一覧
59 + - Repository 固有の Assertion
60 +
61 + は `tests/external-site` に置きます。
62 +
63 + 第三者 Plugin を外部 Package の立場で検証する Fixture は `tests/plugin-dx` にあります(独立した pnpm workspace)。
64 +
65 + ```sh
66 + pnpm test:plugin-dx # Public API だけで書いた Plugin の Unit Test
67 + pnpm test:plugin-dx:external # packed tarball を隔離 Site へ install して検証
68 + ```
69 +
70 + `test:plugin-dx:external` は `@riebeckite/core` と fixture Plugin を `pnpm pack` し、monorepo の外の隔離 Site に install して、公開 Package だけで Plugin が動作することを確認します。外部配布する Plugin の回帰検知に利用できます。
71 +
72 + # テストを実行する
73 +
74 + よく使うコマンドは次のとおりです。
75 +
76 + | コマンド | 内容 |
77 + | --- | --- |
78 + | `pnpm test` | `test` Script を持つすべての Package をテスト |
79 + | `pnpm --filter <package> test` | 特定の Package だけテスト |
80 + | `pnpm test:update` | すべての Golden File を現在の出力で更新 |
81 + | `pnpm test:e2e:external` | External Site E2E を実行 |
82 + | `pnpm test:plugin-dx` | 外部 Package 視点の Plugin Fixture をテスト |
83 + | `pnpm test:plugin-dx:external` | packed tarball を隔離 Site へ install して Plugin を検証 |
84 + | `pnpm test:registry` | Scaffold の install Contract を npm 公開 artifact に対して実行 |
85 +
86 + Scaffold の install Contract は、既定では **workspace を `pnpm pack` した artifact** を install して検証します。そのため、新しい Package を追加した branch でも publish 前に検証できます。`pnpm test:registry` は同じ Contract を npm からの install に切り替え、公開済み artifact が解決できることを確認します。Release 後に実行してください。
87 +
88 + 通常は、変更した範囲に近いテストから実行してください。
89 +
90 + たとえば TOC Plugin だけを変更した場合は、
91 +
92 + ```sh
93 + pnpm --filter @riebeckite/plugin-toc test
94 + ```
95 +
96 + とします。
97 +
98 + その後、必要に応じて Repository 全体の、
99 +
100 + ```sh
101 + pnpm test
102 + ```
103 +
104 + を実行します。
105 +
106 + # Golden File を部分的に更新する
107 +
108 + 1つの Package の Golden File だけを更新したい場合は、`UPDATE_GOLDEN=1` を設定してテストします。
109 +
110 + PowerShell では、
111 +
112 + ```powershell
113 + $env:UPDATE_GOLDEN=1; pnpm --filter @riebeckite/plugin-toc test
114 + ```
115 +
116 + です。
117 +
118 + Golden File の更新は単にテストを通すために行うものではありません。
119 +
120 + **新しい出力が正しいことを確認してから更新してください。**
121 +
122 + # テストの置き場所
123 +
124 + 各 Package のテストは、
125 +
126 + ```text
127 + test/*.test.ts
128 + ```
129 +
130 + に置きます。
131 +
132 + たとえば、
133 +
134 + ```text
135 + packages/plugins/example/
136 + ├─ src/
137 + ├─ test/
138 + │ ├─ example.test.ts
139 + │ └─ __golden__/
140 + │ └─ example.html
141 + ├─ package.json
142 + └─ index.ts
143 + ```
144 +
145 + のような構成です。
146 +
147 + Package 自身が `test` Script を持ちます。
148 +
149 + ```json
150 + {
151 + "scripts": {
152 + "test": "node --import tsx --test \"test/*.test.ts\""
153 + }
154 + }
155 + ```
156 +
157 + `test/` とテストファイルは、
158 +
159 + - Type Check
160 + - Package Build
161 + - 公開 Package の `files`
162 +
163 + から除外されます。
164 +
165 + そのため、Riebeckite の利用者へテストコードが配布されたり、利用側の Build に影響したりしません。
166 +
167 + # 共有テストユーティリティ
168 +
169 + 複数 Package から利用するテスト用機能は、
170 +
171 + ```text
172 + @riebeckite/test
173 + ```
174 +
175 + にあります。
176 +
177 + 必要な Package の `devDependencies` に追加し、
178 +
179 + ```ts
180 + import {
181 + assertGolden,
182 + } from "@riebeckite/test";
183 + ```
184 +
185 + のように Public API から利用します。
186 +
187 + テスト専用の共通処理を各 Plugin にコピーしないでください。
188 +
189 + # Plugin のテスト
190 +
191 + Plugin のテストは、確認したい範囲に合わせて次の4段階に分けられます。すべてを行う必要はありません。小さい範囲から始めてください。
192 +
193 + ```text
194 + Level 1 Pure logic 通常の test runner でよい
195 + Level 2 Markdown / HTML Pipeline に Plugin を渡して変換結果を検証
196 + Level 3 Content / lifecycle ContentManager + In-memory ContentSource
197 + Level 4 外部パッケージ境界 packed tarball を隔離 Site へ install
198 + ```
199 +
200 + ## Level 1: Pure Logic
201 +
202 + Option の解決、文字列変換、AST ヘルパーなど、Riebeckite に依存しない処理は `node:test` などの通常の test runner でテストします。この段階では `@riebeckite/test` も `ContentManager` も不要です。
203 +
204 + ## Level 2: Markdown / HTML Transformation
205 +
206 + Markdown / HTML の変換は、公開されている `Pipeline` に Plugin を渡して検証できます。
207 +
208 + ```ts
209 + import { Pipeline } from "@riebeckite/core";
210 + import { tipPlugin } from "../src/index.ts";
211 +
212 + const pipeline = new Pipeline(new Map(), new Map(), undefined, {
213 + plugins: [tipPlugin()],
214 + });
215 +
216 + const { html } = await pipeline.execute(":::tip\nSave often.\n:::");
217 + assert.match(html, /<aside class="rr-tip">/);
218 + ```
219 +
220 + `Pipeline` の第1引数は content index、第2引数は permalink の `Map` です。単一 Document の変換なら空で構いません。第3引数は embed など他 Content を参照する場合だけ必要です。
221 +
222 + `ContentManager` を使わずに変換だけを確認できるため、Plugin のテストで最もよく使う段階です。
223 +
224 + ## Level 3: Content / lifecycle
225 +
226 + Content の読み込み、hook、manifest、body slot、Page Type を検証する場合は `ContentManager` を使います。後述の「Content を扱うテスト」と同じ In-memory `ContentSource` の形で、`getProcessedContent()` は Pipeline と Content hook を通した結果を、`getManifest()` は解決済み Plugin を含む manifest を返します。
227 +
228 + ## Level 4: 外部パッケージ境界
229 +
230 + 配布する Package は、`pnpm pack` した tarball を monorepo の外の隔離 Site へ install して検証します。これによって、公開 Export の不足、`@riebeckite/core/src/**` への誤った依存、`dependencies` の宣言漏れ、型定義の欠落を検出できます。
231 +
232 + Repository の `tests/plugin-dx` がこの検証の実例で、前述のとおり `pnpm test:plugin-dx` と `pnpm test:plugin-dx:external` で実行します。
233 +
234 + ## `@riebeckite/test` の役割
235 +
236 + `@riebeckite/test` は、Golden File の比較(`assertGolden` / `assertGoldenJson`)など、複数 Package で共有するテスト Helper を提供します。**必須ではありません**。Level 1〜3 のほとんどは `node:test` と公開 Core API(`Pipeline` / `ContentManager`)だけで書けます。`@riebeckite/test/e2e` は外部 Site を生成して検証する Repository 向けの Engine で、第三者 Plugin が通常使うものではありません。
237 +
238 + # 基本的なテストの書き方
239 +
240 + 基本は、対象 Package の処理へ入力を渡して、その結果を検証します。
241 +
242 + ```ts
243 + import assert from "node:assert/strict";
244 + import { test } from "node:test";
245 +
246 + import { renderBreadcrumbNav } from "../src/render.ts";
247 +
248 + test("renders nothing for a single root item", () => {
249 + assert.equal(
250 + renderBreadcrumbNav([]),
251 + "",
252 + );
253 + });
254 + ```
255 +
256 + Riebeckite の Plugin Logic には純粋関数として切り出せる処理が多いため、すべてのテストで Build Pipeline 全体を起動する必要はありません。
257 +
258 + ```mermaid
259 + flowchart LR
260 + Input["Input"]
261 + Function["対象の処理"]
262 + Output["Output"]
263 + Assert["Assertion"]
264 +
265 + Input --> Function
266 + Function --> Output
267 + Output --> Assert
268 + ```
269 +
270 + 小さい処理は、可能な限りこの形で直接テストします。
271 +
272 + # Content を扱うテスト
273 +
274 + Content System の動作を確認したい場合は、実際の Filesystem を用意するより、In-memory の `ContentSource` を利用するのが基本です。
275 +
276 + ```ts
277 + const source = {
278 + async scan() {
279 + return [
280 + {
281 + path: "notes/index.md",
282 + },
283 + ];
284 + },
285 +
286 + async read(entry) {
287 + return `# ${entry.path}`;
288 + },
289 + };
290 +
291 + const config = resolveConfig({
292 + content: {
293 + directory: ".",
294 + },
295 + });
296 +
297 + const manager = new ContentManager(
298 + source,
299 + [],
300 + { config },
301 + );
302 +
303 + const manifest =
304 + await manager.getManifest();
305 + ```
306 +
307 + これによって、
308 +
309 + ```mermaid
310 + flowchart LR
311 + Source["In-memory<br/>ContentSource"]
312 + Manager["ContentManager"]
313 + Manifest["Manifest"]
314 + Assert["Assertion"]
315 +
316 + Source --> Manager
317 + Manager --> Manifest
318 + Manifest --> Assert
319 + ```
320 +
321 + という小さい範囲で Content System を検証できます。
322 +
323 + 実際の Site や Vite Build が必要な問題でなければ、テストのためだけに Application 全体を組み立てないでください。
324 +
325 + # Golden File
326 +
327 + HTML や大きな JSON のように、個別の Assertion を大量に書くより出力全体を確認した方が分かりやすい場合は Golden File を使用します。
328 +
329 + `@riebeckite/test` には2つの Helper があります。
330 +
331 + | Helper | 用途 |
332 + | --- | --- |
333 + | `assertGolden(actual, goldenUrl)` | HTML、Markdown、Text、Serialized JSON |
334 + | `assertGoldenJson(value, goldenUrl)` | Object / JSON |
335 +
336 + `assertGoldenJson()` は比較前に、
337 +
338 + ```ts
339 + JSON.stringify(value, null, 2)
340 + ```
341 +
342 + で整形します。
343 +
344 + # Golden File の配置
345 +
346 + 期待値は、
347 +
348 + ```text
349 + test/__golden__/
350 + ```
351 +
352 + に置きます。
353 +
354 + たとえば、
355 +
356 + ```text
357 + test/
358 + ├─ toc.test.ts
359 + └─ __golden__/
360 + └─ toc.html
361 + ```
362 +
363 + です。
364 +
365 + テストからは `import.meta.url` を基準に URL を渡します。
366 +
367 + ```ts
368 + import { test } from "node:test";
369 + import {
370 + assertGolden,
371 + } from "@riebeckite/test";
372 +
373 + test("renders the table of contents", () => {
374 + assertGolden(
375 + renderToc(entries),
376 + new URL(
377 + "./__golden__/toc.html",
378 + import.meta.url,
379 + ),
380 + );
381 + });
382 + ```
383 +
384 + 絶対 Path や Current Working Directory に依存させないことが重要です。
385 +
386 + # Golden File の比較
387 +
388 + 既定では出力を少し正規化してから比較します。
389 +
390 + 具体的には、
391 +
392 + ```text
393 + CRLF / CR
394 + ↓
395 + LF
396 +
397 + 末尾の複数改行
398 + ↓
399 + 1つの改行
400 + ```
401 +
402 + へ統一します。
403 +
404 + これによって Windows / Linux や Editor の設定による不要な差分を防ぎます。
405 +
406 + バイト単位で厳密な比較が必要な場合は、
407 +
408 + ```ts
409 + assertGolden(
410 + actual,
411 + goldenUrl,
412 + {
413 + normalize: false,
414 + },
415 + );
416 + ```
417 +
418 + とします。
419 +
420 + # Golden File が一致しない場合
421 +
422 + Golden File が、
423 +
424 + - 存在しない
425 + - 現在の出力と一致しない
426 +
427 + 場合、テストは失敗します。
428 +
429 + 出力の変更が意図したものであることを確認したら、
430 +
431 + ```sh
432 + pnpm test:update
433 + ```
434 +
435 + で Golden File を更新します。
436 +
437 + ```mermaid
438 + flowchart TD
439 + Test["Test"]
440 + Match{"Goldenと一致?"}
441 +
442 + Test --> Match
443 +
444 + Match -->|Yes| Pass["Pass"]
445 + Match -->|No| Review["差分を確認"]
446 +
447 + Review --> Correct{"新しい出力が正しい?"}
448 +
449 + Correct -->|No| Fix["実装を修正"]
450 + Correct -->|Yes| Update["Goldenを更新"]
451 +
452 + Fix --> Test
453 + Update --> Test
454 + ```
455 +
456 + Golden File をコミットするのは意図的です。
457 +
458 + 出力の変更を、
459 +
460 + ```diff
461 + - 以前の出力
462 + + 新しい出力
463 + ```
464 +
465 + として Pull Request 上でレビューできるようにするためです。
466 +
467 + **Golden File の更新そのものを修正として扱わず、なぜ出力が変わったのかを必ず確認してください。**
468 +
469 + # Node の Snapshot を使わない理由
470 +
471 + Riebeckite では Node.js 組み込みの Snapshot Assertion、
472 +
473 + ```text
474 + --test-update-snapshots
475 + ```
476 +
477 + は使用していません。
478 +
479 + これは Node.js 22.3 以降を必要とします。
480 +
481 + Riebeckite は Node.js 20.19 以降も対象としているため、独自の Golden File Helper を使用します。
482 +
483 + # Package にテストを追加する
484 +
485 + まだテストを持っていない Package に追加する場合は、次の順番で設定します。
486 +
487 + ## 1. `tsx` を追加する
488 +
489 + Package の `devDependencies` に `tsx` を追加します。
490 +
491 + ## 2. `test` Script を追加する
492 +
493 + ```json
494 + {
495 + "scripts": {
496 + "test": "node --import tsx --test \"test/*.test.ts\""
497 + }
498 + }
499 + ```
500 +
501 + ## 3. 必要なら `@riebeckite/test` を追加する
502 +
503 + Golden Helper などを使う場合は、
504 +
505 + ```text
506 + @riebeckite/test
507 + ```
508 +
509 + を `devDependencies` に追加します。
510 +
511 + ## 4. Plugin Metadata を更新する
512 +
513 + Plugin Package の場合は、
514 +
515 + ```text
516 + scripts/package_metadata.mjs
517 + ```
518 +
519 + の `hasTests` に Package Directory 名を追加します。
520 +
521 + ## 5. Package Metadata を検証する
522 +
523 + Dependency を変更したら、
524 +
525 + ```sh
526 + pnpm install
527 + pnpm check:packages
528 + ```
529 +
530 + を実行します。
531 +
532 + `scripts/check_packages.mjs` は Package の Metadata と `scripts/package_metadata.mjs` の定義を比較します。
533 +
534 + たとえば、
535 +
536 + - `test` Script が必要なのに存在しない
537 + - `hasTests` に追加されていない
538 + - 期待する Metadata と一致しない
539 +
540 + といった問題を検出します。
541 +
542 + # 何をテストするか
543 +
544 + テストでは、**決定的で再現可能な振る舞い**を優先します。
545 +
546 + 特に次のような処理はテストに向いています。
547 +
548 + - Option の解決
549 + - Validation
550 + - Default Value
551 + - Permalink の解決
552 + - TOC の構築
553 + - Metadata の抽出
554 + - HTML Rendering
555 + - JSON Generation
556 + - 空の入力
557 + - Frontmatter の欠落
558 + - Slug の重複
559 + - 不正な Attribute
560 +
561 + たとえば純粋関数なら、
562 +
563 + ```text
564 + 同じ入力
565 + ↓
566 + 同じ処理
567 + ↓
568 + 常に同じ出力
569 + ```
570 +
571 + になるため、安定したテストを書きやすくなります。
572 +
573 + # 避けるべきテスト
574 +
575 + 次のような外部状態へ直接依存するテストは、可能な限り避けます。
576 +
577 + ```text
578 + Network
579 + Current Time
580 + Random Value
581 + Machine-specific Absolute Path
582 + Current Working Directory
583 + ```
584 +
585 + たとえば、
586 +
587 + ```ts
588 + const now = new Date();
589 + ```
590 +
591 + を処理の内部で直接取得すると、テストする時刻によって結果が変化します。
592 +
593 + 代わりに必要な値を注入できるようにします。
594 +
595 + ```ts
596 + function createEntry(now: Date) {
597 + // ...
598 + }
599 + ```
600 +
601 + テストでは固定値を渡せます。
602 +
603 + ```ts
604 + const now =
605 + new Date("2026-01-01T00:00:00Z");
606 +
607 + const entry =
608 + createEntry(now);
609 + ```
610 +
611 + 乱数なども同様です。
612 +
613 + テスト側で実際の時刻や乱数を予測するのではなく、**決定に必要な値を外から渡せる設計**が重要です。
614 +
615 + # どの範囲までテストするか
616 +
617 + 変更した処理に最も近い、小さい範囲からテストします。
618 +
619 + ```mermaid
620 + flowchart TD
621 + Change["変更"]
622 +
623 + Change --> Pure["純粋関数で確認できる?"]
624 + Pure -->|Yes| Unit["Unit Test"]
625 + Pure -->|No| Content["ContentManagerが必要?"]
626 +
627 + Content -->|Yes| Manager["In-memory ContentSource"]
628 + Content -->|No| Package["Package Integration Test"]
629 +
630 + Package --> Site{"実Siteが必要?"}
631 + Site -->|Yes| E2E["External Site E2E"]
632 + ```
633 +
634 + 単純な Renderer の修正を確認するために External Site E2E を使う必要はありません。
635 +
636 + 逆に、
637 +
638 + - Package の配布形式
639 + - External Consumer からの Import
640 + - 実際の Site Build
641 + - Integration をまたぐ問題
642 +
643 + などは Unit Test だけでは十分ではないため、External Site E2E で確認します。
644 +
645 + # 基本方針
646 +
647 + Riebeckite のテストでは、
648 +
649 + **変更した振る舞いを確認できる最小の範囲で、決定的なテストを書く**
650 +
651 + ことを基本にします。
652 +
653 + ```mermaid
654 + flowchart LR
655 + Logic["Pure Logic"]
656 + Unit["Unit Test"]
657 + Content["Content Behavior"]
658 + Manager["ContentManager Test"]
659 + Package["Package Integration"]
660 + E2E["External Site E2E"]
661 +
662 + Logic --> Unit
663 + Content --> Manager
664 + Package --> E2E
665 + ```
666 +
667 + 大きな Build を毎回再現するのではなく、純粋関数や In-memory `ContentSource` で確認できる処理は小さくテストします。
668 +
669 + 一方、Package 境界や実際の External Consumer に関係する問題は E2E で確認します。
670 +
671 + Golden File は、複雑な出力を「テストを通すための Snapshot」ではなく、**レビュー可能な期待出力**として扱ってください。
672 +