Laravelでのテストガイドラインを考えるスクラップ
前提
- デトロイト派でのアプローチを取る
- Eloquentを使用する
- PHPUnitを使用する
- Legacy Factoryは使用しない
Laravel UnitTest Guide
テストコードは本番コードよりも何十回・何百回と読まれる可能性があるので、本番コードと同様に品質に気を使うことが後々のメンテナンスコストを大きく下げることに繋がる。
明確なテストを目指す
明確なテストとは
- 存在目的と失敗理由が失敗の原因を究明するエンジニアから見て明確となるテスト
- 失敗の理由が明白でないか、もともと何故かかれたかを理解するのが難しい場合、テストは明確性を達成できていない
- 明確なテストは、テスト対象システムをドキュメント化したり、新しいテストの基礎の役目をより円滑に果たす
明確なテストを達成するために
- 完全性
- テストがどのようにその結果に到達するか理解するために、読者が必要とする全情報をその本体部分が含んでいる場合に完全
- 簡潔性
- 他の紛らわしい、もしくは無関係な情報が含まれていない場合に、簡潔
つまりテストの本体部分は、重要でない情報や紛らわしい情報は全く含まずに、テストを理解するのに必要な情報を全部含むべきであるということ
if・forを使用しない(ロジックを含めない)
if
やfor
をテストコードの中で使用してしまうと、可読性が著しく落ち、テストへの理解が難しくなり、保守されにくいテストコードができやすくなる。
markInTestCompleteは使用しない
markInTestComplete()
によってテストケースをSkipすることが出来ますが、非推奨です。Skipするとそのテストは「無い」と等しいので消すか修正をする。
どうしても、どうしてもSkipしたくなったら理由をちゃんと書いておく
時間が関係するテストはCarbon::setTestNow()で固定する
時間を固定しないでテストを行ってしまうと、ある時点で成功し、ある時点で失敗するテストが出来上がり読み手が理解できない可能性があるため固定を推奨する。
注意点として、本番コードでCarbonImmutable
を使用している場合はCarbonImmutable::setTestNow()
で固定しなければいけない、同様に本番コードでCarbon
を使用している場合はCarbon::setTestNow()
が必要になる。双方利用している場合は双方指定する必要がある。
また、Factoryで直接指定する場合はCarbonはUTCにキャストを行ってくれない。例えば、created_at
をCarbon::now('Asia/Tokyo')
に指定してしまうとDBにはAsia/Tokyo
の時刻でUTCとしてデータベースに保存されてしまう。
RefreshDatabaseとDatabaseTransaction
RefreshDatabase
はテスト実行前にmigrationを行い、DatabaseTransaction
を呼び出す。テストを迅速に回したい場合はDatabaseTransaction
を使うと良い。
ただし、auto-incrementされた値は初期化されないため、注意する。
Model::Factoryでcreateする場合はIDの決め打ちを行わない
IDを決め打ちしてModel::factory()->create()
を行ってしまうと上記のDatabaseTransactionを使用した際にユニーク制約でテストが失敗する可能性がある。
また、テストの並列化もできなくなる。なので、ID指定は行わずにcreate()
した際の戻り値を使ってテストに使うと良い。
Factoryの定義は何にも依存させない
- Model::Factoryの理想形としては「どのテストケースにも依存しない汎用性を持ちつつ何も考えずにFactoryできる」
- Model::factoryはテストで一番依存される部分であり、ここの定義が何かに依存したものだとテスト保守コストが上がる
- 外部キーは制約がある場合は固定値を指定するのではなく、都度参照先をfactoryするよう設定する
-
'user_id' => User::factory()
のように
-
- あらかじめコーナーケースに設定しておく、例外ケースにしておくなどしてしまうと、Factoryの初期設定に依存するテストケースが出てくる恐れがある
Seederを利用しない
Seederを用いてテストデータを作成しテストに利用してしまうとテストコードの完全性が大きく失われ、保守しにくいテストコードとなる。
また、Table定義が変わった際にcsvでSeedしている場合は依存しているテストが全て壊れるので使用を控える。
DRYではなく、DUMPを目指す
Don't Repeat Your Selfではなく、Descriptive And Meaningful Phrases(説明的かつ意味がわかりやすい言い回し)を目指す。
dataProviderは便利な反面、テストフィクスチャへの見通しが悪くなり、可読性が低下する。
使い所を考える必要がある。
曖昧な名称の共有値と初期設定値は簡潔性を失わない程度に
テストコードの完全性を保つために、曖昧な名称で共有されるメソッドを定義してテストコードに利用することは非推奨である。
テストコードが簡潔に書けるようにはなるが、後々見た時にテストコードが何をテストしているのかが理解しづらい場合が多い。
同様に初期設定(setUp)やデータプロバイダーで多くの事を定義しすぎるとテストの可読性が著しく落ちる。複雑化するデータプロバイダーの場合は別テストメソッドに分離した方が後々のコストは小さくなる。
publicメソッドに対してテストを実装する
privateメソッドに対してテストを書いてしまうと、privateメソッドは実装詳細に当たるため、脆いテスト(リファクタリングをしただけで壊れるテスト)に繋がる可能性がある。
脆いテストになってしまうと、リファクタリングでさえ行えない状態になってしまうためメンテナンスコストが大きくなってしまう。加えて、privateメソッドにもテストを書くとなるとリフレクションが面倒になり、全てpublicで宣言される可能性が出てきてしまう。
privateメソッドが多くなりテスト準備が難しくなっている場合は多くの事をやりすぎている可能性が高いため、クラス設計を見直す方が良い。
また、例外が存在し仕様化テスト(現在実装の出力を固める)ためのテストではprivateメソッドのテストを許可する。
メソッドではなく、挙動をテストする
本番コードのメソッドと1対1になるようにテストメソッドを書いてしまうと、最初は便利だが時間とともに問題が発生する
- メソッドが複雑になるにつれて、そのテストの複雑性が増大し、実際に何をやっているのか推論が難しくなる
- 後からエンジニアがメソッドを拡張した時に、テストメソッドも拡張され、複雑性が増し、扱いが難しくなっていく
- メソッド周りにテストを組み立てていくと、テストが不明確になるように自然と促される
挙動は「〜という前提条件下で(given)」「〜場合(when)」「その場合は〜(then)」といったGiven-When-Thenパターンを利用する。(下記のAAAパターンでもよい)
挙動に対してテストを書く利点としては以下がある
- 自然言語を読むのに比較的近い形で読め、自然に理解ができる
- 各テストの範囲が比較的限られているので、原因と結果がより明確になる
- 各テストが短く説明的になるため、どの機能が既にテストされているのか知るのが比較的容易となっており、既存メソッドの拡張ではなく、効率化された新規テストメソッドを追加するように促される
挙動を強調するようにテストを構成する
空行を用いてGiven-When-Thenを見分けやすくすることで、読者は3つの粒度でテストを読むことができる。
- 読者はテストされる挙動の大雑把な説明をテストメソッド名から理解できる
- メソッド名を見るだけで十分でなかった場合は、挙動の正式な説明として「〜という前提条件下で」/「〜場合」/「その場合は〜」のコメントを見ることができる
- 最後に読者は、その挙動がどのように表現されているか正確に知るため、実際のコードを見ることができる
テストされる挙動にちなんで命名をする
- テスト対象の挙動を要約して命名すると失敗時に理解がしやすくなる。
- テストスイート内の全テストメソッドの名称を読むことで、テスト対象システムが実装している挙動がよく分かる
- テストの名称に「また」という語を使わなければいけない場合、複数の挙動をテストしている可能性が高く、別のテストメソッドに分ける状態である可能性が高い
成功パターンだけのテストにしない
境界値や例外をテストせずに、期待される結果だけをテストするテストは品質面での保証効果が薄い。
モックを最低限に抑える
- モックは利用するのが簡単であり、依存関係を無視できるためテストが簡単に書けるようになる
- その反面、脆いテストになりやすく、モックを利用したテストは信頼性に欠ける
- 本物のオブジェクトが高速で决定性である限りは、モックオブジェクトよりも本物のオブジェクトを使用するほうが好ましい
- モックを行わないと依存関係の解決が難しいコードは設計ミスの可能性が高く、モックを使うことで解決すると、貴重な改善の機会を失う
最近良い勉強会があったので
AgileTestingの資料