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 × Supabase】Jestで中間テーブルを含む多段クエリのモックを実装する方法

Last updated at Posted at 2025-07-22

問題

アプリでは、中間テーブル(word_tags)を使って複数のテーブルからデータを取得し、テーブルごとに異なるデータを返している。
Jestでこのような複雑なデータ取得のモック(テーブルごとに返すデータを切り替える処理)の書き方が分からなかった。

実装

実際のコードでは、以下のように中間テーブル(word_tags)を経由してデータを取得している。

  • 中間テーブル(word_tags)を使い、多対多の関係を表現し、単語とタグを紐付けている
  • 3段階(words → word_tags → tags)でデータを取得し、最終的に「単語ごとにタグ名を表示」

(1)wordsテーブルから、ユーザーが持っている単語一覧を取得

sample.tsx
const { data: words, error: wordsError } = await supabase
    .from("words")
    .select("*")
    .eq("user_id", user ? user.id : null);

(2)word_tagsテーブルから、(1)で取得した単語に紐づくタグ情報を取得
※ wordIdsには事前処理を加えている

sample.tsx
const { data: wordTags, error: wordTagsError } = await supabase
    .from("word_tags")
    .select("*")
    .in("word_id", wordIds)

(3)tagsテーブルから、(2)で取得したタグIDに対応するタグ名を取得
※ tagIdsには事前処理を加えている

sample.tsx
({ data: tags, error: tagsError } = await supabase
    .from("tags")
    .select("*")
    .in("id", tagIds)

解決方法

テーブルごとに返すデータを切り替えるには、builder.then で分岐させる。

test.tsx
// supabaseをモック
jest.mock("../lib/supabaseClient");
// supabaseのモックデータをインポート
import { supabase } from "../lib/supabaseClient";

// 中略

type Word = {
    id: number;
    word_name: string;
    mastered: boolean;
    use: boolean;
    explain: boolean;
};

// 単語
const userWords = [
    { id: 1, word_name: "hello", user_id: mockDummyUser.id, use: true, explain: true, mastered: false },
    { id: 2, word_name: "world", user_id: mockDummyUser.id, use: true, explain: true, mastered: false },
];

// タグ
const testTags = [
    { id: 1, tag_name: "greeting", user_id: mockDummyUser.id },
    { id: 2, tag_name: "noun", user_id: mockDummyUser.id },
    { id: 3, tag_name: "great", user_id: mockDummyUser.id },
];

// 単語とタグの紐付け(word_tagsテーブルのモック)
const testWordTags = [
    { word_id: 1, tag_id: 1 }, // hello → greeting
    { word_id: 1, tag_id: 2 }, // hello → noun
    { word_id: 2, tag_id: 1 }, // world → greeting
    { word_id: 2, tag_id: 2 },// world → noun
];

// 中略
    
const builder = {} as {
    select: jest.Mock;
    in: jest.Mock;
    // 中略 eq, is, insert, single...など実装
    then?: (resolve: (value: unknown) => unknown) => unknown;
};

// どのメソッド(selectやinなど)を呼んでも、常に同じbuilderオブジェクトを返すようにしている
// これにより、select().in().eq()... のようにメソッドチェーンができる
builder.select = jest.fn(() => builder);
builder.in = jest.fn(() => builder);

// 中略 eq, is, insert, single...など実装

// tableごとに返却するモックデータを変える
builder.then = (resolve) => {
    // .mock.calls
    // → このモック関数が「呼ばれた履歴(引数の配列)」を取得
    // 例: supabase.from("words") と呼ばれると、mock.calls に [["words"]] が追加される
    // .at(-1)
    // → 呼び出し履歴の最後(最新)の1件を取得
    // 例: 3回呼ばれていたら、3回目の引数配列が返る
    // ?.[0]
    // → その履歴(配列)の最初の引数を取得
    const lastFromTable = (supabase.from as jest.Mock).mock.calls.at(-1)?.[0];

    if (lastFromTable === "words") {
        return resolve({ data: userWords, error: null });
    }
    if (lastFromTable === "word_tags") {
        // console.log("testWordTags,", testWordTags,);
        return resolve({ data: testWordTags, error: null });
    }
    if (lastFromTable === "tags") {
        // console.log("testTags", testTags);
        return resolve({ data: testTags, error: null });
    }
    return resolve({ data: [], error: null });
};

// 各テスト実行前に必ず実行される初期化処理
beforeEach(() => {
    // Jestのモック関数(jest.fn())の呼び出し履歴や返り値設定をすべてリセット
    // テスト間で前回の状態が残らず、毎回クリーンな状態でテストできる
    jest.clearAllMocks();

    // supabase.fromの動作を上書き(supabaseはfromから始まるので、実質全て上書き)
    // supabase.from(...) が呼ばれたとき、「テストファイル内」の builder を返すようにする
    (supabase.from as jest.Mock).mockImplementation(() => {
        return builder;
    });
});

補足

実装側のコードにconsole.logを仕込んでおくと、ターミナルでテストを実行したときに「SupabaseのAPI通信部分が、Jestのモックデータに正しく書き換わっているか」を確認できる。
本番APIではなく、テスト用のダミーデータが使われていることを目で見て確認できる。

終わりに

どこのデータを変えようとしているか、変えるためにはどこを変更するか考える必要があり、データの流れを1つ1つ細かく追うことが重要だと思いました。

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?