はじめに
以前の記事で紹介したSupabaseのモックでは、実際のテスト実行時にエラーが発生しました。原因はモックの設計がSupabaseの実際の使い方と合っていなかったためです。この記事では、エラーの内容と対策方法を解説します。
テストだけでなく、JSの実装にもつかうPromiseチェーンについても解説しています。
エラー内容
エラーメッセージ
TypeError: _supabase.default.from(...).insert(...).select is not a function
エラーが発生した箇所(テストコード):
await waitFor(() => {
expect(screen.getAllByTestId("title")).toHaveLength(2);
詳細
エラーの原因は、モックのinsert()が返すオブジェクトにselect()メソッドが存在しなかったことです。
実際のアプリのコード(index.jsx):
const { data, error } = await supabase
.from('study-record')
.insert({ title: title, time: time })
.select() // ← insert()の後にselect()をチェーンしている
元々のモック設計:
from: jest.fn(() => ({
select: jest.fn(() => ...),
insert: jest.fn(() => ({
select: jest.fn(() => ...) // ← from()が呼ばれるたびに新しいオブジェクトが生成される
})),
}))
from()が呼ばれるたびに新しいオブジェクトを生成する設計だったため、insert().select()のメソッドチェーンが正しく解決されず、select is not a functionエラーが発生しました。
根本原因:メソッドチェーンの仕組み
Supabaseのようなメソッドチェーン(from().insert().select())を実現するには、各メソッドが次のメソッドを持つオブジェクトを返す必要がある。
from("study-record") → QueryBuilderオブジェクトを返す(insert, select, deleteを持つ)
↓
.insert({...}) → オブジェクトを返す(selectメソッドを持つ)
↓
.select() → Promiseを返す(最終的なデータ取得)
つまり、モックでも同じ構造を再現しないといけない:
// NG: insert()がselectを持たないオブジェクトを返している
insert: jest.fn(() => Promise.resolve({...})) // ← これだとinsert().select()ができない
// OK: insert()がselectメソッドを持つオブジェクトを返す
insert: jest.fn(() => ({
select: jest.fn(() => Promise.resolve({...})) // ← これでinsert().select()が動く
}))
ポイント: メソッドチェーンでは、最後のメソッドだけがPromiseを返し、途中のメソッドは「次のメソッドを持つオブジェクト」を返す。
エラー解決
モックの改善
以下のように修正しました:
jest.mock("../supabase.js", () => {
const mockRecords = [
{
id: 1,
title: "test",
time: 3,
}
];
// 1. 共通のmockQueryBuilderオブジェクトを作成
const mockQueryBuilder = {
select: jest.fn(() => Promise.resolve({ data: mockRecords, error: null })),
insert: jest.fn((newRecord) => ({
select: jest.fn(() => Promise.resolve({
data: { ...newRecord, id: mockRecords.length + 1 }, // 2. 引数を使って動的にデータを返す
error: null
}))
})),
delete: jest.fn(() => ({
eq: jest.fn(() => Promise.resolve({ error: null }))
}))
};
return {
__esModule: true,
default: {
from: jest.fn(() => mockQueryBuilder) // 3. 同じオブジェクトを返す
},
};
});
改善ポイント
| 変更点 | 理由 |
|---|---|
mockQueryBuilderを共通化 |
from()が同じオブジェクトを返すことで、メソッドチェーンが正しく動作する |
insert()がselectを持つオブジェクトを返す |
insert().select()のチェーンを可能にする |
mockRecordsはモック内に閉じ込める |
スコープを限定して堅牢性を高める |
おわりに
モックを作成する際は、実際のライブラリがどのように動作するかを正確に理解し、再現することが重要です。特にSupabaseのようなメソッドチェーンを使うライブラリでは、オブジェクトの参照関係に注意が必要でした。
チャッピー(GPT)と一緒に一つ一つ紐解いて手で書いてみました!
普段業務だとなかなかそうは行かないのでとても勉強になりましたし、血肉になっている気がします...
コツコツ継続します〜〜
学び: モックの設計は「本物と同じ振る舞い」だけでなく、「テストコードからの利用しやすさ」も考慮する。