MENU

フロントエンド開発のモックにMSWという選択肢

こんにちはこんばんわ!積ん読が増え続けている佐々木です!

ここ最近フロントエンド開発時にモックをつかうことがあり、そこではモックにjson-serverを使っていました。
ですが、個人的にはあまり使い勝手が良くなかったので「もっと便利なのあるだろ〜」と思って調べてみたら「MSW(Mock Server Woker)」という結構イケてるライブラリがあったので実際に使ってみました!

目次

MSW(Mock Server Woker)

MSWはService Workerを通じてAPIリクエストをインターセプトし、モックのレスポンスを返すことができるライブラリです。
ローカル開発以外にJestなどのテストやStoryBookにも利用することができます。

https://mswjs.io/docs/

json-serverとは違って、別プロセスを立てる必要がないというのが個人的な推しポイントです。

公式ドキュメントより、本番環境ではMSWの利用を勧めておりませんのでご留意ください。

環境

2022/8/27時点
  • MacBook Pro (2019late)
    • Monterey v12.5

私はReact(TypeScript)でやってみましたので以下が主要な内容です。

  • node v18.2.0
  • react v18.2.0
  • typescript v4.7.4(後述のmswの兼ね合いで4.2.x <= 4.7.x で指定する必要がありました)

MSWのinstall

ではinstallから始めていきます(記事作成時、latestはv0.45.0でしたが念の為指定します)

npm install msw@0.45.0 --save-dev

install後にはmock用のディレクトリとhandlers.tsを作成しましょう。

mkdir src/mocks && touch touch src/mocks/handlers.ts

handlers.tsの中身は以下のように記載します。

// src/mocks/handlers.ts
import { graphql, GraphQLVariables } from 'msw';

export const handlers = [
  graphql.query('GetUserInfo', (req, res, ctx) => {
    const authenticatedUser = sessionStorage.getItem('is-authenticated');
    if (!authenticatedUser) {
      return res(
        ctx.status(400),
        ctx.errors([
          {
            message: 'Not authenticated',
            errorType: 'AuthenticationError',
          },
        ])
      );
    }
    return res(
      ctx.status(200),
      ctx.data({
        user: {
          auth: authenticatedUser,
          firstname: 'USER_FIRSTNAME',
          lastname: 'USER_LASTNAME',
        },
      })
    );
  }),

  graphql.mutation('Login', (req, res, ctx) => {
    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
    const username: string = req.variables.username;
    sessionStorage.setItem('is-authenticated', username);
    return res(
      ctx.status(201),
      ctx.data({
        login: {
          username,
        },
      })
    );
  }),
];

API定義

次はAPI定義を選択します。APIは「REST」と「GraphQL」から選択することができます。

今回は「GraphQL」で進めてみます。

GraphQLでのモックの場合は、GraphQLClientを設定する必要があるようです。

Since we will be working with a GraphQL API, we need to have a GraphQL client installed and configured in our application. We need that client primarily to dispatch queries and mutations. Please refer to the getting started steps of the respective client.

https://mswjs.io/docs/getting-started/mocks/graphql-api#pre-requisites

私はApolloClientを使用することにします。

npm install @apollo/client graphql

https://www.apollographql.com/docs/react/get-started

Service Worker -Browser-

次はブラウザでのリクエストをインターセプトさせるためのService Workerを作成します。

Service Workerは以下のコマンドを実行することで自動で作成されます。
なお、<PUBLIC_DIR>の部分は各JavaScriptプロジェクト毎にディレクトリを指定されていますので従いましょう。
( ReactやVue.jsなどでは public , GatsbyJSでは static に読み替えてください。)

npx msw init <PUBLIC_DIR> --save

ex.react)

npx msw init public/ --save

コマンドを実行すると、指定したディレクトリ内に「mockServiceWorker.js」が作成されます。

Service Workerの構成を設定します。

touch src/mocks/browser.ts

browser.tsには以下の内容を記載します。

// src/mocks/browser.ts
import { setupWorker } from 'msw'
import { handlers } from './handlers'

export const worker = setupWorker(...handlers)

設定についてはこれで終了です。

画面の作成

簡単な画面を作成して正常に動作するか試していきます。

まずApolloClientの設定を記載します。

touch src/ApolloClient.ts
// src/ApolloClient.ts
import { ApolloClient, HttpLink, InMemoryCache } from '@apollo/client'

const cache = new InMemoryCache()

