2021年9月13日
フロントエンドのテストは皆のためのもの
(2021-8-2)by Evgeny Klimenchenko
本記事は、原著者の許諾のもとに翻訳・掲載しております。
テストとは人によって反応が分かれるものの1つであり、大喜びする人もいれば、見ないようにして去ろうとする人もいます。あなたがどちらの側であるにせよ、ここではフロントエンドのテストは皆のためのものであるということを説明します。実際、テストには多くの種類があり、それがテストに対して初めに恐れや混乱を感じる一因なのかもしれません。
この記事では、特に有名で広く利用されている種類のテストを扱います。なかには目新しいものはないと感じる読者の方もいらっしゃるかもしれませんが、少なくとも復習にはなるでしょう。どちらにせよ、筆者の目標は、この記事を通じて世の中のさまざまな種類のテストについて理解を深めてもらうことです。ここではユニットテスト、統合テスト、アクセシビリティテスト、ビジュアルリグレッションテストなどを一緒に見ていきます。
さらに、Mocha、Jest、Puppeteer、Cypressなど、各種類のテストに使用されているライブラリとフレームワークも紹介します。専門用語はあまり使わないようにしますので、ご心配は無用です。ただし、これから扱う事例を理解するには、ある程度フロントエンドの開発経験があったほうがいいでしょう。
それでは始めましょう。
目次
- テストとは?
- プロジェクトのどの部分をチェックするかはテストごとに異なる
- ユニットテスト
- 統合テスト
- エンドツーエンド(E2E)テスト
- アクセシビリティテスト
- ビジュアルリグレッションテスト
- パフォーマンステスト
- まとめ
テストとは?
「ソフトウェアのテストとは、テスト対象のソフトウェア製品またはサービスの品質に関する情報を利害関係者に提供するために行われる調査である」 -Cem Kaner, “Exploratory Testing”(2006年11月17日)
テストの最も基本的な定義は、開発の可能な限り早い段階でエラーを発見するための自動化されたツールです。テストによって、本稼働前に問題を修正できます。またテストには、アクセシビリティなど、特定の観点からのチェック漏れを思い出させてくれる役割もあります。
要するに、フロントエンドのテストは、こちらの意図通りにユーザーがサイトを閲覧し、サイト上で機能を使用できるかを検証するものです。
フロントエンドのテストはアプリケーションのクライアントサイドのために存在します。例えば、フロントエンドのテストでは、「削除」ボタンを押すことで画面からアイテムが適切に削除されるかを確認できます。 一方、データベースから実際に削除されたかの確認は、別途バックエンドのテストでカバーします。 一言で言えば、フロントエンドのテストとはクライアントサイドのエラーを発見し、コードのデプロイ前に修正することです。
プロジェクトのどの部分をチェックするかはテストごとに異なる
テストの種類ごとに、プロジェクトのどの側面をカバー出来るのかは異なります。ですので、各種類の役割を理解することは重要です。種類ごとの目的を混同すると、テストスイートが乱雑で信頼できないものとなってしまいます。
理想的には、さまざまなタイプの潜在的な問題を明らかにするため、複数の異なる種類のテストを実施するのが良いでしょう。中にはカバレッジの分析機能がついたテストもあり、コードのどの程度の割合(%)をチェックできるかを明示できます。これは素晴らしい機能であり、カバレッジを100%にしようとする開発者もいますが、筆者はこの指標のみには頼りません。最も重要なのは、あり得るすべてのエッジケースをカバーし、考慮することです。
このことを念頭に置いて、さまざまな種類のテストに注目していきましょう。ちなみに、これらのテストを全て実施する必要はありません。大事なのは、テストを区別することができ、それによって特定の状況でどのテストを使用すべきかを理解することです。
ユニットテスト
ユニットテストは、様々なテスト方法がある中での最も基本的な構成要素です。このテストでは個々のコンポーネントをチェックし、意図したとおりに機能するかを確認します。この種のテストはあらゆるフロントエンドのアプリケーションに必要不可欠です。なぜなら、コードベースとアプリの信頼性の大幅な向上につながるためです。ユニットテストではエッジケースなどを検討し、カバーすることもできます。
ユニットテストはAPIのテストにおいて特に有効です。本番APIを呼び出すのではなく、ハードコードされた(「モック」)データを使用することで、常に一貫したテストを確実に実行することができます。
例えば、以下の非常にシンプルな(そして原始的な)関数を見てみましょう。
const sayHello = (name) => {
if (!name) {
return "Hello human!";
}
return `Hello ${name}!`;
};
繰り返しますが、これは基本的なケースです。それでも、ユーザーがアプリケーションに名前を入力しなかった場合という小規模なエッジケースをカバーできていることが分かります。nameが入力されていれば、“Hello ${name}!
”が表示されます。${name}
はユーザーが入力すると想定される部分です。
「なぜそんな些細なことをテストする必要があるの?」と思われるかもしれません。これには、以下のとても重要な理由があります。
- 関数が出力し得る結果について深く考えるきっかけとなります。たいていはエッジケースを実際に発見し、コードでそのケースをカバーするのに役立ちます。
- コードの一部がこのエッジケースに依存する場合があります。誰かが重要な部分を削除したときに、テストプログラムが、このコードは重要なので削除してはならないという警告を発します。
ユニットテストは小規模でシンプルなものが多いです。以下は例その例です。
describe("sayHello function", () => {
it("should return the proper greeting when a user doesn't pass a name", () => {
expect(sayHello()).toEqual("Hello human!")
})
it("should return the proper greeting with the name passed", () => {
expect(sayHello("Evgeny")).toEqual("Hello Evgeny!")
})
})
describeとitはただのシンタックスシュガーで、最も重要な行はexpectとtoEqualです。describeとitはテストを論理的なブロックに分け、ターミナルに出力します。expect関数は検証対象の入力に、toEqualは望ましい出力に対応します。アプリケーションのテストに使える関数とメソッドは多種多様です。
ユニットのプログラム開発用ライブラリであるJestを使用してみましょう。上記の例では、JestはsayHello関数をタイトルとしてターミナルで表示します。it関数内のすべては単独のテストとみなされ、ターミナルで関数のタイトルの下に報告されます。すべてが非常に読みやすく表示されています。
緑のチェックマークは両方のテストに通過したことを意味します。やった!
結合テスト
- レベル:中
- 範囲:ユニット間のインタラクションをテストする
- ツール候補:AVA、Jest、Testing Library
ユニットテストが単独の機能の動作をテストするものだとすれば、結合テストは複数の要素が問題なく連動するかを確かめるものです。結合テストが非常に重要なのは、コンポーネント間の相互作用をテストできるからです。アプリケーションが、単独で機能する独立した部分のみで構成されることは(あったとしても)非常に稀です。ですから、結合テストに頼る必要があります。
ユニットテストを実施した関数をもう一度使用しましょう。今回はシンプルなReactアプリケーションの中で使用します。例えば、ボタンをクリックすると画面にあいさつが表示されるとします。これは、関数だけではなく、HTML DOMやボタンの機能もテストに関係することを意味します。これらすべてが一体となってどのように機能するかをテストしましょう。
以下はテスト対象の<Greeting />
コンポーネントのコードです。
export const Greeting = () => {
const [showGreeting, setShowGreeting] = useState(false);
return (
<div>
<p data-testid="greeting">{showGreeting && sayHello()}</p>
<button data-testid="show-greeting-button" onClick={() => setShowGreeting(true)}>Show Greeting</button>
</div>
);
};
以下は結合テストの例です。
describe('<Greeting />', () => {
it('shows correct greeting', () => {
const screen = render(<Greeting />);
const greeting = screen.getByTestId('greeting');
const button = screen.getByTestId('show-greeting-button');
expect(greeting.textContent).toBe('');
fireEvent.click(button);
expect(greeting.textContent).toBe('Hello human!');
});
});
describeとitはユニットテストで確認したとおりです。これらはテストをロジックごとに分割します。特別にエミュレートしたDOMに<Greeting />
コンポーネントを表示するrender関数を設定しており、実際のDOMに触れることなくコンポーネントとのインタラクションをテストできます。そうしなければコストがかかる可能性があります。
次に、テストプログラムはテストID(#greetingと#show-greeting-button)を通じてそれぞれ<p>
と<button>
の要素を取得しています。テストIDを使用するのは、エミュレートしたDOMから必要なコンポーネントを取得するのが比較的容易であるためです。他にもコンポーネントを取得する方法はありますが、筆者はこの方法を最もよく使っています。
7行目からやっと実際の結合テストが始まります。まずは<p>
タグが空であることを確認し、次にclickイベントをシミュレーションすることでボタンをクリックします。最後に、<p>
タグの中身が“Hello human!”であることを確認します。これで終了です。テストするのは、ボタンをクリックした後に、テキストが空のパラグラフに格納されるということだけです。これでコンポーネントはカバーされました。
もちろん、名前の入力欄にインプットを追加して、その入力内容をgreeting関数で使用することもできます。しかし、ここではもう少しシンプルな内容にしました。インプットの使用については、他の種類のテストをカバーするときに説明します。
以下は、結合テストを実行したときのターミナルの表示です。
完璧!<Greeting />
コンポーネントは、ボタンをクリックしたときに正しいあいさつを返しています。
エンドツーエンド(E2E)テスト
- レベル:高
- 範囲:何をすべきか、およびどのような結果が期待されているかに関して、プログラムに指示を出し、実際のブラウザでのユーザーとの間のインタラクションをテストする
- ツール候補:Cypress、Puppeteer
E2Eテストはこのリストの中で最高レベルのテストです。このテストは、ユーザーにとってアプリケーションがどう見えるか、ユーザーがどのようにアプリケーションとインタラクトするかという点のみを扱います。コードや実装とは一切関係ありません。
E2Eテストではブラウザに対し、何をすべきか、何をクリックすべきか、そして何を入力すべきかについて指示を出します。あらゆる種類のインタラクションを作成し、エンドユーザーが体験するとおりにさまざまな機能やフローをテストできます。このテストは、アプリケーションをくまなくクリックしてインタラクションを行い、すべてが機能していることを確かめるという、まさにロボットとしての役割を果たすものです。
E2Eテストは結合テストとやや似ています。しかし、E2Eテストは、モックアップではなく実際のDOMを有する実際のブラウザで実行されるものです。これらのテストでは通常、実際のデータと実際のAPIを使用します。
ユニットテストと結合テストでカバレッジ100%を目指しても良いでしょう。しかし、実際にブラウザでアプリケーションを実行したとき、ユーザーは予想外の挙動に直面する可能性があります。E2Eテストは、それに対する完璧な解決策となるのです。
以下では、極めて有名なテスト用ライブラリのCypressを使用した例を見てみましょう。前述したコンポーネントのE2EテストのためだけにCypressを使用します。ただし、今回はブラウザ内で実行し、機能をいくつか追加しています。
繰り返しになりますが、アプリケーションのコードを見る必要はありません。ここでの想定は、何らかのアプリケーションがあり、それをユーザーとしてテストしたいということだけです。私たちはどのボタンをクリックすべきか、それらのボタンのIDが何であるかを知っています。それだけでテストを始めるには十分です。
describe('Greetings functionality', () => {
it('should navigate to greetings page and confirm it works', () => {
cy.visit('http://localhost:3000')
cy.get('#greeting-nav-button').click()
cy.get('#greetings-input').type('Evgeny', { delay: 400 })
cy.get('#greetings-show-button').click()
cy.get('#greeting-text').should('include.text', 'Hello Evgeny!')
})
})
このE2Eテストは前述の結合テストとそっくりで、コマンドが非常によく似ています。主な違いは実際のブラウザで実行されることです。
まずcy.visitを使用して、アプリケーションが存在する特定のURLへ移動します。
cy.visit('http://localhost:3000')
次にcy.getを使用してIDからナビゲーションボタンを取得し、テストプログラムに対してボタンをクリックするよう指示します。このアクションによって
cy.get('#greeting-nav-button').click()
さらに、続けてinputタグに“Evgeny”と入力し、#greetings-show-buttonボタンをクリックし、最後に望ましいあいさつが出力されていることを確認します。
cy.get('#greetings-input').type('Evgeny', { delay: 400 })
cy.get('#greetings-show-button').click()
cy.get('#greeting-text').should('include.text', 'Hello Evgeny!')
実際のブラウザで、テストプログラムによってボタンがクリックされるのを見るのは非常に壮観です。以下は、何が起きているか分かるように、テストのスピードを遅くしたものです。通常、テストはすべて非常に速いスピードで実行されます。
以下はターミナルの出力です。
アクセシビリティテスト
- レベル:高
- 範囲:アクセシビリティ標準の基準に従い、アプリケーションのインターフェイスをテストする
- ツール候補:AccessLint、axe-core、Lighthouse、pa11y
Webのアクセシビリティとは、Webサイト、ツール、テクノロジーが、障害のある人でも利用できるように設計・開発されていることを意味する -W3C
アクセシビリティテストは、障害のある人がWebサイトへアクセスし、効果的に使用できるようにするためのものです。これらのテストでは、アクセシビリティを考慮したWebサイトを実装するための基準に従っているかを検証します。
例えば、目が見えない人の多くはスクリーンリーダーを使用します。スクリーンリーダーはWebサイトをスキャンし、障害のあるユーザーでも理解可能な形式(通常は音声)で情報提供することを目指すものです。開発者としては、スクリーンリーダーの負担を軽くしたいと思うでしょう。そんなとき、アクセシビリティテストは、どこから着手すればよいかを把握する助けとなります。
アクセシビリティテストのツールは多種多様です。自動化されているものもあれば、手動で検証するものもあります。例えば、ChromeはすでにDevToolsにLighthouseというツールを搭載しています。名前をご存じの方もいるでしょう。
ここではLighthouseを使用して、E2Eテストのセクションで作成したアプリケーションを検証してみます。Chrome DevToolsでLighthouseを開き、「Accessibility」テストオプションをクリックし、レポートを「Generate」します。
やるべきことは、たったのこれだけです。あとはLighthouseがテストを実行し、素晴らしいレポートを作成します。レポートには、スコア、実行したテストのサマリー、スコアの改善余地の概要が記載されます。
しかし、これは特定の観点からアクセシビリティを測定するためのツールの1つにすぎません。アクセシビリティテストにはあらゆる種類のツールが存在します。ですから、何をテストするのか、そのためにどのようなツールを用意するのかについて計画を立てるのは価値があることです。
ビジュアルリグレッションテスト
- レベル:高
- 範囲:コードの変更によって生じる視覚的な差異など、アプリケーションの視覚的な構造をテストする
- ツール候補:Cypress、Percy、Applitools
E2Eテストは、場合によっては、アプリケーションの直近の変更によってインターフェイスの見た目が崩れていないかを確認するのに不十分なことがあります。一部を変更したコードを本番環境にプッシュした結果、他の部分のレイアウトが変になってしまったということはありませんか?そんな経験をしたのは、あなただけではありません。大抵の場合、コードベースを変更すると、アプリの視覚的な構造やレイアウトは崩れてしまいます。
それを解決するのがビジュアルリグレッションテストです。その仕組みは非常に単純で、ページやコンポーネントのスクリーンショットを撮り、過去の成功したテストのスクリーンショットと比較するだけです。テストでスクリーンショットに差異が見つかった場合、何らかの通知が行われます。
Percyというビジュアルリグレッションツールを使って、ビジュアルリグレッションテストの仕組みを見てみましょう。ビジュアルリグレッションテストの方法は他にも数多くありますが、筆者は動作を見るならPercyが分かりやすいと考えています。Paul Ryanが執筆したこちらのCSS-Tricksの記事では、Percyについて詳しく説明しています。 ここではコンセプトを説明するため、非常にシンプルな例を試してみます。
Greetingアプリケーションのボタンを入力欄の下に動かして、レイアウトを意図的に崩しました。このエラーをPercyで発見してみましょう。
PercyはCypressと相性が良いので、インストールガイドに従い、既存のE2Eテストと共にPercyのリグレッションテストを実行できます。
describe('Greetings functionality', () => {
it('should navigate to greetings page and confirm everything is there', () => {
cy.visit('http://localhost:3000')
cy.get('#greeting-nav-button').click()
cy.get('#greetings-input').type('Evgeny', { delay: 400 })
cy.get('#greetings-show-button').click()
cy.get('#greeting-text').should('include.text', 'Hello Evgeny!')
// Percy test
cy.percySnapshot() // HIGHLIGHT
})
})
E2Eテストの末尾に1行だけcy.percySnapshot()
を追加しました。これによってスクリーンショットを取得し、Percyに送信して比較します。たったこれだけです。テストが終わると、リグレッションを確認するためのリンクが送られてきます。以下はターミナルの出力です。
なんと、E2Eテストを問題なく通過してしまいました。このように、E2Eテストは常に視覚的なエラーを発見できるわけではないことが分かります。
以下がPercyの出力です。 明らかに何かが変わっており、修正が必要です。
パフォーマンステスト
- レベル:高
- 範囲:アプリケーションのパフォーマンスと安定性をテストする
- ツール候補:Lighthouse、PageSpeed Insights、WebPageTest、YSlow
パフォーマンステストはアプリケーションの速度を確認するのに適しています。パフォーマンスがビジネスにとって極めて重要である場合(最近はCore Web VitalsとSEOでも注目されているようです)、コードベースの変更がアプリケーションの速度に悪影響を与えていないかを確認することは必須でしょう。
パフォーマンステストは、一連のテストフローに組み込むことも、手動で実行することもできます。これらのテストをどう実行するか、どれくらいの頻度で実行するかはあなた次第です。一部の開発者はいわゆる「パフォーマンスバジェット」を作成しており、アプリの容量を計算するテストを実行しています。容量が一定の閾値を超えた場合、テストは失敗となり、デプロイが出来なくなります。あるいは、Lighthouseはパフォーマンス指標も測定できるので、折に触れて手動でテストを行うという手段もあります。2つを組み合わせて、Lighthouseをテストスイートに組み込むのもいいでしょう。
パフォーマンステストは、パフォーマンスに関連するあらゆるものを測定できます。アプリケーションの読み込み速度、最初のバンドルの容量、さらに特定の機能の速度さえ測定可能です。パフォーマンステストは、ある程度広い範囲を対象としているのです。
ここでLighthouseを使用した簡単なテストを紹介します。LighthouseはCore Web Vitalsに重点を置いており、またインストールや設定の必要なくChromeのDevToolsから容易にアクセス可能であるため、パフォーマンステストを理解するのに適していると思います。
あまり良いスコアではありませんが、少なくともどんな状況かは分かります。さらに、改善余地についていくつかのアドバイスが提示されています。
まとめ
以下はこの記事で紹介したテストの内訳です。
種類 | レベル | 範囲 | ツールの例 |
---|---|---|---|
ユニット | 低 | アプリケーションの関数とメソッドをテストする。 | AVA Jasmine Jest Karma Mocha |
結合 | 中 | ユニット間のインタラクションをテストする。 | AVA Jest Testing Library |
エンドツーエンド | 高 | 何をすべきか、およびどのような結果が期待されているかに関して、プログラムに指示を出し、実際のブラウザでのユーザーとの間のインタラクションをテストする。 | Cypress Puppeteer |
アクセシビリティ | 高 | アクセシビリティ標準の基準に従い、アプリケーションのインターフェイスをテストする。 | AccessLint axe-core Lighthouse pa11y |
ビジュアルリグレッション | 高 | コードの変更によって生じる視覚的な差異など、アプリケーションの視覚的な構造をテストする。 | Applitools Cypress Percy |
パフォーマンス | 高 | アプリケーションのパフォーマンスと安定性をテストする。 | Lighthouse PageSpeed Insights WebPageTest YSlow |
結局、テストは皆のためのものなのでしょうか?その答えはイエスです。開発のあらゆる段階で、アプリケーションのさまざまな側面をテストすることができるライブラリ、サービス、ツールは数多く存在します。ですから少なくとも、コードを基準や予想に照らして測定・テストできるようにしてくれる何かが見つかるはずです。さらに、一部のツールはコードや設定さえ必要ありません。
筆者の経験では、多くの開発者はテストを軽視しており、単純にアプリをくまなくクリックしたり、開発後チェックを実施したりすれば、コードの変更によって発生し得るバグを避けられると考えています。アプリケーションが意図した通りに動作し、可能な限り多くの人にとってインクルーシブなものとなり、効率的に実行され、また優れた設計となることを確実にしたいならば、自動か手動かを問わず、テストをワークフローの中心に据える必要があります。
テストの種類やその機能が分かったところで、皆さんの業務にもテストを導入してみてはいかがでしょうか。
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa