はじめに
ローカルではちゃんと通るテストが、GitHub Actionsだとエラーで落ちるという経験をしました。
Error: supabaseUrl is required.
❯ validateSupabaseUrl ...
❯ new SupabaseClient ...
❯ createClient ...
❯ src/lib/supabase.ts:7:25
❯ src/services/studyRecords.ts:1:1
vi.mock でモックしてるのになぜ?と最初は混乱しましたが、原因を理解したらすっきり解決できたので書き残しておきます。
なぜ起きるのか
構造の確認
まず問題になったコードの構造はこんな感じです。
// src/lib/supabase.ts
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseKey = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY;
export const supabase = createClient<Database>(supabaseUrl, supabaseKey);
// src/services/studyRecords.ts
import { supabase } from '../lib/supabase'; // このimportがsupabase.tsを実行する
// テストファイル
vi.mock("../services/studyRecords"); // ファクトリなし
ファクトリとは
vi.mock の第2引数として渡せる関数のことです。
// ファクトリなし(自動モック)
vi.mock("../services/studyRecords");
// ファクトリあり(手動でモックの中身を定義)
vi.mock("../services/studyRecords", () => ({
fetchRecords: vi.fn().mockResolvedValue({ data: 'default data' }),
saveRecord: vi.fn(),
}));
ファクトリを渡すと、Vitestは実モジュールを読み込まずに「この関数が返したオブジェクトをモックとして使う」ため、supabase.ts は一切実行されません。
vi.mock があっても失敗する理由
ポイントは ファクトリなしの vi.mock の挙動 です。
vi.mock("../services/studyRecords") をファクトリなしで呼ぶと、Vitestは実際のモジュールを一度ロードしてエクスポートを解析し、そこから自動モックを生成します。
このロード時に supabase.ts も実行されるため、createClient(undefined, undefined) が呼ばれてエラーになるわけです。
ローカルでは .env ファイル(gitignored)に VITE_SUPABASE_URL が設定されているので通りますが、CI環境にはそのファイルがないため失敗します。「ローカルだけ通る」の正体はこれでした。
解決策:vite.config.ts でダミー値を注入する
vite.config.ts の test セクションに env を追加して、テスト実行時に常にダミー値を提供するようにします。
test: {
env: {
VITE_SUPABASE_URL: 'https://placeholder.supabase.co',
VITE_SUPABASE_PUBLISHABLE_KEY: 'placeholder-key',
},
},
これだけです。ダミー値があれば createClient のインスタンス化は通ります。テスト内では studyRecords がモックされているため、実際にSupabaseへ接続することはありません。
まとめ・学んだこと
-
vi.mock(path)はファクトリなしだと実モジュールをロードするため、モジュールレベルの副作用(APIクライアントの初期化など)は回避できない - 環境変数に依存するモジュールレベルの初期化は、テスト環境で壊れやすい
- 対策は2つで、
-
vite.config.tsのtest.envでダミー値を注入する(今回の方法) -
vi.mockにファクトリを渡してモジュールロードを完全に置き換える
-