🍚

LoaderFunctionとActionFunctionをテストする

2022/12/31に公開約5,600字

LoaderFunctionActionFunction のテストを行いました。ここではVitestを使ってテストを行っています。テスト環境の構築については、以下の記事をご覧ください。

https://zenn.dev/kyrice2525/articles/article_tech_006

LoaderFunction のテスト

テストを行うにあたって、ソースを修正します。app/routes/index.tsxを以下のように書き換えます。

app/routes/index.tsx
import { useLoaderData } from "@remix-run/react";
import { json } from "@remix-run/node";
import type { LoaderArgs, LoaderFunction } from "@remix-run/node";

export const loader: LoaderFunction = async ({ request }: LoaderArgs) => {
  return json({
    email: "test@test.com",
    password: "p@ssw0rd",
  });
};

export default function Index() {
  const loaderData = useLoaderData();
  return (
    <>
      <div>{loaderData.email}</div>
      <div>{loaderData.password}</div>
    </>
  );
}

loader()からの値を受け取ってそのまま表示しています。
npm run devを実行してhttp://localhost:3000を表示すると以下のような画面が表示されます。

0001

ではテストを書いていきます。testsフォルダ配下にroutes/index.test.tsxを作成します。

この loader()は固定値を返すだけなので、固定値が期待するものかどうかをテストします。

routes/index.test.tsx
import { expect, test } from "vitest";
import { loader } from "~/routes";

test("loaderがemailとpasswordを返す", async () => {
  const response = await loader({
    request: new Request("http://localhost:3000"),
    params: {},
    context: {},
  });

  const loaderData = await response.json();

  expect(loaderData).toStrictEqual({
    email: "test@test.com",
    password: "p@ssw0rd",
  });
});

loader()の引数として request、params、context を渡しています。Request を作成して渡します。

loader()で return されたオブジェクトを toStrictEqual()で比較しています。toStrictEqual()は vitest のテスト用の関数で、値が受け取った値と等しいか、同じ構造を持っているか、それがオブジェクト (再帰的に比較) であり、同じ型であるかを判定します。

ActionFunction のテスト

app/routes/index.tsxを以下のように書き換えます。入力フォームに文字を入力して送信するとinput-wordありが表示されます。文字を入力しないで送信するとinput-wordなしが表示されます。

app/routes/index.tsx
import { Form, useActionData, useLoaderData } from "@remix-run/react";
import { json } from "@remix-run/node";
import type {
  ActionArgs,
  ActionFunction,
  LoaderArgs,
  LoaderFunction,
} from "@remix-run/node";

export const loader: LoaderFunction = async ({ request }: LoaderArgs) => {
  return json({
    email: "test@test.com",
    password: "p@ssw0rd",
  });
};

export const action: ActionFunction = async ({ request }: ActionArgs) => {
  const form = await request.formData();
  const inputWord = form.get("input-word");

  if (inputWord?.length === 0) {
    return json({
      word: "input-wordなし",
    });
  } else {
    return json({
      word: "input-wordあり",
    });
  }
};

export default function Index() {
  const actionData = useActionData();
  const loaderData = useLoaderData();
  return (
    <>
      <div>{loaderData.email}</div>
      <div>{loaderData.password}</div>
      <div>
        <Form method="post">
          <input type="text" name="input-word" id="input-word" />
          <button type="submit">送信</button>
        </Form>
      </div>
      <div>{actionData?.word}</div>
    </>
  );
}

以下 2 点をテストします。

  • 入力フォームに文字が入力されている場合はinput-wordありを返すこと
  • 入力フォームに文字が入力されている場合はinput-wordなしを返すこと

テストを書いていきます。

routes/index.test.tsx
import { expect, test } from "vitest";
import { action, loader } from "~/routes";

test("loaderのテスト", async () => {
  const response = await loader({
    request: new Request("http://localhost:3000"),
    params: {},
    context: {},
  });

  const loaderData = await response.json();

  expect(loaderData).toStrictEqual({
    email: "test@test.com",
    password: "p@ssw0rd",
  });
});

test("入力フォームが入力されていない場合、「input-wordなし」が返ってくる", async () => {
  const body = new URLSearchParams({
    "input-word": "",
  });

  const request = new Request("http://localhost:3000", {
    method: "POST",
    body: body,
  });

  const response = await action({
    request: request,
    params: {},
    context: {},
  });

  const decoder = new TextDecoder();
  const reader = response.body.getReader();
  const { value } = await reader.read();
  const actionData = JSON.parse(decoder.decode(value));

  expect(response.status).toBe(200);
  expect(actionData).toStrictEqual({
    word: "input-wordなし",
  });
  reader.releaseLock();
});

test("入力フォームが入力されている場合、「input-wordあり」が返ってくる", async () => {
  const body = new URLSearchParams({
    "input-word": "test",
  });

  const request = new Request("http://localhost:3000", {
    method: "POST",
    body: body,
  });

  const response = await action({
    request: request,
    params: {},
    context: {},
  });

  const decoder = new TextDecoder();
  const reader = response.body.getReader();
  const { value } = await reader.read();
  const actionData = JSON.parse(decoder.decode(value));

  expect(response.status).toBe(200);
  expect(actionData).toStrictEqual({
    word: "input-wordあり",
  });
  reader.releaseLock();
});

URLSearchParams を設定します。入力フォームの name をinput-wordに設定しているので、キーをinput-wordにして文字列を設定します。
Request を作成します。body には先ほど作成した URLSearchParams()を設定します。
action()の引数として request、params、context を渡しています。

返ってきた response の body はReadableStreamなので、デコードする必要があります。
getReader()でリーダーを取得します。するとReadableStreamDefaultReaderが取得できます。これをread()で読み取ります。Promise を返してくるので、await で待ちます。すると value と done が取得できます。value をみると Uint8Array オブジェクトになっているので、これをデコードして JSON にしています。

最後にreleaseLock()でストリームのロックを解除して終了です。

最後に

LoaderFunctionActionFunction のテストを書いてきました。今までテスト書いてなかったので、どんどんテスト書いていこうと思います。

Discussion

ログインするとコメントできます