お知らせ

現在サイトのリニューアル作業中のため、全体的にページの表示が乱れています。
  • global.navigatorをモックにする方法
    • モックというか書き換えてるだけ
  • jestのモック機能はプロパティのモックが出来ないので、実オブジェクトを強制的に書き換えて実施する

サンプルコード

  • Object.defineProperty() を利用して実装
    • value, プロパティが返す値
    • configurable, 再定義可能かどうか、設定しないと再実行でコケる
  • 実際の使用ではユーティリティ関数を作っておき、 afterAll()navigator.userAgentを初期値に戻すのが望ましい
Object.defineProperty(global.navigator, 'userAgent', {
  value:
      'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.82 Safari/537.36',
  configurable: true,
});
  • 理論上可能そうだったので、ひたすら試行錯誤してたらできたのでメモがてら
  • axiosそのものをモックして、その結果に応じた処理が想定通り走っているかどうかを確認したい
  • create-react-appで作成したTypeScript Reactのプロジェクトにnpm tを掛けてテストすることを想定
  • 但し検証用のプロジェクトは@lycolia/ts-boilerplate-generator-cliで作成

確認環境

Env Ver
react 17.0.1
react-scripts 4.0.2
typescript 4.1.3

確認内容

検証対象

  • ページ読み込み時に一回だけAPIを蹴ってなんかすることを想定
import axios from 'axios';
import { useEffect } from 'react';

export const AsyncHookExample = () => {
  useEffect(() => {
    axios
      .get('https://localhost/')
      .then((res) => console.log(res))
      .catch((err) => console.error(err));
  }, []);

  return <p />;
};

検証方法

  • axiosをモックした上で、モック関数のPromiseの解決を待ち、 PromiseのコールバックをspyOnして検査する内容
  • モック関数のPromiseの解決法を求めるのにハマりましたが、これはfoo.mock.results[0].valueに対してawait expect(foo.mock.results[0].value).resolvesとしてやればいける
  • 細かいことはソースコードのコメント参照
import { render } from '@testing-library/react';
import axios from 'axios';
import { AsyncHookExample } from './AsyncHookExample';

// `axios` をモックにする
jest.mock('axios');
// `axios` のモックを取得
const mockedAxios = axios as jest.Mocked<typeof axios>;
// `console.log()` を `spyOn'
const spiedConsoleLog = jest.spyOn(console, 'log');
// `console.error()` を `spyOn'
const spiedConsoleError = jest.spyOn(console, 'error');

describe('AsyncHookExample', () => {
  it('resolve promise in Hook', async () => {
    // テストに期待する結果
    // `axios` はモックなので値は適当
    const beResult = {
      status: 200,
      data: null,
    };
    // `axios` のモックが `resolve` する値を設定
    mockedAxios.get.mockResolvedValue(beResult);
    // コンポーネントを `render` して `useEffect` を走らせる
    render(<AsyncHookExample />);
    // `axios.get()` が呼ばれたことを確認
    expect(mockedAxios.get).toBeCalled();
    // モックの結果を取得
    const testResult = mockedAxios.get.mock.results[0].value;
    // `reject` された値が期待通りであることを確認
    await expect(testResult).resolves.toEqual(beResult);
    // `useEffect` の中の `Promise` の中にある `console.log()` が呼ばれたことを確認
    expect(spiedConsoleLog).toBeCalled();
  });

  it('reject promise in Hook', async () => {
    // テストに期待する結果
    // `axios` はモックなので値は適当
    const beResult = {
      status: 400,
      data: null,
    };
    // `axios` のモックが `reject` する値を設定
    mockedAxios.get.mockRejectedValue(beResult);
    // コンポーネントを `render` して `useEffect` を走らせる
    render(<AsyncHookExample />);
    // `axios.get()` が呼ばれたことを確認
    expect(mockedAxios.get).toBeCalled();
    // モックの結果を取得
    const testResult = mockedAxios.get.mock.results[0].value;
    // `reject` された値が期待通りであることを確認
    await expect(testResult).rejects.toEqual(beResult);
    // `useEffect` の中の `Promise` の中にある `console.error()` が呼ばれたことを確認
    expect(spiedConsoleError).toBeCalled();
  });
});

あとがき

  • mockResolvedValueのスコープやらモックのPromiseを解決させるマッチャやら調べるのに手間取った
    • あとはそもそもPromiseが解決した結果がどこに入るのかとか、兎に角あれこれ
  • expect(mockedAxios.get).toBeCalled()が通るのはすぐに気づいたので、なら出来るだろうとひたすら調べてた
    • とりあえず言えることはJestの公式ドキュメントが大正義なので、ここを穴が開くまで読めば大抵のことは解決する
