Upgrade to Pro — share decks privately, control downloads, hide ads and more …

テストコードのガイドライン 〜作成から運用まで〜

Rikuto Sato
December 21, 2024

テストコードのガイドライン 〜作成から運用まで〜

PHP Conference Japan 2024の登壇資料です。

Rikuto Sato

December 21, 2024
Tweet

More Decks by Rikuto Sato

Other Decks in Programming

Transcript

  1. #phpcon #track5 17 Outline ガイドライン作成を決意するまで 書籍から学んだテストの基本 • なぜテストするのか • 振る舞いをテストするということ

    • 何をどのくらいテストすればいいのか • 認知負荷を下げるための⼯夫 ガイドライン作成のポイント 導⼊後の課題 まとめ‧今後の課題
  2. #phpcon #track5 18 Outline ガイドライン作成を決意するまで 書籍から学んだテストの基本 • なぜテストするのか • 振る舞いをテストするということ

    • 何をどのくらいテストすればいいのか • 認知負荷を下げるための⼯夫 ガイドライン作成のポイント 導⼊後の課題 まとめ‧今後の課題
  3. #phpcon #track5 19 なぜテストするのか 少なくともリグレッションテストにおける⾃動テストの⽬的とは、 成⻑し続けるプロダクトを安全にリリースできる状態を、持続可能なコストに キープし続けることだとわかります。(テスト⾃動化実践ガイド p29) 「バグを捕捉すること」は、テストの動機の⼀部にすぎない。 (中略)同等に重要なものは、変化を可能とする能⼒を備えておくため、

    という理由である。 新機能追加、コードの健全性に主眼を置いたリファクタリングの実施、 (中略)どれを⾏っていようが、⾃動テストは間違いを素早く捕捉でき、 そのおかげで信頼性を保ちつつソフトウェアを変更できるようになる。 (Googleのソフトウェアエンジニアリング 11章 テスト概観) バグを早い段階で捕捉し、変更容易性を保つ
  4. #phpcon #track5 23 Outline ガイドライン作成を決意するまで 書籍から学んだテストの基本 • なぜテストするのか • 振る舞いをテストするということ

    • 何をどのくらいテストすればいいのか • 認知負荷を下げるための⼯夫 ガイドライン作成のポイント 導⼊後の課題 まとめ‧今後の課題
  5. #phpcon #track5 24 実装の詳細と振る舞い 最終的な出⼒ 関数が返す結果 (例) テスト対象の処理の結果、 得られた値が意図したものに なっていることを検証する

    (例) テスト対象のコードから呼ばれる 関数が、想定通りに呼ばれている ことを検証する 実装の詳細 振る舞い 具体的な処理の中⾝
  6. #phpcon #track5 26 実装の詳細のテスト(例) 代表例はモック[*]を利⽤したテスト final class Checkout { public

    function execute(int $totalPrice): int { if ($this->discountService->needsDiscount($totalPrice)) { // 割引処理 $checkoutPrice = $this->discountService->discount($totalPrice); } } } 「1000円以上で100円割引」というロジックを考えてみる ※不完全なコードであることはご容赦ください この関数(execute)のテストについて discountServiceをモックにして書いてみる [*] 厳密にはテストダブルという表現が正しいですが、説明を簡単にするため 聞き馴染みのあるモックという⾔葉を使っています
  7. #phpcon #track5 27 実装の詳細のテスト(例) public function test_1000円未満の場合は割引されない() { $discountServiceMock =

    $this->createMock(DiscountService::class); $discountServiceMock ->expects($this->once()) ->method('needsDiscount') // needsDiscountメソッドが1度だけ ->with(500) // 引数500で呼ばれることを確認して ->willReturn(false); // falseを返すようにmockする $discountServiceMock ->expects($this->never()) // discountメソッドが呼ばれないことを確認 ->method('discount'); $checkout = new Checkout($discountServiceMock); $checkout->execute(500); }
  8. #phpcon #track5 28 実装の詳細のテスト(例) public function test_1000円未満の場合は割引されない() { $discountServiceMock =

    $this->createMock(DiscountService::class); $discountServiceMock ->expects($this->once()) ->method('needsDiscount') // needsDiscountメソッドが1度だけ ->with(500) // 引数500で呼ばれることを確認して ->willReturn(false); // falseを返すようにmockする $discountServiceMock ->expects($this->never()) // discountメソッドが呼ばれないことを確認 ->method('discount'); $checkout = new Checkout($discountServiceMock); $checkout->execute(500); } 割引されない条件を discountメソッドが呼ばれないことで テストしている if ($this->discountService->needsDiscount($totalPrice)) { // 割引処理 $checkoutPrice = $this->discountService->discount($totalPrice); } テスト対象のコード
  9. #phpcon #track5 29 仕様変更が⼊ったら… 1000円以上で100円の割引に加え、 無条件に50円引きが追加されたら…? public function execute(int $total)

    { if ($this->discountService->needsDiscount($total)) { $total = $this->discountService->discount($total); } // 50円引き! $checkoutPrice = $this->sale50YenOff($total); } ※雑なコードであることはご容赦ください discount()以外に割引処理が⼊った!
  10. #phpcon #track5 30 テストが仕様変更に追従しない public function test_1000円未満の場合は割引されない () { $discountServiceMock

    = $this->createMock(DiscountService::class); $discountServiceMock ->expects($this->once()) ->method('needsDiscount') // needsDiscountメソッドが1度だけ ->with(500) // 引数500で呼ばれることを確認して ->willReturn(false); // falseを返すようにmockする $discountServiceMock ->expects($this->never()) // discountメソッドが呼ばれないことを確認 ->method('discount'); $checkout = new Checkout($discountServiceMock); $checkout->execute(500); } 100円引きの関数(discount)の呼び出ししか確認していないので、 本来失敗しなければいけないテストが通過してしまう 無条件に50円引きのはずだが…
  11. #phpcon #track5 31 テストが仕様変更に追従しない public function test_1000円未満の場合は割引されない () { $discountServiceMock

    = $this->createMock(DiscountService::class); $discountServiceMock ->expects($this->once()) ->method('needsDiscount') // needsDiscountメソッドが1度だけ ->with(500) // 引数500で呼ばれることを確認して ->willReturn(false); // falseを返すようにmockする $discountServiceMock ->expects($this->never()) // discountメソッドが呼ばれないことを確認 ->method('discount'); $checkout = new Checkout($discountServiceMock); $checkout->execute(500); } 100円引きの関数(discount)が呼ばれないことしか確認していないので、 本来失敗しなければいけないテストが通過してしまう 無条件に50円引きのはずだが… バグを正しく検出できない! 時間の都合上触れませんが コードの変更もしづらくなります
  12. #phpcon #track5 32 どうすればいいのか 振る舞いをテストすれば、テストも仕様変更に追従する public function test_1000円未満では割引されない() { $checkout

    = new Checkout(); $this->assertSame(999, $checkout->execute(999)); } public function test_1000円以上で100円割引される() { $checkout = new Checkout(); $this->assertSame(1400, $checkout->execute(1500)); } 最終的な結果(=振る舞い)を確認すれば、バグは正しく検出される 関数を実⾏して得られた結果が、期待する値になっているかを確認する
  13. #phpcon #track5 33 振る舞いをテストできるコードを書こう final class DiscountService { // 割引するかの判定

    public function needsDiscount(int $total): bool { return $total >= self::MINIMUM_DISCOUNT_PRICE; } // 割引処理 public function discount(int $total): int { return $total - self::DISCOUNT_AMOUNT; } } 実装の詳細である 割引判定ロジックが publicメソッド 実装の詳細を公開すると、振る舞いをテストしにくい 割引判定と割引処理を 呼び出し先に委ねている
  14. #phpcon #track5 34 振る舞いをテストできるコードを書こう final class DiscountService { // 割引するかの判定

    private function needsDiscount(int $total): bool { return $total >= self::MINIMUM_DISCOUNT_PRICE; } // 割引処理 public function execute(int $total): int { // 割引判定 if ($this->needsDiscount($total)) { return $total - self::DISCOUNT_AMOUNT; } return $total; } 振る舞いだけを公開すると、振る舞いのテストが書ける! 実装の詳細である 割引判定ロジックが privateメソッド 振る舞いである割引処理の結果だけ publicにして公開する
  15. #phpcon #track5 37 Outline ガイドライン作成を決意するまで 書籍から学んだテストの基本 • なぜテストするのか • 振る舞いをテストするということ

    • 何をどのくらいテストすればいいのか • 認知負荷を下げるための⼯夫 ガイドライン作成のポイント 導⼊後の課題 まとめ‧今後の課題
  16. #phpcon #track5 39 テストには「レベル」がある 単体 統合 E2E (現実的に)実⾏可能なテストケースの数 ⾼速 ⾼決定性

    低速 忠実 レベルごとに⻑所と短所がある 決定性:≒冪等性 何回やっても同じ結果になる性質
  17. #phpcon #track5 40 各テストレベルごとの役割 単体 E2E 特徴 単⼀のプロセスで完結するため、 実⾏時間が短く決定性が⾼い 短所

    テストで実⾏されるコード量が少なく、 忠実性が⼩さい ⻑所 細かいテストケースをたくさんテストできる 正常系に加え異常系をたくさんテストする レベル低のテスト 統合
  18. #phpcon #track5 41 各テストレベルごとの役割 単体 統合 E2E 特徴 DB等のリソースへアクセスを⾏う。 実⾏時間が⻑くコストがかかるが、忠実

    短所 実⾏できるテストケース数が限られてくる ⻑所 テストで実⾏されるコード量が多く、 忠実性が⾼い 実⾏するテストケースを絞り モジュール‧リソース間の疎通に重点を置いたテストにする レベル⾼のテスト
  19. #phpcon #track5 43 コードに紐付けて考える ドメインロジック Service Controller E2E (View) オープンロジの技術スタックである

    Laravel(MVC+Service)にざっくり当てはめる Modelはここに含まれると思いますが 誤解を招かないようドメインロジック としました ⾼速 ⾼決定性 低速 忠実
  20. #phpcon #track5 46 Outline ガイドライン作成を決意するまで 書籍から学んだテストの基本 • なぜテストするのか • 振る舞いをテストするということ

    • 何をどのくらいテストすればいいのか • 認知負荷を下げるための⼯夫 ガイドライン作成のポイント 導⼊後の課題 まとめ‧今後の課題
  21. #phpcon #track5 // NG: 認知負荷の高い例 $this->assertSame(in_array($result, 'value', true), true); //

    OK: 適切なAssertionを使うと読みやすくなる $this->assertContains($result, 'value'); テストが落ちたときの エラーの⽂⾔もわかりやすくなります 47 認知負荷を下げるための⼯夫 関数の命名は検証したいことを具体的に表現する // NG: 検証したい内容が不明瞭 public function test_validation_error() // OK: テストケースをわかりやすく書く public function test_お届け予定日を過去の日付に設定するとエラー () 適切なアサーションを使う ちょっとしたことで可読性は向上する
  22. #phpcon #track5 Redux Style Guide 57 重要度別にPriorityがついている この基になったというVue Style GuideはPriority

    Dまでありました Priority Description A 必須。必ず守ること B 強い推奨。可能な限り守る C 複数の選択肢があった場合のデフォルト
  23. #phpcon #track5 72 参考⽂献⼀覧 書籍 『単体テストの考え⽅/使い⽅』Vladimir Khorikov(著), 須⽥智之(訳), マイナビ出版 『ソフトウェアテストの教科書

    第2版』布施昌弘ほか, SBクリエイティブ 『Good Code, Bad Code』Tom Long(著), 秋勇紀ほか(訳), ⼭本⼤祐(監訳), 秀和システム 『テスト⾃動化実践ガイド』末村 拓也, 翔泳社 『Googleのソフトウェアエンジニアリング』Titus Wintersほか(編), ⽵辺靖昭(監訳), 久富⽊隆⼀(訳), オライリー‧ジャパン Web サバンナ便り 〜ソフトウェア開発の荒野を⽣き抜く〜 https://gihyo.jp/dev/serial/01/savanna-letter/0003 ほか, 最終閲覧 2024/12/20