ナガモト の blog

Full Cycle Developerを目指すエンジニアが有用そうな技術記事や、ポエムのようなよしなしごとを投稿するブログです。

Railsアプリ開発におけるテスト戦略 〜オレオレベタープラクティス〜

いきなりですが、戦術・戦略という言葉を正しく理解していますか?大雑把でもいいのでどう違うのか、どちらがより抽象度が高く・具体性が低いのか理解しておきましょう。

ざっくり言うと、戦略は大局的な目的や方策のことであり、戦術は戦略を実現するための具体的な手段や計画のことです。*1*2

大切なのは、戦略が間違っていてはどんなに優れた戦術を用いても大きく状況を改善するのは難しいということです。*3

ソフトウェアのテストにおいても、具体的な書き方・ルールの整備など優れた戦術を取っているにも関わらず、そもそもの戦略を間違えているがばかりに状況が改善しないことがあります。当記事はソフトウェアテスト(特にRailsRSpecの場合)における戦略を紹介します。

Railsアプリ開発におけるテスト戦略について語る前によくある課題と戦術、それから発生する問題を説明します。

テストコードによくある課題

  • テストが不足している
  • テストを書くコストが高い
  • テストを読む難易度が高い

これらの問題は表裏一体で繋がっています。そもそもテストを読む難易度が高いがために、書くコストが高くなり、コストが高いためにテストを書けずに不足してしまいます。

(RSpecで)とりがちな戦術

  • letは遅延評価なので冒頭ですべて定義しておく
  • よく使用するデータセットをテスト実行前に入れておく
  • FactoryBotで汎用性の高いデータを定義して使い回す
  • 定義済みレコードにupdateをかけて使い回す
  • shared_xxでまとめて定義して使い回す
  • オリジナルな記法・メソッドを定義して多数のexpectaion記述を端折る
  • eachやwhileなどのループで記法で多パターンのテストを行う

大抵は書く量を減らして読みやすくしたり、書きやすくしようという狙いで採用されます。*4 これらの戦術は用法用量を守って用いれば薬になるでしょうが、毒になる場合もあります。また、そもそもの戦略が間違っている場合、戦術レベルの対応では状況はじわじわと悪くなる一方です。

発生しがちな問題

  • 仕様変更やリファクタリングでshared_xxなど共通化した箇所が軒並み使えなくなる
  • 通化されているため、ある特定のケースにのみ、テスト項目を増やすことが難しい
  • 複数ファイルを行き来しなければテストの動作を把握できない
  • テストコードの複雑度が上がり、テストコードをテストする必要性を感じ始める

ではこういった状況に陥らないためのオレオレベタープラクティスを説明していきます。

Railsアプリ開発におけるテスト戦略 〜オレオレベタープラクティス〜

ベタープラクティスは次の流れで行います。

  • テストによりどんな課題を解決したいか確認する
  • ソフトウェアテストについての基礎基本を開発メンバー全員に理解してもらう
  • E2Eテスト, Controllerテスト, Modelテスト各レイヤの落とし所を決める
  • 戦略についてチームで共通認識を作る
  • 戦術についてチームに浸透させる

テストによりどんな課題を解決したいか決める

私はRailsアプリを開発する際にはスピーディな開発改善サイクルを回すことが重要だと思っています。そのためには高い頻度でリリースをしたいです。しかし、自動テストなしではデグレ発生が恐ろしく頻繁なデプロイはできません。そのため、解決したい課題は次の通りです。

「品質の担保を容易にし、安全に高い頻度でリリースを行えるようにする」

この課題について何を今更と思う人は多いでしょう。しかし継続的に開発していく中でテストを書くという行為がただの作業になってしまうことはたまにあります。そのような状態では非効率的なテストを書いてしまったり、実際にはありえないような意味のないテストを書いてしまいます。そんなときはそもそもの目的・課題に立ち返りましょう。

ソフトウェアテストについての基礎基本を開発メンバー全員に理解してもらう

ソフトウェアテストについて学んだことはありますか?「単体テスト結合テストホワイトボックステストブラックボックステスト・分岐網羅・条件網羅・境界値分析」これらの意味はわかるでしょうか?

私は大学・大学院で研究のためにソフトウェアテストの基礎基本を学びました。*5意識の低い学生でしたが、そこで学んだ知識はテストを設計するときに非常に役立っています。テストは経験則で自然と書いたり設計したりできるようになると思われがちですが、やはり基礎基本を抑えるとテストの勘所を掴むのが早くなり、効率よくどんどん良いテストを設計できるようになります。

