Web フロントエンドのレガシーコードを置き換えるためのテストの考え方

この記事は、ドワンゴもスポンサードしていた JSConf JP 2021 にて、「Web フロントエンドのリプレースを支えるテストの考え方」というタイトルで berlysia がトークした内容をもとに再構成したものです。トークのアーカイブもご覧いただけます。

この記事は ドワンゴ Advent Calendar 2021 の3日目の記事です。

speakerdeck.com

宣伝

ドワンゴ EdTech Talk』と題した事業説明イベントを 12/8(水) に開催します。

ドワンゴ EdTech Talk

ドワンゴの教育事業で提供するオンライン学習サービス「N予備校」のライブ配信の授業を体験いただきながら、教育事業での取り組みを知っていただくためのイベントです。

最後までご参加いただくと N 予備校の有料会員相当の教材を 3 か月間無料で利用できる ように用意をしております。

Web 開発を学ぶ教材として好評をいただいているプログラミング講座もご利用いただけるほか、Web デザインや動画クリエイター講座などすべての有料会員向けコースが利用可能です。

ドワンゴの教育事業に現時点で関心のない方も、ぜひご参加ください。詳細は connpass のイベントページをご覧ください。

※社会人経験のある方を対象としております。

dwango.connpass.com


前書き:Webフロントエンドのレガシーコードを置き換えていくために必要なテスト

Web フロントエンドのレガシーコードを置き換えていくとき、既存の動作を維持しなければなりません。このときテストで維持する動作のカバー度合いや精度への要求は、そのサービスの個別の性質に大きく影響を受けますが、基本的な考え方は共通しています。

次のような性質を満たすテストが求められます:

  • 置き換えるにあたって動作の維持を確認し続けたいので、十分合理的な速度で実行できる
  • 置き換える前のコードも後のコードも同じように検証でき、置き換え前後の変化を観察できる
  • レガシーコードはテストが不十分なことが予測でき、ある程度簡易に作れるもので構成できる
  • 置き換えることが主眼であるため、あまり数を多くせずとも信頼性を確保できる

これらの性質を満たすようなテストとして、 シナリオベースの画面操作を検証する結合テスト に注目するとよいことを説明します。

置き換え(リプレース)とはリファクタリングの一種である

レガシーソフトウェア改善ガイド によれば、レガシーコードを改善するリファクタリングには次のような分類があります:

リファクタリング
コードの構造をメソッドやクラスのレベルで変更する
リアーキテクティング
モジュールやコンポーネントのレベルで行うリファクタリング
リライト
可能な限り高いレベルで行うリファクタリング
ビッグ・リライト
全てを一気に書き直し、置き換える
インクリメンタルなリライト
小さい単位でリライト、徐々に置き換える

ここで話題にしている置き換えは、レガシーソフトウェア改善ガイドにおけるリライトそのものであるとわかります。

何のために置き換えるのかを明らかにする

レガシーソフトウェア改善ガイド によれば、リライトは難しく、できるだけ避けるべきとしています。新規開発時の不確実さをまるまる引き継ぐほか、仕様自体の複雑さを解消しないと書き直しても複雑なままになることを欠点として挙げています。一方で利点もあるとしていて、歴史的経緯からの脱却や、テスタビリティを得るチャンスとなることを挙げています。

本の中では、それでもリライトを選ぶとしたら、リライト以外のリファクタリングでは達成できない問題があり、パラダイムシフトの反映を目的としているときにすべきだ、とも言っています。

f:id:berlysia:20211203130652p:plain
どんなときに置き換えを行うべきか

どのように作り直せばよいかよく吟味するためには、ドメインの理解と同時に、現状にどのような問題があって、どのようになりたいかを明確にすべきです。問題の姿が明らかでなかったり、そもそも今の実装がなにをやっているか理解できていない場合は、調査的リファクタリングによって洞察を深めることにより、端緒が開けるかもしれません。*1

