Nightwatchでのコンポーネントテストはバージョン2.4で改良され、Reactコンポーネントのテスト(@nightwatch/react プラグイン経由)のサポートが大幅に改善されました。また、人気のTesting LibraryをNightwatchで使用するための新しいプラグイン – @nightwatch/testing-libraryもリリースしました。これはNightwatch v2.6以降で利用可能です。

ここでは、NightwatchとTesting Libraryを使用してReactコンポーネントをテストする方法の詳細な例を作成します。私たちは、Jestで書かれているReact Testing Libraryのドキュメントにある複雑な例を使用します。

このチュートリアルでは、次の方法について説明します。

  1. Viteを使用して新しいReactプロジェクトをセットアップします。これはNightwatchがコンポーネントテストに内部で使用しているものでもあります。
  2. NightwatchとTesting Libraryをインストールして設定します。
  3. @nightwatch/api-testingプラグインを使用してAPIリクエストをモックします。
  4. NightwatchとTesting Libraryを使用して、複雑なReactコンポーネントテストを作成します。

ステップ 0. 新しいプロジェクトを作成する

まず、Viteで新しいプロジェクトを作成します。

npm init vite@latest

プロンプトが表示されたら、ReactJavaScriptを選択します。これにより、ReactとJavaScriptで新しいプロジェクトが作成されます。

ステップ 1. NightwatchとTesting Libraryをインストールする

React用のTesting Libraryは、@testing-library/reactパッケージでインストールできます。

npm i @testing-library/react --save-dev

Nightwatchをインストールするには、initコマンドを実行します。

npm init nightwatch@latest

プロンプトが表示されたら、Component testingReactを選択します。これにより、nightwatch@nightwatch/reactプラグインがインストールされます。ドライバーをインストールするブラウザを選択します。この例ではChromeを使用します。

1.1. @nightwatch/testing-libraryプラグインをインストールする

v2.6以降、NightwatchはTesting Libraryクエリをコマンドとして直接使用するための独自のプラグインを提供しています。後でテストを作成するためにこれが必要になるので、今すぐインストールしましょう。

npm i @nightwatch/testing-library --save-dev

1.2 @nightwatch/apitestingプラグインをインストールする

この例には、コンポーネントをテストするために必要なモックサーバーが含まれています。@nightwatch/apitestingプラグインに付属の統合モックサーバーを使用します。以下を使用してインストールします。

npm i @nightwatch/apitesting --save-dev

ステップ 2. Loginコンポーネントを作成する

React Testing Libraryドキュメントと同じコンポーネントを使用します。新しいファイルsrc/Login.jsxを作成し、次のコードを追加します。

// login.jsx
import * as React from 'react'

function Login() {
  const [state, setState] = React.useReducer((s, a) => ({...s, ...a}), {
    resolved: false,
    loading: false,
    error: null,
  })

  function handleSubmit(event) {
    event.preventDefault()
    const {usernameInput, passwordInput} = event.target.elements

    setState({loading: true, resolved: false, error: null})

    window
      .fetch('http://localhost:3000/api/login', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify({
          username: usernameInput.value,
          password: passwordInput.value,
        }),
      })
      .then(r => r.json().then(data => (r.ok ? data : Promise.reject(data))))
      .then(
        user => {
          setState({loading: false, resolved: true, error: null})
          window.localStorage.setItem('token', user.token)
        },
        error => {
          setState({loading: false, resolved: false, error: error.message})
        },
      )
  }

  return (
    <div>
      <form onSubmit={handleSubmit}>
        <div>
          <label htmlFor="usernameInput">Username</label>
          <input id="usernameInput" />
        </div>
        <div>
          <label htmlFor="passwordInput">Password</label>
          <input id="passwordInput" type="password" />
        </div>
        <button type="submit">Submit{state.loading ? '...' : null}</button>
      </form>
      {state.error ? <div role="alert">{state.error}</div> : null}
      {state.resolved ? (
        <div role="alert">Congrats! You're signed in!</div>
      ) : null}
    </div>
  )
}

export default Login

ステップ 3. コンポーネントテストを作成する

