giftee Tech Blog

ギフティの開発を支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信しています。

運用中のアプリケーションに後からテストコードを追加し品質を高めるための戦略を考える

運用中のアプリケーションに後からテストコードを追加し品質を高めるための戦略を考える

初めまして。そろそろ入社半年、SaaS事業部エンジニアの千葉です。今回は運用中のRailsアプリケーションについて、これまでテストを書かずにプロダクションコードを開発し続けてきたプロダクトへどのようにテストを追加し、品質をあげていくのかについて考察してきたことを話したいと思います。

背景

SaaS事業部で開発・運用を行なっているeGift Systemについて、チーム内で今後テストコードをしっかり追加していこうという機運が高まってきています。eGift Systemの開発については以前松島さんが書いてくれているeGift Systemの開発の流れについての記事を参考にしてください。eGIft Systemは弊社が発行しているeGiftの発行・消込を担う基幹システムのような位置付けで、24時間365日の安定稼働が求められている中、ほぼ毎日リリースが行われているほど現在も改善・機能追加が盛んに行われているプロダクトとなっています。

このようなプロダクトでありながらむしろなぜ今までテストコードを書いていなかったのか?というところは、ベンチャー企業としてビジネス機会を損失しないために求められる機能要求のスピード感に対してなりふり構っていられなかったというところです(gitのログが語っている)。5年以上運用されている今でなおビジネス要求は尽きることを知らないわけですが、これは使われているアプリケーションである証拠で本当に喜ばしいことです。

というわけで、プロダクションコードはいまなお厚みを増しており、安全に機能追加をしていくために時間がかかるようになってきているという実感があり、後からでもテストコードを追加して少しでも改善していこう、というわけです

話すこと

  • テストコードのプラクティス
  • テストを追加することで期待する効果
  • 運用中のアプリケーションの現状に合わせ、どのようにテストコードを追加していくことが効果的であるか

話さないこと

  • テストの書き方や実践方法

現状について

対象アプリケーションの現状として以下が挙げられます

  • 対顧客画面、管理画面、APIを含むモノリシックアーキテクチャ
  • 24時間365日稼働
  • 5年以上運用
  • ほぼ毎日リリース
  • テストはrspec使用
  • POROはほとんど使われておらず、ドメインロジックはView, Controller、Modelに散在

テストコードのプラクティス

そもそもテストコードを追加することにより、どのようにして品質を上げられるのかというところを正しく捉えられなければ、テストコードでさえも負債となってしまいます。

テストコードは振る舞いを定義する

テストコードは振る舞いを記述します。TDD(テストファースト)が効果を発揮できるのは、開発を自然な考えに分離できるからです。

  • 仕様を考える: テストコードを先に書くことで、そのコードがどのようなI/F、状態において実行後にどのような戻り値、状態となっているかを定義することができる
  • 機能するようにする: プロダクションコードを書き、機能するようにできる
  • 品質を高める: リファクタリングを行い、変更に強い実装にできる

テストコードもコードなのでリファクタリングする

テストコード自体の変更容易性を意識しなければ負債として作用してしまいます。

テストコードは振る舞いを定義するものとすると、テストコードが冗長であるということは機能が冗長であることを意味します。

  • 適切な抽象レイヤーを導入しバリエーションを吸収できる可能性がある
  • 必要なエンティティを見つけられていない可能性がある など

テスト自動化のピラミッド

  • ユニットテスト > 統合テスト > UIテストの順で多く書くようにすると費用対効果が高くなります。
    • 上に行くほど、テストコードの実装・実行コストが高くなります。

後からテストを追加する

以上がテストコードのプラクティスの代表です。では後からテストを追加していく時にはどのような方針になるかを考えます。

既存のコードに対して

  • UIテスト > 統合テスト > ユニットテスト の優先順位で書く
    • UIのレイヤーでの振る舞いは安定しているが、振る舞いの単位で実装ができていない状態になっている
    • ユニットテストを追加しても後々負債になってしまう。ハードルは高くなるが負債を生産するよりましなので、
      • I/Fの安定したUIテスト(request spec)から書く
      • 既存のコードを利用しながら、統合レイヤー(service, form object)を作成・リファクタリングしつつ、テストを追加していく
      • 最後にユニットテストを追加する
  • テストファーストよりもコスト・ハードルが高くなるので、全てに適用せず重要なドメインを選定することが重要。

新規コードに対して

  • 完全新規の場合には、基本テストファーストを推していく。
  • 機能拡張の場合には、新しく抽象やI/Fの拡張が必要となる。この場合は可能な限り、上記の方針でテストを追加・リファクタリングを進めた上で、機能拡張に対するテストコード・プロダクションコードを追加する。

今後の課題

Rails wayから大きく外れたくない

  • RESTの構造から外れ始めると、モデルが単一の振る舞いを書く場所と適切でなくなっていく→ドメインロジックが散在していく
  • 初期開発時から漸次的に開発を進める場合、初期に定義したデータ構造から歪みが生まれるのは必然的
  • (感想文)定期的にデータ構造を含めたリファクタリングをしていく必要性

対応案

  • フォームオブジェクトによるユースケースの吸収とサービスクラスによる複数modelを扱う振る舞いを吸収する
  • まとまった振る舞いをストラングラーパターンで漸次的に移行する

以上です。今回、後からテストコードを追加していく際に主に知っておくべきポイント書きましたが、実際にテストコードを書いていくといかにテスタビリティの高いプロダクションコードを書くか、フレームワークに合わせどういった構成が最適かなど、本当に奥が深く日々試行錯誤していくのは楽しんでいます。効果のほどはまた記事を書けたらと思います。テストを追加してもっとに楽しく開発していこうと思っています。

主に参考にした文献

  • すがわら まさのり,前島 真一,橋立 友宏,五十嵐 邦明,後藤 優一. パーフェクト Ruby on Rails【増補改訂版】
  • David Scott Bernstein, レガシーコードからの脱却
  • KentBeck. テスト駆動開発