この記事は UIT 新春 Tech blog 2023 4日目の記事です。
こんにちは。LINEフロントエンド開発センターの幾野(ikuno)です。普段は LINE NEWS のフロントエンド開発をしています。
LINE NEWS では昨年テストをたくさん書いた
昨年 LINE NEWS ではたくさんテストを書きました。当初 78 ファイルだったテストファイルは半年で 150 ファイルに倍増しました。 テストカバレッジも statements 33.6% から 64.4% まで改善しました。
今回はどういったテストを書いたらうまくいったかについて書いていきます。
テストは意図しない breaking changes を防ぐために書く
そもそも、なぜテストを書くのでしょうか? 筆者は、テストは意図しない breaking changes を防ぐために書く と思っています。
もし意図しない breaking changes が起きたら一番に影響を受けるのはユーザーです。また、プロダクトにとってもユーザー離れや売上毀損などのリスクがあります。このリスクを少しでも減らすことができれば、私たちエンジニアは安心して機能開発ができ、リファクタリングや技術的アップデートなどにも挑戦できます。
testing-library を使う
LINE NEWS では testing-library を使ってテストを書いています。testing-library ではコンポーネントの詳細を見ないテスト(ブラックボックステスト)を簡潔に記述できます。例えば「open ボタンを押したらアコーディオンが開いて、open ボタンが close ボタンに切り替わる」というテストは以下のように書けます。
Accordion.tsx
const Accordion: FC<PropsWithChildren> = ({ children }) => {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen((v) => !v)}>
{isOpen ? 'close' : 'open'}
</button>
{isOpen && children}
</div>
);
};
Accordion.test.tsx
test('Accordion shows content if open button is clicked', () => {
// レンダリングして
const { getByRole, queryByRole, queryByText } = render(
<Accordion>
<p>content</p>
</Accordion>
);
// open ボタンを押したら
act(() => {
getByRole('button', { name: 'open' }).click();
});
// コンテンツと close ボタンが表示されているか
expect(queryByText('content')).not.toBe(null);
expect(queryByRole('button', { name: 'close' })).not.toBe(null);
});
このテストがパスしていれば、ユーザーが実際に open ボタンを押したときにもコンテンツが表示されるはずです。 逆にこのテストが落ちた場合にはなんらかの変更が起きたということです。 もちろん仕様を変えるときにはテストが落ちます。しかし、仕様を変えてないのに落ちた場合、修正方法に問題があると分かります。
testing-library は「画面をこう操作したら画面がこうなるべき」という記述を直接的に表現できます。コレは testing-library のユーティリティ関数が「アプリケーションコンポーネントをユーザーが実際に使うやり方でテストするために有用であるべき(筆者翻訳)」1)という原則に基づいて設計されているからです。「open と書かれたボタン」「content と表示されている要素」といった testing-library のクエリは実際にユーザーが見る画面に近いです。
enzyme のセレクタでも同様のことが出来なくはないですが、最初から設計思想に組み込まれている testing-library の方が自然に書けます。
画面操作とそれに対する期待する挙動が直接表現されているということは、テストの意図が汲み取りやすいということです。そのため、 テストが落ちたときにそれが意図した breaking changes なのかどうか判断しやすい です。コレは結構重要です。「テストが落ちたけど、このテストの意図がよく分からない…」となるとせっかく書いたテストも効果が半減です。
実際に LINE NEWS で書いているテストの例
実際に LINE NEWS で書いているテストをいくつか挙げてみます。
・一定時間ごとにニュースの横のパネル情報が 降水情報→天気→占い の順に切り替わっていくか
・「新しい記事をみる」ボタンを押すと記事一覧が更新されるか
いずれもコード修正時に変わってほしくない挙動ばかりです。
DOM スナップショットテストは費用対効果が高い
既存のプロダクトにテストを追加する場合、まずアプリの仕様を調べなければならず、テストを書く敷居がやや高いです。しかし、DOM スナップショットテストであれば詳しい仕様を把握してなくても簡単に追加ができます。しかもテストの効果も高いです。
スナップショットテストとは、予め取っていたスナップショットから変化してないか確認するテストです。DOM スナップショットテストは、アプリの DOM 構造が変化してないかを確認するスナップショットテストです。
DOM スナップショットテストはたった2行で書けます。 とても簡単です。
const { asFragment } = render(<App />);
expect(asFragment()).toMatchSnapshot()
コレだけで DOM 構造が変化してないか確認できます。画面崩れの簡易チェックにもなりますし、描画ロジックの確認にもなります。画面表示というのは最もユーザーの目につきやすい場所なので、breaking changes が起こってほしくない箇所の代表格でもあります。ぜひテストしましょう。
その他やっているテスト
今回詳しく書きませんでしたが、LINE NEWS では他にもテストを導入しています。storybook を使った visual regression test、puppeteer を使ってブラウザで実際に動かすテスト(動画の再生やカルーセルの動作を検証している)などです。いずれも意図しない breaking changes を防ぐという目的のもと書いています。
終わりに
「テストで意図しない breaking changes を防ぐ」という意識でテストを書いていった結果、「このテストは消していいんだろうか…」「テストが落ちてるけど、どこの変更がダメなのか分からない…」と悩むことがだいぶ減りました。また、CI でテストを自動実行してバグをたくさん検知できたり、リファクタリングを気軽に行えるようになりました。
本記事で見てきたように既存のプロダクトへのテスト追加も十分に有益ですので、ぜひテストを書いてみてください。
1) Matan Borenkraout. "Guiding Principles". Testing Library. 2020-11-04, https://testing-library.com/docs/guiding-principles/, 参照 2023-02-03
UIT 新春 Tech blog 2023記事一覧
1. Git submoduleを使ってマルチリポジトリなMonorepoを管理する
2. LINEドクターフロントエンド開発の流れ
3. 静的リソースをCDN配信する社内サービス『Abyss』のフロントエンドを作り直した話
4. LINE NEWS フロントエンドの自動テストの改善