Testing Libraryの基本原則の1つは、テストはユーザーがアプリケーションを操作する方法にできる限り似ている必要があるということです。JSXを使用してNightwatchでコンポーネントテストを作成する場合、Component Story Formatを使用して、コンポーネントストーリーとしてテストを記述する必要があります。これは、Storybookによって導入された宣言的な形式です。

これにより、実装方法ではなく、コンポーネントの使用方法に焦点を当てたテストを作成できます。これは、Testing Libraryの理念に沿ったものです。詳細については、Nightwatchドキュメントを参照してください。

この形式を使用してテストを作成することの素晴らしい点は、同じコードを使用してコンポーネントのストーリーを作成できることです。これは、Storybookでコンポーネントをドキュメント化し、紹介するために使用できます。

3.1 有効な認証情報によるログインテスト

新しいファイルsrc/Login.spec.jsxを作成し、Jestで書かれた複雑な例と同じことを行う次のコードを追加します。

NightwatchでJSXを使用してコンポーネントをレンダリングするには、レンダリングされたコンポーネントのエクスポートを作成するだけです。オプションで、一連のプロップスを使用できます。playおよびtest関数は、コンポーネントと対話し、結果を検証するために使用されます。

  • playはコンポーネントと対話するために使用されます。ブラウザコンテキストで実行されるため、Testing Libraryのscreenオブジェクトを使用してDOMをクエリし、イベントを発生させることができます。
  • testは結果を検証するために使用されます。Node.jsコンテキストで実行されるため、Nightwatchのbrowserオブジェクトを使用してDOMをクエリし、結果を検証できます。
// login.spec.jsx
import {render, fireEvent, screen} from '@testing-library/react'
import Login from '../src/login'

export default {
  title: 'Login',
  component: Login
}

export const LoginWithValidCredentials = () => <Login />;
LoginWithValidCredentials.play = async ({canvasElement}) => {
  //fill out the form
};

LoginWithValidCredentials.test = async (browser) => {
  // verify the results
};

モックサーバーを追加する

この例では、ログインリクエストをシミュレートするためにモックサーバーを使用します。@nightwatch/apitestingプラグインに付属の統合モックサーバーを使用します。

このために、テストファイルに直接記述できるsetupおよびteardownフックを使用します。両方のフックはNode.jsコンテキストで実行されます。

また、ログインエンドポイントをLoginコンポーネントのhttp://localhost:3000/api/loginに設定する必要があります。これはモックサーバーへのURLです。

完全なテストファイル

完全なテストファイルは次のようになります。

// login.spec.jsx
import {render, fireEvent, screen} from '@testing-library/react'
import Login from '../src/Login'

let server;
const token = 'fake_user_token';
let serverResponse = {
  status: 200,
  body: {token}
};

export default {
  title: 'Login',
  component: Login,
  setup: async ({mockserver}) => {
    server = await mockserver.create();
    server.setup((app) => {
      app.post('/api/login', function (req, res) {
        res.status(serverResponse.status).json(serverResponse.body);
      });
    });

    await server.start(mockServerPort);
  },

  teardown: async (browser) => {
    await browser.execute(function() {
      window.localStorage.removeItem('token')  
    });
    
    await server.close();
  }
}

export const LoginWithValidCredentials = () => <Login />;
LoginWithValidCredentials.play = async ({canvasElement}) => {
  //fill out the form
  fireEvent.change(screen.getByLabelText(/username/i), {
    target: {value: 'chuck'},
  });

  fireEvent.change(screen.getByLabelText(/password/i), {
    target: {value: 'norris'},
  });

  fireEvent.click(screen.getByText(/submit/i))
};

LoginWithValidCredentials.test = async (browser) => {
  const alert = await browser.getByRole('alert')
  await expect(alert).text.to.match(/congrats/i)

  const localStorage = await browser.execute(function() {
    return window.localStorage.getItem('token');
  });

  await expect(localStorage).to.equal(fakeUserResponse.token)
};

デバッグ

エンドツーエンドテストに利用できる同じAPIを持つことに加えて、Nightwatchをコンポーネントテストに使用する主な利点の1つは、JSDOMなどの仮想DOM環境ではなく、実際のブラウザでテストを実行できることです。

