はじめに
これまでReact学習の一環としていくつかのアプリ制作を行ってきて、その際にJestを使ったテストを実行することがありました。
Jestのテストで必要不可欠となるモック関数のしくみと仮データの渡し方について、自分なりに気づいたことを備忘録代わりとして記事にしようと思います。
この記事は学習のアウトプットを目的としたものなため一部不正確な記述がある可能性がございます。予めご了承ください。
そもそも「モック」(「モックアップ」)とは?
モック関数について考える前提として、そもそも「モック」とは何かを見てみます。
モック(モックアップ)の意味は以下のようなものがあります。
モックアップとは、実物とほぼ同様に似せて作られた模型のことである。
引用元:https://ejje.weblio.jp/content/mock+up#google_vignette
商品化する前に作った模型やレプリカ、本物ではないもののことを指します。
引用元:https://news.mynavi.jp/article/20210611-1892998/
上記の記述から、モック関数は関数版のモック(モックアップ)、つまり実際の関数と似せて作られているが本物の関数ではないものとなります。
その前提を踏まえて実際のモック関数の記述を見てみましょう。
①jest.fn()
について
jest.fn()
は、先ほどの例を踏まえると特定の関数のレプリカを作るための記述です。
jest.fn()
を使った関数は**「関数のレプリカ」**です。事前に関数の処理が記載されていた関数名であっても、モック化した関数にはその関数の処理は実行されません。
使い方としては以下のような形です。ここではsampleFunction
をモック関数として登録しています。
const sampleFunction = jest.fn()
②jest.mock
について
jest.mock
は他のモジュールの関数をモック化する場合に使います。
import { hoge } from "./sample"
jest.mock("./sample"), ()=>{
return hoge; jest.fn()
}
上記の例で言うとインポートしてきたhoge
関数をモック化しています。
インポートした関数(hoge
)をモック化することで、関数hoge
のインポート元で定義されていた処理は該当のファイル内で実行されなくなります。
裏を返すと、該当の関数(hoge
)は「元々の処理は実行されないものの関数としては存在している」状態です。
これがどういった意味を持つのかは次の章で見ていきます。
モック関数の活用例
モック関数を活用する場面の例としてデータベース連携の絡むテストコードが挙げられます。
もしDB連携用の関数をそのままテストコードで使うと、テストコード内で実際のDB内の値を参照したりDBに値を挿入する処理が発生します。
その場合、DB内のデータに依存したテストとなるためテストの汎用性が下がります。また、挿入関数をそのまま使う場合、テストを行うたびにデータがDB内に挿入されてしまいます。
具体例を以下に挙げてみます。
下記のようなコードの記述がある場合、App.tsx
のコンポーネントをそのままテスト対象にすると、getAllUserSkills
関数の処理が実行された際に実際のDBの値を参照することになります。
export const getAllUserSkills = async () => {
const response = await supabase.from("skills").select("*");
if (response.error) {
throw new Error(response.error.message);
}
return response.data;
};
import { getAllUserSkills } from "./utils/supabase-function";
// 上記の関数が使われている場所の処理を抜粋
useEffect(() => {
const getUserSkill = async () => {
const gettingSkill = await getAllUserSkills();
// 以下の処理は略
};
getUserSkill();
}, []);
このままだとDBの値やデータ数が変わるごとにテストの結果が変わりかねません。
そこで活躍するのが先述のモック関数です。
getAllUserSkills
をモック化することで、テストコード内でgetAllUserSkills
関数を実行した扱いにでき、DBからの直接データ参照を防げます。
import { getAllUserSkills } from "./utils/supabase-function";
jest.mock("./utils/supabase-function"), ()=> {
return getAllUserSkills: jest.fn()
}
ただし、この状態ではモック化されたgetAllUserSkills
関数の実体がないため、読み込んだ扱いにするデータが存在しません。
問題を解消するためにはモック関数に値を登録する必要があります。
そこで使われるのがmockResolvedValue
をはじめとしたMock Functionsです。
Mock Functionとモック関数への値の登録
jest.fn()
でモック化した関数にはMock Function
を用いて値を登録したりすることができます。
ここではモック関数に「値を注入して処理が通った扱いにする」ためのMock FunctionであるmockResolvedValue
を見てみます。
test('async test', async () => {
const asyncMock = jest.fn().mockResolvedValue(43);
await asyncMock(); // 43
});
引用元:https://archive.jestjs.io/docs/ja/22.x/mock-function-api#mockfnmockreturnvaluevalue
上記コードではモック化された関数asyncMock
に対して43という値を注入しています。
ここではasyncMock
関数は「43を返す関数」として定義されています。
先ほどのgetAllUserSkills
関数のケースも見てみます。
以下のように、先にテストデータとして注入したい値をあらかじめ作成しておくことで直接DBに接続しなくても擬似的なデータフェッチを実現させています。
import { getAllUserSkills } from "./utils/supabase-function";
jest.mock("./utils/supabase-function"), ()=> {
return getAllUserSkills: jest.fn()
};
describe("登録ページのテスト", () => {
const RegisterData = {
user_id: "sample",
name: "田中太郎",
description: "よろしく",
};
test("サンプルテスト", async () => {
(insertUserData as jest.Mock).mockResolvedValue(RegisterData);
render(<App />)
await waitFor(async () => {
// ここにテストしたい項目が入ります
});
});
注意点として、 関数のモック化は該当のコンポーネントを呼び出す前に記述してください。
モック関数を先に定義しておかないと、コンポーネント内にある関数(モック化されていないもの)が先に呼び出されてしまい、モック関数の意味をなさなくなるためです。
最後に
ここまで読んでいただきありがとうございます。
モック関数の概念の理解、モック関数を活用したテストコードの作成は初学者にとっては難関だと感じています。実際、私も未だに苦戦しています。
同じようにモック関数に苦しんでいる人の支えになれば幸いです。
参考資料