Netflixのエンジニア向けブログで、同社のエンジニアによる「SafeTest: フロントエンドテストの新しいアプローチ」が公開された。この記事では、新しいライブラリSafeTestがWebベースのUIアプリケーションのエンドツーエンド(E2E)テストを大きく変革する可能性について、具体的なコード例とともに紹介されている。
従来のUIテストの課題
従来、UIテストはユニットテストまたは統合テスト(エンドツーエンドテストとも呼ばれる)を使用して実施されてきた。ただし、それぞれの手法には一定のトレードオフが存在する。
たとえば、ユニットテストのソリューションであるreact-testing-libraryを使用する場合、アプリケーションコード全体の動作を完全に制御できるが、実際に描画されるページとの対話が限られてしまい、特に複雑なUI要素(例:<Dropdown />コンポーネント)を、ユーザーによるインタラクションとしてテストするのは難しい。
逆にCypressやPlaywrightのような統合テストツールを使用すると、実際にユーザー向けに描画されたページを制御することができ、対話性に優れたテストを実行できる。しかし、アプリケーションの起動をフックすることができず、アプリケーション全体のコードを自在に制御することが難しいため、テストできる範囲が限られたり、実行環境に大きく左右されるなどの課題も発生する。
SafeTestの概要
SafeTestはこれらの課題に対処するために開発された、UIテストへの新しいアプローチである。その主なアイディアは、 アプリケーションの起動時にテスト用のフックをインジェクトする ことだ。SafeTestはテストを実行する際にのみテストを動的にロードするため、通常のアプリの使用にほとんど影響を与えることはない。
このアプローチにより、様々なエキサイティングな機能も提供される。例えば、特定のテストへのディープリンク、ブラウザとテスト(ノード)コンテキスト間の双方向通信、Playwrightで提供されるDX機能へのアクセスなどだ。
SafeTestでのテスト例
SafeTestを実際に使用するサンプルコードは以下のようなものだ。
import { describe, it, expect } from 'safetest/jest';
import { render } from 'safetest/react';
describe('my app', () => {
it('loads the main page', async () => {
// ページがレンダリングされるまで待つ
const { page } = await render();
// ページ内に特定の文字列が表示されることをテストする
await expect(page.getByText('Welcome to the app')).toBeVisible();
// ページのスクリーンショットを撮り、一致を確認する
expect(await page.screenshot()).toMatchImageSnapshot();
});
});
特定のコンポーネントをテストするのも同様に簡単だ。
import { describe, it, expect, browserMock } from 'safetest/jest';
import { render } from 'safetest/react';
describe('Header component', () => {
it('has a normal mode', async () => {
// <Header>コンポーネントのレンダリングを待ち、テストする
const { page } = await render(<Header />);
await expect(page.getByText('Admin')).not.toBeVisible();
});
it('has an admin mode', async () => {
const { page } = await render(<Header admin={true} />);
await expect(page.getByText('Admin')).toBeVisible();
});
it('calls the logout handler when signing out', async () => {
const spy = browserMock.fn();
const { page } = await render(<Header handleLogout={fn} />);
await page.getByText('logout').click();
expect(await spy).toHaveBeenCalledWith();
});
});
Overridesの活用
SafeTestはReact Contextを使用して、テスト中に値のオーバーライドを可能にする。以下は、fetchPersonというAPIの戻り値をオーバーライドして、結果をテストするコードだ。
import { useAsync } from 'react-use';
import { fetchPerson } from './api/person';
export const People: React.FC = () => {
const { data: people, loading, error } = useAsync(fetchPerson);
if (loading) return <Loader />;
if (error) return <ErrorPage error={error} />;
return <Table data={data} rows={[...]} />;
}
これをオーバーライドするためには:
import { fetchPerson } from './api/person';
import { createOverride } from 'safetest/react';
const FetchPerson = createOverride(fetchPerson);
export const People: React.FC = () => {
const fetchPeople = FetchPerson.useValue();
const { data: people, loading, error } = useAsync(fetchPeople);
if (loading) return <Loader />;
if (error) return <ErrorPage error={error} />;
return <Table data={data} rows={[...]} />;
}
そして、テストではこの呼び出しのレスポンスをオーバーライドできる:
const pending = new Promise(r => { /* Do nothing */ });
const resolved = [{ name: 'Foo', age: 23 }, { name: 'Bar', age: 32 }];
const error = new Error('Whoops');
describe('People', () => {
it('has a loading state', async () => {
const { page } = await render(
<FetchPerson.Override with={() => () => pending}>
<People />
</FetchPerson.Override>
);
await expect(page.getByText('Loading')).toBeVisible();
});
it('has a loaded state', async () => {
const { page } = await render(
<FetchPerson.Override with={() => async () => resolved}>
<People />
</FetchPerson.Override>
);
await expect(page.getByText('User: Foo, name: 23')).toBeVisible();
});
it('has an error state', async () => {
const { page } = await render(
<FetchPerson.Override with={() => async () => { throw error }}>
<People />
</FetchPerson.Override>
);
await expect(page.getByText('Error getting users: "Whoops"')).toBeVisible();
});
});
これでテスト時に特定のコンポーネントの振る舞いを制御できる。他にもSafeTestが提供する機能や実例についてはOverrides sectionを参照して欲しい。
レポーティング
SafeTestは強力な報告機能を提供し、ビデオリプレイやPlaywrightトレースビューアなどに自動的にリンクされるだけでなく、テスト対象のコンポーネントへのディープリンクもサポートしている。
企業環境でのSafeTest
SafeTestの柔軟なオーバーライド機能を利用し、認証やテストユーザーをセットアップすることも可能だ。実際にNetflixではこの機能を使用してテストユーザーを生成し、認証情報をインジェクトしてアプリケーションをテストしている。
import { setup } from 'safetest/setup';
import { createTestUser, addCookies } from 'netflix-test-helper';
type Setup = Parameters<typeof setup>[0] & {
extraUserOptions?: UserOptions;
};
export const setupNetflix = (options: Setup) => {
setup({
...options,
hooks: { beforeNavigate: [async page => addCookies(page)] },
});
beforeAll(async () => {
createTestUser(options.extraUserOptions);
});
};
React以外での利用もサポート
この記事はReactに焦点を当てているが、SafeTestはReactに限定されていない。Vue、Svelte、Angularなどさまざまなフレームワークで動作し、JestまたはVitestに基づくテストランナーを使用している。examples folderには、さまざまなツールとの組み合わせでSafeTestを使用する方法の例があり、新しいケースの貢献が歓迎されている。
まとめ
SafeTestは既にNetflix内で採用されている強力なテストフレームワークであり、テストの容易な作成と包括的なレポート提供により、UIテストを大幅に改善するものだ。
詳細はIntroducing SafeTest: A Novel Approach to Front End Testingを参照してください。