1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Jestを使ってSpabaseをモックしてテストを書く②

Posted at

はじめに

以前の記事で紹介した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)と一緒に一つ一つ紐解いて手で書いてみました!
普段業務だとなかなかそうは行かないのでとても勉強になりましたし、血肉になっている気がします...
コツコツ継続します〜〜

学び: モックの設計は「本物と同じ振る舞い」だけでなく、「テストコードからの利用しやすさ」も考慮する。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?