これにより、Chrome Dev Toolsを使用してテストをデバッグできます。

たとえば、LoginWithValidCredentials.play関数にdebuggerステートメントを追加してみましょう。

LoginWithValidCredentials.play = async ({canvasElement}) => {
  //fill out the form
  fireEvent.change(screen.getByLabelText(/username/i), {
    target: {value: 'chuck'},
  });

  fireEvent.change(screen.getByLabelText(/password/i), {
    target: {value: 'norris'},
  });
  
  debugger;
  
  fireEvent.click(screen.getByText(/submit/i))
};

次に、--debugおよび--devtoolsフラグを使用してテストを実行しましょう。

npx nightwatch test/login.spec.jsx --debug --devtools

これにより、Dev Toolsが開いた新しいChromeウィンドウが開きます。Dev Toolsにブレークポイントを設定して、コードをステップ実行できます。

Debugging

3.2 サーバー例外でのログインテスト

Testing Libraryドキュメントの元のには、サーバーが例外をスローした場合のテストも含まれています。

Nightwatchで同じものを記述してみましょう。今回はtest関数のみを使用します。これは、この方法でもコンポーネントと対話できるためです。前述したように、test関数はNode.jsコンテキストで実行され、Nightwatchのbrowserオブジェクトを引数として受け取ります。

また、モックサーバーの応答を更新して、500ステータスコードとエラーメッセージを返す必要があります。これは、LoginWithServerExceptionコンポーネントストーリーにpreRenderテストフックを記述することで簡単に実現できます。

export const LoginWithServerException = () => <Login />;
LoginWithServerException.preRender = async (browser) => {
  serverResponse = {
    status: 500,
    body: {message: 'Internal server error'}
  };
};

LoginWithServerException.test = async (browser) => {
  const username = await browser.getByLabelText(/username/i);
  await username.sendKeys('chuck');

  const password = await browser.getByLabelText(/password/i);
  await password.sendKeys('norris');

  const submit = await browser.getByText(/submit/i);
  await submit.click();

  const alert = await browser.getByRole('alert');
  await expect(alert).text.to.match(/internal server error/i);

  const localStorage = await browser.execute(function() {
    return window.localStorage.getItem('token');
  });

  await expect(localStorage).to.equal(token)
};

4. テストを実行する

最後に、テストを実行しましょう。これにより、ChromeでLoginWithValidCredentialsおよびLoginWithServerExceptionコンポーネントストーリーが実行されます。

npx nightwatch test/login.spec.jsx

ブラウザを開かずにテストを実行するには、--headlessフラグを渡すことができます。

すべてうまくいけば、次の出力が表示されます。

[Login] Test Suite
────────────────────────────────────
ℹ Connected to ChromeDriver on port 9515 (1134ms).
  Using: chrome (108.0.5359.124) on MAC OS X.

Mock server listening on port 3000

  Running <LoginWithValidCredentials> component:
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
[browser] [vite] connecting...
[browser] [vite] connected.
  ✔ Expected element <LoginWithValidCredentials> to be visible (15ms)
  ✔ Expected element <DIV[id='app'] > DIV > DIV> text to match: "/congrats/i" (14ms)
  ✔ Expected 'fake_user_token'  to equal('fake_user_token'): 

  ✨ PASSED. 3 assertions. (1.495s)

  Running <LoginWithServerException> component:
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
[browser] [vite] connecting...
[browser] [vite] connected.
  ✔ Expected element <LoginWithServerException> to be visible (8ms)
  ✔ Expected element <DIV[id='app'] > DIV > DIV> text to match: "/internal server error/i" (8ms)
  ✔ Expected 'fake_user_token'  to equal('fake_user_token'): 

  ✨ PASSED. 3 assertions. (1.267s)

  ✨ PASSED. 6 total assertions (4.673s)

5. 結論

以上です!この例の完全なコードは、GitHubリポジトリにあります。PRを歓迎します。

ご質問やフィードバックがある場合は、Nightwatch Discordまでお気軽にお越しください。