const link = new HttpLink({
    uri: 'http://localhost:3000/graphql',
    fetch: (...args) => fetch(...args),
})

// Isolate Apollo client so it could be reused
// in both application runtime and tests.
export const client = new ApolloClient({
    cache,
    link,
})

次に入力フォームを作成します。

touch src/LoginForm.tsx
// src/LoginForm.tsx
import React, { useCallback, useState } from 'react';
import { gql, useMutation, useQuery } from '@apollo/client';

const LOG_IN = gql`
  mutation Login($username: String!) {
    login(username: $username) {
      username
    }
  }
`;

interface LoginSessionDetails {
  id: number;
  username: string;
}

// login form
export const LoginForm = () => {
  const [username, setUserName] = useState('');
  // mutation
  const [Login, { data, loading, error }] = useMutation<{ Login: LoginSessionDetails }, { username: string }>(LOG_IN, {
    variables: { username },
  });

  const handleUsernameChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
    setUserName(event.target.value);
  }, []);

  const handleFormSubmit = useCallback(
    (event: React.FormEvent<HTMLFormElement>) => {
      event.preventDefault();

      Login({
        variables: {
          username,
        },
      }).catch((err) => console.log(err));
    },
    [username, Login]
  );

  if (loading) {
    return <p>Loading...</p>;
  }

  if (error) {
    return <p>Error while fetching the user data ({error.message})</p>;
  }

  if (data) {
    return <UserInfo />;
  }

  return (
    <div>
      <h1>Login</h1>
      <form onSubmit={handleFormSubmit}>
        <div>
          <label htmlFor='username'>Username:</label>
          <input id='username' name='username' value={username} onChange={handleUsernameChange} />
          <button type='submit'>Submit</button>
        </div>
      </form>
    </div>
  );
};

interface UserInfo {
  firstname: string;
  lastname: string;
}

interface User {
  user: UserInfo;
}

const GET_USER_INFO = gql`
  query GetUserInfo {
    user {
      firstname
      lastname
    }
  }
`;

const UserInfo = () => {
  // queries
  const { loading, data, error } = useQuery<User>(GET_USER_INFO);

  if (loading) return <p>Loading ...</p>;

  return (
    <>
      {data && (
        <>
          <h3>FirstName:</h3>
          <div>{data.user.firstname}</div>
          <h3>LastName:</h3>
          <div>{data.user.lastname}</div>
        </>
      )}
      {error && (
        <>
          <div>{error.message}</div>
        </>
      )}
    </>
  );
};

次にindex.tsxを修正します。

// src/index.tsx
import React from 'react';
import { ApolloProvider } from '@apollo/client';
import { client } from './ApolloClient';
import { LoginForm } from './LoginForm';
import { createRoot } from 'react-dom/client';
import { worker } from './mocks/browser';

const container = document.getElementById('root');
const root = createRoot(container!); // createRoot(container!) if you use TypeScript

// Start the mocking conditionally.
if (process.env.NODE_ENV === 'development') {
  worker.start().catch((err) => console.log(err));
}

root.render(
  <React.StrictMode>
    <ApolloProvider client={client}>
      <LoginForm />
    </ApolloProvider>
  </React.StrictMode>
);

これで試してみる環境は整いましたので、ブラウザで確認していきます。

画面確認

http://localhost:3000/ にアクセスして検証ツールを開きます。

検証ツールのコンソールを開いて、以下のような表示がされていれば正常に動作しています。

現在の画面では、フォームに入力してSubmitボタンを押すと、mutation、queryを実行します。

入力してSubmitボタンを押すと、以下のような画面になることを確認出来ます。

ちなみになにも入力せずにSubmitボタンを押すとエラーを返すようにしています。

作成物のリンクはこちらです。

https://github.com/daisuke8000/msw-sample

最後に

基本的に公式のGettingStartedに従った内容になりますが、すごいシンプルに実装できました。

皆さんのフロントエンド開発のモック検討時の選択肢にぜひ加えてみてください!!

それではまた〜!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

miracleaveはワクワクする最高のITサービスをユーザーに提供するテクノロジー集団です。
「ITでワクワクできる未来へ」をミッションに掲げ、楽しいを創り出す組織から、お客様に感動を与えるようなサービスを届けること、そして、新たな挑戦をする人をデジタルコンテンツの力で後押し、幸せな未来を作っていきたいと考えています。

目次