🔓

nextjs-auth0を使用時の、privateなページに対する簡単なテストの実装方法

2021/08/09に公開

はじめに

最近、Next.js + Auth0の組み合わせでアプリケーションを開発することが多いのですが、
要認証の非公開ページもあるため、withPageAuthRequiredwithAPIAuthRequiredを使用しています。

その場合のテストの実装について、公式ではSDKのインスタンスを作成する方法が紹介されているわけですが、単にwithPageAuthRequired withAPIAuthRequiredの中の挙動を確認したいだけならwithPageAuthRequired withAPIAuthRequiredをspy化して、テストを実装すればいいのでは?と、考えやってみたので記事化してみました。

本記事はnextjs-auth0/examples/kitchen-sink-exampleをもとに、

  • Jest
  • React Testing Library
    を使用してテストの実装を行います。

今回使用したサンプルコードは
https://github.com/mongolyy/nextjs-auth0-test-example
に置いておきますので、実装する方は参考にしてみてください。

SSRでwithPageAuthRequiredを使用したページ

テスト対象のページはこちらです。

profile-ssr.tsx
import React from 'react';
import { UserProfile, withPageAuthRequired } from '@auth0/nextjs-auth0';
import { NextPage } from 'next';

type ProfileProps = {
  user: UserProfile
  sampleProp: string
};

const Profile: NextPage<ProfileProps> = ({ user, sampleProp }: ProfileProps) => {
  return (
    <>
      <h1>Profile</h1>

      <div>
        <h3>Profile (server rendered)</h3>
        <pre data-testid="email">{JSON.stringify(user, null, 2)}</pre>
        <pre data-testid="sampleProp">{sampleProp}</pre>
      </div>
    </>
  );
};

export const getServerSideProps = withPageAuthRequired({
  async getServerSideProps(ctx) {
    const key = ctx.query.key || 'noKey';

    return { props: { sampleProp: key }};
  }
});

export default Profile;

クエリストリングについて、keyというキーがあればそれを、無ければ、noKeyをpropsとして返すgetServerSidePropsを作成しています。

pagesのテストについて、getServerSidePropscomponentを一緒にテストするかどうかは、議論あるところかもしれませんが、今回の例では、withPageAuthRequiredをspy化するところを見せるために、getServerSidePropsのテストのみをした場合を示したいと思います。
getServerSidePropscomponentについて、独立してテストした場合にcomponentのテストをどのようにするか気になる方は、私のサンプルコード見ていただけると幸いです。
https://github.com/mongolyy/nextjs-auth0-test-example/blob/main/test/pages/profile-ssr.test.tsx

それでは、getServerSidePropsのテストです。

profile-ssr.test.tsx
import '@testing-library/jest-dom';
import httpMocks from 'node-mocks-http';
import { GetServerSidePropsContext } from "next";
import { WithPageAuthRequiredOptions } from "@auth0/nextjs-auth0";

describe('Profile-ssr page', () => {
  jest.spyOn(require('@auth0/nextjs-auth0'), 'withPageAuthRequired')
    // @ts-ignore
    .mockImplementation((opts: WithPageAuthRequiredOptions) => {
      return opts.getServerSideProps;
    });

  const getServerSideProps = require('../../src/pages/profile-ssr').getServerSideProps;

  describe('getServerSideProps', () => {
    const expectedKey = 'bar';

    const ctx: GetServerSidePropsContext = {
      req: httpMocks.createRequest(),
      res: httpMocks.createResponse(),
      params: undefined,
      query: { 'key': expectedKey },
      resolvedUrl: ''
    };

    test('ログインしている場合はpropsを返す', async () => {
      const result = await getServerSideProps(ctx);
      expect(result).toStrictEqual({ props: { sampleProp: expectedKey }});
    });

    test('クエリストリングにkeyがない場合は、noKeyを返す', async () => {
      const noKeyCtx = {
        ...ctx,
        query: {}
      };
      const result = await getServerSideProps(noKeyCtx);
      expect(result).toStrictEqual({ props: { sampleProp: 'noKey' }});
    });
  });
});

ポイントとしては、withPageAuthRequiredのspy化です。
単純ではありますが、withPageAuthRequiredの中で宣言されたgetServerSidePropsをそのまま返すという実装にしました。

また、私が嵌ったポイントとして、getServerSidePropsの生成と、withPageAuthRequiredのspy化のタイミングというのがありました。
getServerSidePropsを先に生成してしまうと、profile-ssr.tsx内の名前付きimportが先に走ってしまっているようで、withPageAuthRequiredのspyを生成してもspyに差し替わらずに動いてしまいました。