投稿日:
ライブラリ::Next.js言語::TypeScript

nextjs.orgをサラーっと読み流したログ
Next.js 9.3時点でのドキュメント、SSRについては基本言及しない

ドキュメントの和訳でもなんでもなく単なる書き殴りです
書き殴りなので情報の漏れなどは当然ありますし、そもそも知りたいところしか読んでません。
つまり、ここに書いてあることは何も信じてはならないので、真実は君の目で確かめて欲しい。
私はまだコードも書いてないし何もわからない、知らない、関知できない!すまないッッ!!!!

Getting Started

システム要件

  • Node.js 10.13 or later
  • MacOS, Windows (including WSL), and Linux are supported

プロジェクトの生やし方

  • create-next-app --example with-typescript project-name

プロジェクトの蹴り方

  • dev - 開発サーバーが上がるよ
  • build - プロダクションビルドを生成するよ
  • start - プロダクションサーバーを上げるよ

Basic Feature

Pages

ページの生やし方
  • pages/about.tsxのようにすると静的ページが生えるよ。やったね。
  • pages/posts/[id].tsxなら動的ルーティングも可能だ!
  • コンポーネントをexportするときは面倒でもexport defaultしないと上手く行かないので注意しよう
    • これはダメ
      • export const Foo = () => { return <p /> };
    • 多分コンポーネントでなければ大丈夫?
事前レンダリング
  • Next.jsでは基本的にページを事前レンダリングするよ
  • つまり静的HTMLとして事前に吐き出すよ
    • これによってSEO効果やページのパフォーマンスが向上するよ
    • このHTMLには最小限のJSが含まれていて、ページの対話性は保たれているよ
      • この工程をhydrationと呼ぶよ
二つの事前レンダリング

Next.jsでは次の二種類のレンダリングに対応しているよ

  • Static Generation
    • 静的HTMLを吐くからSEOもパフォーマンスもバッチリ
  • Server-side Rendering
    • 毎回サーバーで捏ねるからパフォーマンスはイマイチ
    • CDNの設定も大変

勿論、CSRはどちらでも使えるのだけど、CSRのデータフェッチについてはこっち

Data Fetching

データの取得方法について

getStaticProps
  • この関数はライフサイクルフックなようなもので、Static Generationの場合、HTML生成前に実行されるよ
    • つまりサーバーサイドで事前にデータを拾っておいて詰め込む時に使う
export const getStaticProps = (context) => {
  return {
    props: {}, // will be passed to the page component as props
  };
};
  • contextの中身はここを参照
    • RouterのProps的なものが入っているっぽい?
    • 使わないのであれば省いてもいい
  • ここの戻り値はコンポーネントの引数に入ってくるよ
    • props required。突っ込みたいデータを入れてね
    • revalidate Optional。ISRをする時に使うよ。詳しくはここ
    • notFound Optional。trueにすると404が表示される。使用例は以下のような感じ
export const getStaticProps = (context) => {
  const res = await fetch(`https://.../data`);
  const data = await res.json();
  if (!data) {
    return {
      notFound: true,
    };
  }
  return {
    props: {}, // will be passed to the page component as props
  };
};

Static File Serving

  • public/配下は静的ファイルの配置場所として使える
    • 例えば次のコードでYAMLファイルの読み込みができたりする
      • fs.readFileSync('public/biography.yaml')
  • 余談ですがgetStaticProps()path.join(process.cwd(), 'contents/biography.yaml')みたいに書いてやるとpublic外のファイルを読めます

確認環境

  • historyのバージョンとreact-router-domのバージョンが上手く噛み合ってないと既存実装で型エラーが出る可能性がある
Env Ver
react 17.0.2
react-dom 17.0.2
react-router-dom 5.2.0
react-scripts 4.0.3
typescript 4.2.3
history 4.10.1

サンプルコード

  • ToPageに飛ばしたlocation.stateを検査する内容

ToPage.tsx

export const ToPage = () => {
  const location = useLocation<{ test?: string }>();
  return <p>from {location.state?.test ?? 'nothing'}</p>;
};

ToPage.spec.tsx

const renderWithRouter = (ui: JSX.Element, { route = '/' } = {}) => {
  window.history.pushState({}, 'Test page', route);

  return render(ui, { wrapper: BrowserRouter });
};

describe('onload', () => {
  it('from history.push', () => {
    const history = createMemoryHistory<{ test: string }>();
    history.push(AppRoute.to.path, { test: 'foo' });
    const { container } = renderWithRouter(
      <Router history={history}>
        <ToPage />
      </Router>
    );

    expect(container).toHaveTextContent('from foo');
    expect(history.location.state.test).toBe('foo');
  });
});