Preact Testing Libraryを使ったテスト
Preact Testing Library は、preact/test-utils
の軽量なラッパーです。これは、ユーザーがページ上で要素を見つけるのと同様の方法で、レンダリングされたDOMにアクセスするための一連のクエリメソッドを提供します。このアプローチにより、実装の詳細に依存しないテストを作成できます。その結果、テスト対象のコンポーネントがリファクタリングされた場合、テストの保守が容易になり、より堅牢になります。
Enzymeとは異なり、Preact Testing LibraryはDOM環境内で呼び出す必要があります。
インストール
次のコマンドでtesting-library Preactアダプターをインストールします
npm install --save-dev @testing-library/preact
注: このライブラリは、DOM環境が存在することを前提としています。Jest を使用している場合、これは既に含まれており、デフォルトで有効になっています。Mocha や Jasmine などの別のテストランナーを使用している場合は、jsdom をインストールすることで、ノードにDOM環境を追加できます。
使い方
初期値と、それを更新するためのボタンを表示する Counter
コンポーネントがあるとします
import { h } from 'preact';
import { useState } from 'preact/hooks';
export function Counter({ initialCount }) {
const [count, setCount] = useState(initialCount);
const increment = () => setCount(count + 1);
return (
<div>
Current value: {count}
<button onClick={increment}>Increment</button>
</div>
);
}
Counterが初期カウントを表示すること、そしてボタンをクリックするとカウントが増加することを確認したいと考えています。Jest や Mocha など、任意のテストランナーを使用して、これら2つのシナリオを記述できます
import { expect } from 'expect';
import { h } from 'preact';
import { render, fireEvent, screen, waitFor } from '@testing-library/preact';
import Counter from '../src/Counter';
describe('Counter', () => {
test('should display initial count', () => {
const { container } = render(<Counter initialCount={5}/>);
expect(container.textContent).toMatch('Current value: 5');
});
test('should increment after "Increment" button is clicked', async () => {
render(<Counter initialCount={5}/>);
fireEvent.click(screen.getByText('Increment'));
await waitFor(() => {
// .toBeInTheDocument() is an assertion that comes from jest-dom.
// Otherwise you could use .toBeDefined().
expect(screen.getByText("Current value: 6")).toBeInTheDocument();
});
});
});
そこで waitFor()
の呼び出しに気付いたかもしれません。これは、PreactがDOMにレンダリングし、すべての保留中のエフェクトをフラッシュするのに十分な時間があることを保証するために必要です。
test('should increment counter", async () => {
render(<Counter initialCount={5}/>);
fireEvent.click(screen.getByText('Increment'));
// WRONG: Preact likely won't have finished rendering here
expect(screen.getByText("Current value: 6")).toBeInTheDocument();
});
内部的には、waitFor
は、渡されたコールバック関数がエラーをスローしなくなるか、タイムアウトになるまで(デフォルト:1000ms)、繰り返しコールバック関数を呼び出します。上記の例では、カウンターが増加し、新しい値がDOMにレンダリングされたときに、更新が完了したことがわかります。
"getBy" の代わりに "findBy" バージョンのクエリを使用することで、非同期優先のテストを書くこともできます。非同期クエリは内部で waitFor
を使用して再試行し、Promiseを返すため、それらを待つ必要があります.
test('should increment counter", async () => {
render(<Counter initialCount={5}/>);
fireEvent.click(screen.getByText('Increment'));
await screen.findByText('Current value: 6'); // waits for changed element
expect(screen.getByText("Current value: 6")).toBeInTheDocument(); // passes
});
要素の検索
完全なDOM環境が整っていれば、DOMノードを直接検証できます。一般的に、テストでは、入力値などの属性が存在すること、または要素が表示/非表示になっていることを確認します。これを行うには、DOM内の要素を見つけることができる必要があります。
コンテンツの使用
Testing Libraryの哲学は、「テストがソフトウェアの使用方法に似ているほど、より多くの自信を得ることができる」ということです。
ページを操作する推奨される方法は、ユーザーが行うのと同じように、テキストコンテンツを通じて要素を見つけることです。
適切なクエリを選択するためのガイドは、Testing Libraryドキュメントの'どのクエリを使用すればよいですか'ページにあります。最も簡単なクエリは getByText
で、要素の textContent
を調べます。ラベルテキスト、プレースホルダー、title属性などのクエリもあります。 getByRole
クエリは、DOMを抽象化し、アクセシビリティツリー内の要素を見つけることができるため、最も強力です。これは、スクリーンリーダーによってページがどのように読み取られるかを示しています。role
と accessible name
を組み合わせることで、1つのクエリで多くの一般的なDOMトラバーサルをカバーできます.
import { render, fireEvent, screen } from '@testing-library/preact';
test('should be able to sign in', async () => {
render(<MyLoginForm />);
// Locate the input using textbox role and the accessible name,
// which is stable no matter if you use a label element, aria-label, or
// aria-labelledby relationship
const field = await screen.findByRole('textbox', { name: 'Sign In' });
// type in the field
fireEvent.change(field, { value: 'user123' });
})
テキストコンテンツを直接使用すると、コンテンツが頻繁に変更される場合や、テキストを異なる言語に翻訳する国際化フレームワークを使用する場合に、摩擦が生じることがあります。これを回避するには、テキストをスナップショットするデータとして扱うことで、更新を容易にしながら、真実のソースをテストの外部に保つことができます。
test('should be able to sign in', async () => {
render(<MyLoginForm />);
// What if we render the app in another language, or change the text? Test fails.
const field = await screen.findByRole('textbox', { name: 'Sign In' });
fireEvent.change(field, { value: 'user123' });
})
翻訳フレームワークを使用していなくても、文字列を別のファイルに保存し、以下の例と同じ戦略を使用できます
test('should be able to sign in', async () => {
render(<MyLoginForm />);
// We can use our translation function directly in the test
const label = translate('signinpage.label', 'en-US');
// Snapshot the result so we know what's going on
expect(label).toMatchInlineSnapshot(`Sign In`);
const field = await screen.findByRole('textbox', { name: label });
fireEvent.change(field, { value: 'user123' });
})
テストIDの使用
テストIDは、コンテンツの選択があいまいまたは予測不可能な場合、またはDOM構造などの実装の詳細から分離するために、DOM要素に追加されるデータ属性です。他の要素検索方法が意味をなさない場合に使用できます。
function Foo({ onClick }) {
return (
<button onClick={onClick} data-testid="foo">
click here
</button>
);
}
// Only works if the text stays the same
fireEvent.click(screen.getByText('click here'));
// Works if we change the text
fireEvent.click(screen.getByTestId('foo'));
テストのデバッグ
現在のDOM状態をデバッグするには、debug()
関数を使用して、DOMの整形されたバージョンを出力できます。
const { debug } = render(<App />);
// Prints out a prettified version of the DOM
debug();
カスタムコンテキストプロバイダーの提供
多くの場合、共有コンテキスト状態に依存するコンポーネントが作成されます。一般的なプロバイダーは、通常、ルーター、状態、テーマ、特定のアプリにグローバルなものまで多岐にわたります。これは、各テストケースを繰り返し設定するのが面倒になる可能性があるため、@testing-library/preact
のものをラップしてカスタム render
関数を作成することをお勧めします.
// helpers.js
import { render as originalRender } from '@testing-library/preact';
import { createMemoryHistory } from 'history';
import { FooContext } from './foo';
const history = createMemoryHistory();
export function render(vnode) {
return originalRender(
<FooContext.Provider value="foo">
<Router history={history}>
{vnode}
</Router>
</FooContext.Provider>
);
}
// Usage like usual. Look ma, no providers!
render(<MyComponent />)
Preactフックのテスト
@testing-library/preact
を使用すると、フックの実装もテストできます。カウンター機能を複数のコンポーネントで再利用したい(カウンターが大好きです!)と考えて、それをフックに抽出したとします。そして、それをテストしたいと考えています。
import { useState, useCallback } from 'preact/hooks';
const useCounter = () => {
const [count, setCount] = useState(0);
const increment = useCallback(() => setCount(c => c + 1), []);
return { count, increment };
}
以前と同様に、その背後にあるアプローチは似ています。カウンターを増やすことができることを確認したいと考えています。そのため、何とかフックを呼び出す必要があります。これは、内部で周囲のコンポーネントを自動的に作成する renderHook()
関数を使用して行うことができます。この関数は、現在のフックの戻り値を result.current
の下に返します。これを使用して検証を行うことができます
import { renderHook, act } from '@testing-library/preact';
import useCounter from './useCounter';
test('should increment counter', () => {
const { result } = renderHook(() => useCounter());
// Initially the counter should be 0
expect(result.current.count).toBe(0);
// Let's update the counter by calling a hook callback
act(() => {
result.current.increment();
});
// Check that the hook return value reflects the new state.
expect(result.current.count).toBe(1);
});
@testing-library/preact
の詳細については、https://github.com/testing-library/preact-testing-library を参照してください。