Nightwatch 中的組件測試已在 2.4 版本中進行了優化,並且對測試 React 組件的支援(通過 @nightwatch/react 外掛程式)得到了顯著改進。我們還發布了一個新的外掛程式,用於將流行的 Testing Library 與 Nightwatch 一起使用 – @nightwatch/testing-library,自 Nightwatch v2.6 起可用。

現在,我們將建立一個詳細的範例,說明如何使用 Nightwatch 和 Testing Library 來測試 React 組件。我們將使用 複雜範例,該範例可在 React Testing Library 文件中找到,並使用 Jest 編寫。

本教學將涵蓋如何

  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 的基本原則之一是,測試應盡可能地模擬使用者與應用程式互動的方式。當在 Nightwatch 中使用 JSX 編寫組件測試時,我們需要使用 Component Story Format(由 Storybook 引入的宣告式格式)將測試編寫為組件故事。

這使我們能夠編寫專注於組件使用方式而非實作方式的測試,這符合 Testing Library 的理念。您可以在 Nightwatch 文件中閱讀更多相關資訊。

使用此格式編寫測試的好處是,我們可以重複使用相同的程式碼來編寫組件的故事,這些故事可用於記錄和展示 Storybook 中的組件。

3.1 使用有效憑證登入測試

建立一個新檔案 src/Login.spec.jsx 並新增以下程式碼,其功能與使用 Jest 編寫的 複雜範例 相同

若要在 Nightwatch 中使用 JSX 呈現組件,我們只需為呈現的組件建立一個匯出,並可選擇性地設定一組 props。playtest 函數用於與組件互動並驗證結果。

  • 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 外掛程式隨附的整合式模擬伺服器。

為此,我們將使用 setupteardown hooks,我們可以將它們直接編寫在測試檔案中。這兩個 hooks 都在 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 進行組件測試的主要好處之一是,我們可以在真實瀏覽器中執行測試,而不是在虛擬 DOM 環境(例如 JSDOM)中執行測試。

這使我們能夠使用 Chrome 開發人員工具來偵錯測試。

例如,我們可以在 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

這將開啟一個新的 Chrome 視窗,其中開啟了開發人員工具。我們現在可以在開發人員工具中設定一個中斷點,並逐步執行程式碼。

Debugging

3.2 使用伺服器例外狀況登入測試

Testing Library 文件中的原始 範例 也包含在伺服器擲回例外狀況時的測試案例。

讓我們嘗試在 Nightwatch 中編寫相同的程式碼。這次我們只會使用 test 函數,因為我們也可以使用這種方式與組件互動。正如我們前面提到的,test 函數會在 Node.js 內容中執行,並且它會接收 Nightwatch browser 物件作為引數。

我們還需要更新模擬伺服器回應,以傳回 500 狀態碼和錯誤訊息。我們可以通過在 LoginWithServerException 組件故事中編寫 preRender 測試 hook 來輕鬆達成此目的。

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 中執行 LoginWithValidCredentialsLoginWithServerException 組件故事。

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