CSRでwithPageAuthRequiredを使用したページ

テスト対象のページはこちらです。

profile.tsx
import React from 'react';
import { useUser, withPageAuthRequired } from '@auth0/nextjs-auth0';
import Layout from '../components/layout';

export default withPageAuthRequired(function Profile(): React.ReactElement {
  const { user, isLoading } = useUser();

  return (
    <Layout>
      <h1 data-testid="h1">Profile</h1>

      {isLoading && <p>Loading profile...</p>}

      {!isLoading && user && (
        <>
          <p>Profile:</p>
          <pre data-testid="email">{JSON.stringify(user, null, 2)}</pre>
        </>
      )}
    </Layout>
  );
});

nextjs-auth0のでもとほぼ同じく、ユーザー情報を表示するページです。
テストはssrのやり方とだいたい同じです。

profile.test.tsx
import React from 'react';
import '@testing-library/jest-dom';
import { render, screen } from "@testing-library/react";
import { UserProvider } from "@auth0/nextjs-auth0";

describe('Profile page', () => {
  jest.spyOn(require('@auth0/nextjs-auth0'), 'withPageAuthRequired')
    .mockImplementation((Component) => {
      return Component;
    });

  const Profile = require('../../src/pages/profile').default;

  test('propsを渡すと、メールアドレスが表示される', async () => {
    const user = {
      email: 'test@example.com'
    };
    const { getByTestId } = render(
      <UserProvider user={user}>
        <Profile user={user} />
      </UserProvider>
    );
    expect(getByTestId('h1')).toHaveTextContent('Profile');
    expect(await screen.findByText('Profile:')).toBeInTheDocument();
    expect(getByTestId('email')).toHaveTextContent(user.email);
  });
});

withPageAurhRequiredのspyですが、SSRのときと同様に中身を返すようなspyを作成しました。

withAPIAuthRequiredを使用したAPI Route

API Routeはnextjs-auth0よりも少しシンプルなコードにしました

shows.ts
import { withApiAuthRequired } from '@auth0/nextjs-auth0';

export default withApiAuthRequired(async function shows(req, res) {
  if (req.method === 'GET') {
    const key = req.query.key || 'noKey';
    res.status(200).json({ key });
  } else {
    res.status(404).json({ errorMessage: 'Not Found' });
  }
});

テストはnode-mocks-httpをフル活用して実装しています。

shows.test.ts
import { NextApiRequest, NextApiResponse } from 'next';
import '@testing-library/jest-dom';
import httpMocks from 'node-mocks-http';

describe('shows api', () => {
  jest.spyOn(require('@auth0/nextjs-auth0'), 'withApiAuthRequired')
    .mockImplementation((apiRoute) => {
      return apiRoute;
    });
  const shows = require('../../../src/pages/api/shows').default;

  test('keyがある場合、keyを返す', async () => {
    const expectedKey = 'fooKey';
    const mockReq = httpMocks.createRequest<NextApiRequest>({
      method: 'GET',
      query: { key: expectedKey }
    });
    const mockRes = httpMocks.createResponse<NextApiResponse>();

    await shows(mockReq, mockRes);

    expect(mockRes.statusCode).toBe(200);
    expect(mockRes._getData()).toBe(JSON.stringify({ key: expectedKey }));
  });

  test('keyがない場合、デフォルトのkeyを返す', async () => {
    const mockReq = httpMocks.createRequest<NextApiRequest>({
      method: 'GET',
      query: {}
    });
    const mockRes = httpMocks.createResponse<NextApiResponse>();

    await shows(mockReq, mockRes);

    expect(mockRes.statusCode).toBe(200);
    expect(mockRes._getData()).toBe(JSON.stringify({ key: 'noKey' }));
  });

  test('リクエストのメソッドがGET以外の場合、404を返す', async () => {
    const mockReq = httpMocks.createRequest<NextApiRequest>({
      method: 'POST',
      query: {}
    });
    const mockRes = httpMocks.createResponse<NextApiResponse>();

    await shows(mockReq, mockRes);

    expect(mockRes.statusCode).toBe(404);
    expect(mockRes._getData()).toBe(JSON.stringify({ errorMessage: 'Not Found' }));
  });
});

withApiAuthRequiredのspyですが、withPageAurhRequiredと同様、中身を返すようなspyを作成しました。

おわりに

spy化のコードは、見てみると単純ですね。
ただ私の場合はnode経験も浅く、node moduleのモック化ってどうやれば??とか、テスト対象のコンポーネント、関数と、spyの生成の順序のところでだいぶ苦戦しました。

Next.js + Auth0のアーキテクチャを採用している方の参考になれば幸いです。

Discussion