バックエンドのテスト理論を学び、戦略を考える
Hahnah こと 原井夏樹 です!
Xをフォローしてくれると喜びます。
本記事は私の所属するスマサテ株式会社で行ったテスト勉強会の内容と、テスト戦略策定について紹介するものです。
バックエンドに焦点を当てて、テスト戦略やテストの流派について学びましょう!
大前提: テストにおける大切なこと
- テストすることが開発サイクルに組み込まれていること
- コードベースの特に重要な部分のみがテスト対象となっていること
- 最小限の保守コストで最大限の価値を生み出せるようになっていること
(書籍「単体テストの考え方/使い方」より)
これらを達成するために必要なことは様々にありますが、それを分かるようになるために、テストにおける基礎的な知識を学んでいきましょう。
よくある誤解
❌ 「テストカバレッジは高いほどいい」
重要でないコードベースに詳細なテストが大量にあっても、価値を発揮しない上に保守コストがかかります。むしろいらないテストです。
そのようなテストは、「コードベースの特に重要な部分のみがテスト対象となっていること」「最小限の保守コストで最大限の価値を生み出せるようになっていること」に反することがわかります。
世の中のテスト戦略たち
さまざまなテスト戦略の特徴や良し悪しを抑えましょう。
以下の3つについてみていきます:
- アイスクリームコーン
- テストピラミッド
- テストハニカム
1. アイスクリームコーン
アイスクリームコーン型のテスト戦略では、手動テストやE2Eテストを充実させ、インテグレーションテストやユニットテストの数・それにかかるコストを抑える戦略を取ります。
(画像は https://gihyo.jp/dev/serial/01/savanna-letter/0005 より)
これはテストのリアリティに重きを置いた戦略ですが、現代の技術ではE2Eテストは不安定で実行が遅く、メンテナンスコストが高いです。
この戦略での自動テストでは、品質を保証する役割をこなすことは現実的にはできず、開発生産性が著しく低下してプロダクトの競争力を失うことに繋がってしまいます。
将来的に技術革新が起こりE2Eテストの安定性・実行時間・コストが改善されれば、この戦略が妥当になる可能性はありますが、少なくとも現代ではアンチパターンです。
2. テストピラミッド
テストピラミッドではユニットテストを最も充実させ、次にインテグレーションテストを重視します。E2Eテストにはそれほど労力を割きません。
(画像は https://gihyo.jp/dev/serial/01/savanna-letter/0005 より)
テストピラミッドは安定的で、実行が高速で、維持しやすい、費用対効果の高いテスト戦略です。
E2Eテストは実行が失敗しやすく、壊れやすくメンテナンスコストが高い上、構築や実行に時間がかかります。人的リソース含めお金もかかります。
テストピラミッドの戦略では、その点は現実の制約を受け入れ、E2Eは必要最小限に留め、他のテストを比較的充実させることを優先します。
この戦略は現実的でコストパフォーマンスにも優れ、信頼性と開発速度のバランスが取れると言われています。
3. テストハニカム
テストハニカムはマイクロサービスのテスト戦略に良いとされるものです。
(画像は https://engineering.atspotify.com/2018/01/testing-of-microservices より)
インテグレーションテストを重視し、マイクロサービス内部の詳細な実装のテスト(Implementation Detail)はそこまで重視しません。他のサービスの挙動も含めてテストするIntegratedテストは、他のサービスに依存するが故に壊れやすく脆弱なため、これもあまり重視しません。Integratedテストはゼロになることが理想です。その代わりに各サービス内でテストするべきでしょう。
テストハニカムはマイクロサービス向けのテスト戦略ではありますが、プロダクトの特性上インテグレーションテストを重視すべき理由があれば同じようなテスト戦略を取ることはあり得るかもしれません。
コードに対するテストの価値
はじめに、以下のことが大切だと述べました。
コードベースの特に重要な部分のみがテスト対象となっていること
どんなコードに対するテストなのかによって、そのテストの価値は概ね決まります。
最も大切なのは、ビジネスロジックを含む部分、つまり ドメインモデル のコードに対するテストです。
そのほかのコードの種類としては、
- インフラ関連のコード
- 外部サービスや依存関係にあるもの
- 構成要素同士を結ぶグルーコード
などがあり、これらに対するテストはそれほど重要ではありません。
(ただしプロダクトの特性に依ります)
何のコードに対するテストを重視するかを自覚しておくことで、自信を持ってテスト戦略を定められるはずです。
テスト分類の境界はどこにあるか
クイズ: これって何テストだと思いますか?
require 'rails_helper'
RSpec.describe User, type: :model do
describe '#full_name' do
let(:user) { User.create!(first_name: '太郎', last_name: '山田') }
it '姓と名を連結した文字列を返す' do
expect(user.full_name).to eq '山田太郎'
end
end
describe '#create!' do
let!(:user) { User.create!(first_name: '太郎', last_name: '山田') }
it 'ユーザーレコードが保存される' do
expect(User.find_by(first_name: '太郎', last_name: '山田')).to be_present
end
it 'プロフィールが作成される' do
profile = Profile.find_by(user_id: user.id)
expect(profile).to be_present
expect(profile.user).to eq user
end
end
end
答えはこちら
• 古典学派においては、1つ目も2つ目も3つ目もユニットテスト
• ロンドン学派においては、1つ目: ユニットテスト、2つ目: インテグレーションテスト、3つ目: インテグレーションテスト
• テストサイズ分類においては、1つ目: Smallテスト、2つ目: Mediumテスト、3つ目: Mediumテスト
「ユニットテストの定義って24種類あんねん」 by ア⚫️ミカ
ユニットテストを重視する?インテグレーションテストを重視する?それともE2Eテスト?
では、それらテストの境界はどこにあるんでしょうか?
定まった答えはありません。それもそのはず、ユニットテストだけとっても、24種類もの定義があるからです。
しかし、テスト戦略において例えばテストピラミッドのように「ユニットテスト > インテグレーションテスト > E2Eテスト の順に多くする」と決めたとしたなら、何がユニットテストで何がインテグレーションテストなのかを定義しないと、戦略の遂行が適切にできません。
24種類の中から選んでも良し、自分たち独自の定義を作っても良しです。
次に、ユニットテストの定義の中でも主流なものを紹介します。
主流なユニットテストの定義たち
古典学派によるユニットテストの定義
古典学派では「1つの振る舞いに対するテスト」をユニットテストとします。
また、2つ以上の振る舞いの組み合わせに対するテストがインテグレーションテストとなります。
古典学派はコードが分からずともテストケースを作れるのでTDDと相性が良いというメリットがあります。
ロンドン学派によるユニットテストの定義
ロンドン学派では、「依存ロジックやシステム(DB含む)などがないテスト」、あるいは「それらが全てモックやスタブに置き換えられた状態でなされるテスト」をユニットテストとします。
テストサイズでのスモールテストの定義 (ユニットテスト相当)
Google社内から広まったテスト分類方法で、「テストサイズ」あるいは「SMLテスト」と呼ばれるものがあります。
ユニットテスト、インテグレーションテスト、E2Eテストに分類するのではなく、
Small テスト、 Medium テスト、 Large テストに分類します。
ユニットテストの定義は曖昧で分類がいい加減になりがちなのに対し、テストサイズの考え方はより明確で分類しやすいです。
次の表の基準に従って分類します。
機能 | Small | Medium | Large |
---|---|---|---|
ネットワークアクセス | ❌ | localhost only | ✅ |
DBアクセス | ❌ | ✅ | ✅ |
ファイルシステムアクセス | ❌ | ✅ | ✅ |
外部システム | ❌ | なるべく❌ | ✅ |
マルチスレッド | ❌ | ✅ | ✅ |
スリープ | ❌ | ✅ | ✅ |
システム設定 | ❌ | ✅ | ✅ |
実行時間上限 (秒) | 60 | 300 | 900+ |
- Small
単一のプロセス内で動作するテスト。非常に高速に動作し、かつスケールするが、単一プロセス内で動作させるため外部リソースを用いない。プロセス外への通信はテストダブルで置き換える。 - Medium
単一のマシンに閉じた環境内であれば、外部リソースの利用を許容する。 - Large
自動テストを実行するマシンから他のマシンへの接続を許容する。本番環境やそれと同等環境を利用したテストなどが相当する。
3流派の比較
古典学派のユニットテスト、ロンドン学派のユニットテスト、テストサイズにおけるSmallテストが扱う範囲は、相対的には下表のようになります。
古典学派 | ロンドン学派 | テストサイズ | |
---|---|---|---|
ユニット(Small)テストが扱う範囲 | かなり広い | 狭い | 中くらい(狭い寄り) |
モックやスタブの利用 | 必要最小限 | 積極的 | 積極的 |
モック・スタブ利用の良し悪し
まずは定義を確認します。
- テストダブルとは: テストコードにしか現れない偽りの依存。モックとスタブに分類できる。
- モックとは: システム外部へ向かうコミュニケーション(出力)を模倣したもの。対象例は別システムへのAPIリクエストなど。
- スタブとは: システム内部へ向かうコミュニケーション(入力)を模倣したもの。対象例は別システムからのAPIレスポンスなど。
モック・スタブ利用には良し悪しがあります。
流派によって良い・悪いという立場をはっきり取っている場合もありますが、そこにはトレードオフがあります。
メリット | デメリット | |
---|---|---|
モック・スタブをなるべく使わない | 実際のシステムに近い状態でテストできる | テストの準備が大変。テスト実行が遅い。テストが壊れやすい。別システムの挙動にテスト結果が振り回される |
モック・スタブを積極的に使う | 実際のシステムでは再現しづらい、きめ細やかなケースをテストできる。テストの準備が比較的簡単。テスト実行が早い。 | 実際のシステムをあくまで模倣したテストでしかない |
モック・スタブをどういうシーンで使い・使わないかもテスト戦略に影響してきます。
テストハニカムについて備考
テストハニカムの戦略では、それ自体がテスト分類法も持ち合わせています。
なのでテストハニカム戦略を取る場合はその分類法に則るのを第一選択肢として考えるのが良いです。
Ruby on Rails における 古典学派 vs ロンドン学派 vs テストサイズ
スマサテではバックエンド(Ruby on Rails)のテストはRSpec書くのですが、RSpecでは積極的にテストダブルを使う文化があり、そのためのAPIも充実しています。
そういった点を踏まえると、積極的にモックを使うロンドン学派やテストサイズによる分類と相性が良いように思えます。
しかし一方で、ActiveRecordによるドメインモデルとデータベースの密結合があり、それをモックするのには無理があります。この点においては、モックを必要最小限に留める古典学派と相性が良いとも言えます。ロンドン学派やテストサイズでの分類通りにテストを書き分けることは難しいです。
よって、Ruby on Rails で作ったシステムのテストを実装するには、ロンドン学派、テストサイズは適用しづらく、古典学派が適用しやすいです。
ただ、古典学派はユニットテストと表現される範囲がかなり広いため、テスト戦略を遂行できるかどうかに注意を払う必要があります。
ここまでの話を踏まえると、Ruby on Rails においては他のテスト分類法を模索する必要がありそうです。
スマサテのバックエンドテスト戦略を考える
以上の基礎知識を踏まえ、スマサテでは自分たちに合ったテスト戦略を考えました。
仮にテストピラミッドの戦略を取るとしましょう。
テストの分類について古典学派に則ったとしたなら、それはロンドン学派にとってはテストハニカムに近い戦略をとったことと等しいです。
故にテスト戦略とテスト分類法の組み合わせは重要です。
自分たちが何を重視すべきか、何が合理的かを考えて、どのようにユニットテストを定義してどの戦略を取るのかを定める必要があります。
また、そのテスト戦略が維持できるものでなければ意味がありません。
テスト戦略の結論
コストパフォーマンスに優れるテストピラミッドの戦略を選びました。
テスト分類は独自のものを考え、以下のようにしました。
- ユニットテスト: Model, Serviceクラスのテスト
- インテグレーションテスト: Requestのテスト
Model, Service では積極的にテストダブルを使用し、さまざまなパターンのテストを実現します。
ActiveRecordによるドメインモデルとデータベースの密結合はモックせずに扱います。これにより、ModelとServiceのテストは完全にユニットテストとみなせます。
この分類は、Ruby on Rails においてごく自然ではないでしょうか。
そして、コアなドメインロジックは、ModelとServiceにあるので、テストピラミッド戦略によりそれらを重視することは合理的と言えます。
最初に述べた以下の基準を満たすことができました。
• テストすることが開発サイクルに組み込まれていること
• コードベースの特に重要な部分のみがテスト対象となっていること
• 最小限の保守コストで最大限の価値を生み出せるようになっていること
テスト戦略の別案: テストハニカム
別の戦略も考えていました。
スマサテのシステムはマイクロサービスの構成に近かったので、テストハニカムの戦略を取ることを考えていました。
メインとなるシステムでテストハニカム戦略をとり、各サービスとの連携のテスト(Integrationテスト)を重視します。
そして各サービスではテストピラミッドの戦略でテストするのです。
メインとなるシステムのテストでは、Implementation Detail テストが薄くなりますが、それをカバーするためにRubyの型記述言語RBIと型チェックツールSorbetの導入を考えていました。
理想的な戦略のように思えるのですが、そのための体制を維持できるかどうかに懸念があり、この戦略は採用しませんでした。
フロントエンドのテスト戦略はどうなのか
私が以前書いたこちらの記事で、フロントエンドのテストについて整理しています。
ご参考までに。
最後に
以上が、バックエンドテストに関する基礎知識と、スマサテがとったテスト戦略です。
参考になったら、"いいね❤️" と、Xをフォローしてくれると喜びます
また、弊社ではWebフルスタックエンジニアを積極的に採用しています。
ぜひ気軽にお話を聞きに来てください〜 お待ちしています!
Discussion