このあたりの調査的リファクタリングや、運よくリライトより小さい範囲でよいとわかった取り組みには、 レガシーコードからの脱却 が述べている知識が支えになることでしょう。具体的なリファクタリングの例は リファクタリング(第2版) が親切に説明していて、参考になると思います。

置き換えに役立つテストとはどんなものか

置き換えはリライトであり、リファクタリングの一種でした。ところで置き換えたいようなレガシーコードには、十分なテストがなく、テストを書くことも難しいことがあります。どのようなテストがあればよいでしょうか。

冒頭で述べた次の 4 つの性質がありました:

  • 置き換えるにあたって動作の維持を確認し続けたいので、十分合理的な速度で実行できる
  • 置き換える前のコードも後のコードも同じように検証でき、置き換え前後の変化を観察できる
  • レガシーコードはテストが不十分なことが予測でき、ある程度簡易に作れるもので構成できる
  • 置き換えることが主眼であるため、あまり数を多くせずとも信頼性を確保できる

これらの性質を満たすテストは具体的にどのようなものかを考えます。

The Test Pyramid から

f:id:berlysia:20211203054405p:plain
発表スライドより。 画像は TestPyramid から引用

テストのピラミッドは、結合度によるテストの分類のうえでいくらかの性質を述べている図です。次のようなことを言っています:

  1. より結合度の低いテストは実行速度が速く、結合度が高いほど遅い
  2. より結合度の低いテストはメンテコストが低く、結合度が高いほど高い
  3. 分類ごとの数のバランスは結合度が低いほど多く、結合度が高いほど少ないことが望ましい

この図には多くの指摘がついていますが、Martin Fowlerさんによれば次のような補足が与えられています:

TestPyramid によれば、次のことに注意が必要です:

  • 1と2の性質が実際には満たされず、結合度の高いテストが十分に実行速度が速くてメンテコストが低いのなら、結合度の低いテストは省きえます。

合わせて The Practical Test Pyramid によれば、この図から次の2点を学ぶべきです:

  1. 結合度の異なるテストを書くべきである
  2. 結合度のより高いテストは、より少ない数でも効果を発揮する

The Testing Trophy から

f:id:berlysia:20211203054420p:plain
発表スライドより。 画像は Static vs Unit vs Integration vs E2E Testing for Frontend Apps から引用

Testing Libraryの作者のKent C Doddsさんが提唱しているテスティングトロフィーという概念は、次のようなことを言っています。上からE2Eテスト、結合テスト、単体テスト、静的テストの順に並んでいて:

  1. より下位のテストは実行速度が速く、上位ほど遅い
  2. より下位のテストはメンテコストが低く、上位ほど高い
  3. より上位のテストはより大きな問題に関心があり、より多くの信頼度をもたらす
  4. 数のバランスは、図中のトロフィーのシルエットと対応することが望ましい。結合テストが最も多い

とくに4について、1,2,3の関係から信頼度とコスト・速度の間にトレードオフの関係が存在するとわかるので、最もバランスが良い結合テストを多くするのがよい、ということを言っています。

変更範囲とテストの能力

三重になったモジュールの入れ子関係がある。内側のふたつを全く新しい実装に置き換えることを考える
このようなコードの変更を考える

いま私たちがやろうとしているのは、コードの置き換えです。図のようなモジュールの関係のもとでコードの置き換えをするときに、役に立つテストと立たないテストがあります。

f:id:berlysia:20211203062111p:plain
4つのテスト

次のような4つのテストをイメージします:

  1. E2Eテストなど、置き換えるコードより外側に書かれたブラックボックステスト
  2. 置き換えるコードに書かれた結合テストで、ブラックボックステスト
  3. 置き換えるコードより内側に書かれた結合テストで、ブラックボックステスト
  4. 置き換えるコードに書かれた結合テストで、内側を知っているホワイトボックステスト
f:id:berlysia:20211203062525p:plain
役に立つテストはどれか