ja.wikipedia.org

E2Eテスト, Controllerテスト, Modelテスト各レイヤの落とし所を決める

まず大前提として、すべてのレイヤで網羅率100%となるようなテストを行うのは現実的ではありません。そのため、基本的には品質の高いテストで効率よくバグを検出する方針をとることになります。

ソフトウェアテストの一分野、組み合わせテストでは「大多数のバグは少数因子の値の組み合わせで検出できる」と知られています。*6*7ソフトウェアは多数のモジュール(因子)の組み合わせとみなすことができるため、小さい各モジュール毎の単体テストを厚く行うことが比較的少ないテストで多数のバグを検出できるよいテスト方針となります。そもそも、複数のモジュールを組み合わせたテストを設計するのと、モジュール毎にテストを設計するのでは後者の方が簡単ということも単体テストを厚く行う方針の追い風です。

単体テストなどのソフトウェアテスト用語をRailsに対応させると次の通りです。

つまり、Modelテストを厚く行うことが効率の良い品質の高いテストになります。しかしながら、各レイヤ毎にしか検証できない項目や低い割合とはいえ組み合わせたときにのみ検出されるバグもあるため、Controllerテスト・E2Eテストも行わないというのはよくないと考えます。

理論とこれまでの経験も踏まえた結果として、私がよく利用する落とし所はこれです。

「Modelテストをほぼすべてのメソッドを検証する程度に厚く、 Controllerテストを正常系・準正常系のレスポンスをそれぞれ1パターン以上、E2Eテストをサービスにおけるクリティカルな機能*8について行う」

※時々のリソースや状況次第です

戦略についてチームで共通認識を作る

当然ですがテストコードもアプリケーションコードと同じく全開発メンバーが書くでしょう。あるメンバーはControllerテストのみを厚く書き、Modelテストを書かない。別のメンバーはModelテストしか書かない。そのような状況では品質の高いテストが実行できているとは言えません。

テスト戦略について決めているのであれば共有しましょう。チーム全員で決めて納得感を醸成することもいいでしょう。(少人数チームに限る)

戦術についてチームに浸透させる

戦略を共有し、同じ粒度のテストを書いていても、それぞれが自由きままに勝手にテストコードを書いていてはカオスになるでしょう。RailsというFWを用いてアプリケーションコードについては規約を重んじ、統一感のある書き方をしている。そんなエンジニアであれば、テストコードでも統一感ある書き方をした方がよいということはすぐ理解できるでしょう。

RSpecを用いたテストコードの書き方については次の記事に参考資料をまとめてあります。チームが大きくなるときまでには、そのチーム合った書き方・規約を大まかにでも定められるといいでしょう。 ngmt83.hatenablog.com

まとめ

Railsアプリ開発におけるテスト戦略 〜オレオレベタープラクティス〜」は、

  • ソフトウェアテストの基礎基本を大まかに理解する
  • 「Modelテストはほぼすべてのメソッドを検証する程度に厚く、 Controllerテストは正常系・準正常系をそれぞれ1パターン以上、E2Eテストはサービスにおけるクリティカルな機能についてのみ行う」
  • チームが大きくなるまでにRSpecの書き方・規約を大まかに定める
  • テストの目的・戦略を定期的に見つめ直す

これらをチームメンバー全員に共有・浸透させる。

です。

*1:「戦略」と「戦術」 - 違いがわかる事典

*2:3分でわかる孫子の兵法/戦略!戦いを避けながら、弱者が強者に勝つ | ビジネススキル大全 | ダイヤモンド・オンライン

*3:大阪から東京へ向かう際に、西へ進むという戦略ではどんなに早い乗り物(優れた戦術)を用いても東京にたどり着くために莫大なコストがかかる。

*4:他にはテスト実行時間が長く、オーバーヘッドを減らすためという狙いもある。

*5:ディペンダビリティ(高信頼性)を専門とする研究室でソフトウェアテストにおけるテストケースの自動設計について研究してました。

*6:出典論文: Software fault interactions and implications for software testing - IEEE Journals & Magazine

*7:組み合わせテストの日本語参考記事: 組み合わせテストの用語「2因子間網羅」「直交表」「All-Pairs法」 - Qiita

*8:例: ECサービスにおけるカートに入れて購入完了まで。マッチングサービスにおけるマッチング申請、承認、マッチング成立まで。ユーザ新規登録から完了まで。etc