nextjs-auth0を使用時の、privateなページに対する簡単なテストの実装方法
はじめに
最近、Next.js + Auth0の組み合わせでアプリケーションを開発することが多いのですが、
要認証の非公開ページもあるため、withPageAuthRequiredやwithAPIAuthRequiredを使用しています。
その場合のテストの実装について、公式ではSDKのインスタンスを作成する方法が紹介されているわけですが、単にwithPageAuthRequired
withAPIAuthRequired
の中の挙動を確認したいだけならwithPageAuthRequired
withAPIAuthRequired
をspy化して、テストを実装すればいいのでは?と、考えやってみたので記事化してみました。
本記事はnextjs-auth0/examples/kitchen-sink-exampleをもとに、
- Jest
- React Testing Library
を使用してテストの実装を行います。
今回使用したサンプルコードは
に置いておきますので、実装する方は参考にしてみてください。
withPageAuthRequired
を使用したページ
SSRでテスト対象のページはこちらです。
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のテストについて、getServerSideProps
とcomponent
を一緒にテストするかどうかは、議論あるところかもしれませんが、今回の例では、withPageAuthRequired
をspy化するところを見せるために、getServerSideProps
のテストのみをした場合を示したいと思います。
getServerSideProps
とcomponent
について、独立してテストした場合にcomponent
のテストをどのようにするか気になる方は、私のサンプルコード見ていただけると幸いです。
それでは、getServerSideProps
のテストです。
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に差し替わらずに動いてしまいました。
withPageAuthRequired
を使用したページ
CSRでテスト対象のページはこちらです。
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のやり方とだいたい同じです。
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よりも少しシンプルなコードにしました
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をフル活用して実装しています。
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