このとき、コードの置き換え前後で変わらない信頼性を発揮し、置き換えによる変更を保証してくれるのは、1と2のテストです。すなわち、置き換える単位より外側に書かれたブラックボックステストだとわかります。

置き換えを達成することに注目して

f:id:berlysia:20211203062651p:plain
レガシーコードのテストにありがちなこと:ない

ここまでで、結合テストかつブラックボックステストであることが望ましいとわかっています。Testing Libraryのような道具により、ユーザーの操作過程で順に特徴点をとらえるようなテストがほしいところです。

f:id:berlysia:20211203064731p:plain
結合テストの例

しかし、置き換えたいコードはレガシーです。特徴点を捉えることは難しいかもしれません。

たとえば特徴点が取りづらいときは、DOMのスナップショットを撮って比較することも選択肢になります。いわゆるChange Detectorではありますが、変更が結果に与える変化を監視するには十分な効果が得られます*2。同様にビジュアルリグレッションテストも外観のスナップショット比較として活用しえます。

f:id:berlysia:20211203065027p:plain
DOMのスナップショット比較をする例

こうした一時的に信頼度を稼ぐためのテストは、置き換えが終わったあとに特徴点を捉えるような記述で置き換えていくと、長期的に役立つちょうどいいテストになっていくでしょう。

実際にこうした一時的に役立たせるようなスナップショットによるテストは、N予備校のフロントエンドでも大きく効果を発揮しました。*3

結論:シナリオベースの画面操作を検証する結合テストに注目せよ

ここまでの議論から、Webフロントエンドのレガシーコードを置き換えるときのテストに求められる4つの性質に対して、次のような説明が与えられます。

  • 置き換えるにあたって動作の維持を確認し続けたいので、十分合理的な速度で実行できる
    • より結合度が低いテストは実行速度が速い
    • 結合度が高いテストが十分に高速であれば、より結合度が高いテストでも問題ない
  • 置き換える前のコードも後のコードも同じように検証でき、置き換え前後の変化を観察できる
    • 置き換える単位よりも結合度が高く、かつ内部構造に依存しないテストがよい
  • レガシーコードはテストが不十分なことが予測でき、ある程度簡易に作れるもので構成できる
    • ビジュアルリグレッションテストやスナップショットテストのように、少々メンテコストがかかっても作成コストが低いテストが役立つ
  • 置き換えることが主眼であるため、あまり数を多くせずとも信頼性を確保できる
    • ユーザーの使い方に近く、より結合度が高いテストは信頼性が高い

以上より、シナリオベースの画面操作を検証する結合テスト に注目すると良いことがいえます。

宣伝

12/8のイベントもよろしくお願いします。くどいですが、最後までご参加いただくと N 予備校の有料会員相当の教材を 3 か月間無料で利用できる 権利が得られます。

dwango.connpass.com

We are hiring!

株式会社ドワンゴの教育事業では、一緒に未来の当たり前の教育をつくるメンバーを募集しています。 カジュアル面談も行っています。 お気軽にご連絡ください!

カジュアル面談応募フォームはこちら

www.nnn.ed.nico

開発チームの取り組み、教育事業の今後については、他の記事や採用資料をご覧ください。

speakerdeck.com

*1:より攻めた概念として「ソフトウェア式年遷宮」に触れておきます。置き換えを定期的に行うことで、チームのドメインへの理解を深め、時代に即した作りへの変化を達成し続けることを指しています。

*2:差分が大きすぎる場合はスナップショットのdiffを保存するとよいですが、gitの差分のなかでdiffのdiffを見るのはとても眼力がいるため、レビューしにくいことに注意してください

*3:このリファクタリングは私がリードしつつ、当時のメンバーと二人三脚で進行しました。一時期は私が離れてお任せするような時期もありました。今になってこの時の取り組みがやっと実を結びつつありますが、それでもまだ半ばです