Skip to content

Instantly share code, notes, and snippets.

@denvernine
Last active July 28, 2023 03:20
Show Gist options
  • Save denvernine/5f0f5692d9164ded7497daea276e37c8 to your computer and use it in GitHub Desktop.
Save denvernine/5f0f5692d9164ded7497daea276e37c8 to your computer and use it in GitHub Desktop.
『単体テストの考え方/使い方』を読んだ
original:
author: Vladimir Khorikov
title: Unit Testing Principles, Practices, and Patterns
publisher: Manning
published_on: Jan, 14, 2020
isbn13: 9781617296277
ja:
訳者: 須田智之
: 単体テストの考え方/使い方
出版社: 株式会社マイナビ出版
出版日: 2022年12月28日
: 初版第1刷・電子版ver1.00
isbn13: 9784839981723

第2章

  • 単体テストを「少量のコード(1単位のふるまい)」を「短い実行時間」で「(他のテストケースから)隔離された状態で実行される」ものと定義する
    • 8章の先取りになるが、統合テストは上記の定義に あてはまらないテスト ということになる
  • デトロイト学派は古典主義で、本番コードと同じように依存クラスなどを準備する
    • 共有依存関係であるDBなどはモック化する
  • ロンドン学派はモック主義で、依存クラスはすべてモック化する
    • Enumやconstは不変のためモック化しない
  • pp44-45の図が参考になる

デトロイト学派的隔離: p37

  • テストケース同士を隔離すること
    • 1つのふるまいを隔離する
  • テストケースの干渉を防ぐためにプロセス外依存(DBなど)をモック化する
    • 依存先クラスなどはモック化しないでプロダクションコードをつかう
    • プロセス外依存を モック化しない テストはIT(Integration Test、統合テスト、結合テスト)などより上位のテストで行う
    • ロンドン学派からみると統合テストのようにみえる
  • バイブルは『テスト駆動開発』 12

pros

  • 大きなまとまりとしての動作は検証しやすい

cons

  • 依存関係の解決が大変
    • 依存クラスの依存クラスの依存クラスの……依存クラスを用意しなければならない
      • その場合はそもそもの構造が問題であることもある

デトロイト学派的統合テスト

  • 上述した単体テストの定義から外れるテスト
    • 2つ以上のふるまいを同時にテストする場合
    • 時間がかかるテストの場合

ロンドン学派的隔離: p29

  • テストケースと依存先を隔離すること
    • 1つのクラスを隔離する
  • 隔離のためにモックを多用する
    • たとえば、依存先のクラスはすべてモック化する
  • バイブルは『実践テスト駆動開発』 34

pros

  • 1クラス1テストなので問題個所の発見が簡単になる
    • 1テスト≠1テストケース
  • 依存関係の解決が(比較的)簡単
    • 全部モック化するので

cons

  • テストコード量が増える
  • プロダクションコードとの結びつきが強い
    • モック化するので、振る舞いの再現のためにコードの詳細を知る必要がある
      • プロダクションコードの依存先が変わるとテストコードも修正しなければならない
        • テストコードの書き方によってはデトロイト学派でも同じな気もする
  • モック化することで保守性が下がる
    • プロダクションコードに変更があってもモックは変更されないので、既存コードのテストは通ってしまう
      • テストコードの更新忘れがあっても気づくのが遅れたりする
    • テストコード→プロダクションコードの順に修正すればそれで解決する?

ロンドン学派的統合テスト

  • 依存先クラスをモック化せずにするテスト

感想など

著者的にはデトロイト推し?デトロイト学派スタイルの利点が詳細に書いてあるのに対して、ロンドン学派スタイルは問題点や課題のほうが詳細に書いてある印象を受ける。

Footnotes

  1. Kent Beck, Test-Driven Development: By Example, Addison-Wesley Professional, 2002

  2. 和田卓人訳, テスト駆動開発, オーム社, 2017

  3. Steve Freeman and Nat Pryce, Growing Object-Oriented Software, Guided by Tests, Addison-Wesley Professional, 2009

  4. 和智右桂 髙木正弘訳, 実践テスト駆動開発, 翔泳社, 2012

第4章

理想的なテスト: p96

  • A: 退行(regression)に対する保護: p96
    • 退行とはバグのこと
    • なんらかの変更(機能追加など)があっても既存の機能が意図したように動いているほど保護されている
      • テスト時に実行されるプロダクションコードの量を少なくする
      • プロダクションコードの複雑度を小さくする
      • プロダクションコードの扱う重要度を小さくする
  • B: リファクタリングへの耐性: p98
    • テストが失敗することなくプロダクションコードのリファクタリングを行えるか
    • 偽陽性の発生が少ないほど耐性がある
      • 正しく(期待通りに)リファクタリングした結果、テストが失敗するようになることを「偽陽性」と呼ぶ
  • C: 迅速なフィードバック: p113
    • テストを速やかに行えること
  • D: 保守のしやすさ: p113
    • 保守コスト
    • テストケースの理解困難度
    • テストの実行困難度

テストケースの価値: p114

  • テストの価値 = A{0..1} * B{0..1} * C{0..1} * D{0..1}
    • いづれかの要素が0になったとき、そのテストケースの価値は0になってしまう
    • 理想的なテストケースはすべての要素の度合いが1になっていることだが、そのようなテストケースを作ることはできない
      • A, B, Cの3要素は互いに排反であるためである
        • ABを1にするとCは0になってしまう
          • e.g.) E2Eテストだけでは迅速なフィードバックを得ることはできない
        • BCを1にするとAは0になってしまう
          • e.g.) 取るに足らないテスト(プロダクションコードと同じことを別の書き方で表現しているだけのテスト)では何も検証していないも同然で、退行に対する保護は全く備わっていない
        • ACを1にするとBは0になってしまう
          • e.g.) 壊れやすいテスト(プロダクションコードをリファクタリングすると失敗するようになるテスト)にはリファクタリングへの耐性がない
      • p120の図が参考になる

実現可能なテストケースのうち、もっとも理想に近いテストケース: pp120-122

  • B, Dを1とし、A, Cでバランスを調整する
    • 退行に対する保護と迅速なフィードバックのどちらかを多少犠牲にするということ
      • この感覚はデータストアなどで使われる「CAP定理」というものに似ているらしい
    • テスト・ピラミッドにあてはめると、以下のようなバランスとなる
      • 単体テスト: A{.3} * B{1} * C{.7} * D{1}
      • 統合テスト: A{.5} * B{1} * C{.5} * D{1}

CAP定理: p123

  • C, A, Pのうち同時に保証できるのは2つまでだという考え方で、一般的にRDBMSはCA、NoSQL/分散データベースではCPまたはAPを選択する

    • Consistency: 一貫性
    • Availability: 可用性
    • Partition-tolerance: 分断耐性

    CA分散データベースについては、理論上の検討を行うことはできても、実際的には存在しないと言えます。
    -- CAP定理の概要 | IBM

    • DynamoDBはNoSQLらしくAPデータストアになっている

    アマゾンの経験では、ACID(Atomicity(原子性))、Consistency(一貫性)、Isolation(独立性)、Durability(永続性))を保証するデータストアは高可用性が維持できない。これは産業界でも学会でも広く認められていることだ。Dynamoでは高可用性につながるならば、一貫性を多少犠牲にしても運用できるアプリケーションをターゲットにしている
    つまり、いかなる場合でも顧客がショッピングカートに商品を入れられるように、データの一貫性を多少犠牲にしてでも可用性を優先するというアプローチをとっているということになる
    -- クラウドの衝撃: IT史上最大の創造的破壊が始まった 1

ブラックボックステスト・ホワイトボックステスト: pp124-128

  • ホワイトボックステストでは偽陽性が多く含まれ、リファクタリングへの耐性が欠落する。また意味のある振る舞いを認識しづらくなる傾向がある。
    • よってブラックボックステストを選択(採用)するべき
      • リファクタリングへの耐性を高くするために、「実装の詳細ではなく、最終的な結果を確認する」: p106
特徴 退行に対する保護 リファクタリングへの耐性
ブラックボックステスト ソースコードなどの内部構造を知らずに検証する。仕様や要求から作成される 劣っている 優れている
ホワイトボックステスト ソースコードなどの内部構造を検証する。ソースコードから作成される 優れている 劣っている

感想など

「偽陽性」を抑えるためにリファクタリングへの耐性が必要という点は強く共感できる。

偽陽性への対処

  • 偽陽性は「実装は正しい(期待通りである)のに、テストは失敗している」状態
  • 偽陰性は「実装が間違っているが、テストは成功している」状態

ブラックボックステストを採用することでプロダクションコードとテストコードを「隔離」する。2章でも感じた(モックを多用したテストなどで発生しやすいであろう)「偽陰性」にも注意したい。

偽陽性についての文量が多く、コラムにも偽陽性によって起きた問題が書かれている。偽陽性の強い(?)プロジェクトではテストのことを考えなくなっていくという旨のことが書いてあるが、これは『レガシーコード改善ガイド』の「第24章 もうウンザリです。なにも改善できません」という部分にも通ずるところがあるのかもしれない。 23

Footnotes

  1. 城田真琴, クラウドの衝撃: IT史上最大の創造的破壊が始まった, 東洋経済新報社, 2009

  2. Michael C. Feathers, Working Effectively with Legacy Code, Prentice Hall Professional Technical Reference, 2004

  3. ウルシステムズ株式会社 平澤章 越智典子 稲葉信之 田村友彦 小堀真義 訳, レガシーコード改善ガイド, 翔泳社, 2009

第5章

モックとは

大分類 中分類 小分類 備考
テストダブル モック モック モックフレームワークが提供する
テストダブル モック スパイ 開発者自身が実装するモック
テストダブル スタブ スタブ 設定によって返す結果を変えられるダミー
テストダブル スタブ ダミー null, false などのハードコード値
テストダブル スタブ フェイク まだ存在しない依存を置き換える
  • モックは外部に向かうコミュニケーション(出力)を模倣/検証する場合に使う
    • 外部に向かうコミュニケーションとは

      テスト対象システムが依存に対してその依存の状態を変えるために行う呼び出し

  • スタブは内部に向かうコミュニケーション(入力)を模倣する場合に使う
    • 内部に向かうコミュニケーションとは

      テスト対象システムが依存からデータを取得するために行う呼び出し

    • スタブとのやり取りは 検証してはならない
      • テストデータを提供しているだけなので
  • メール送信システムで例えると、
    • テスト対象システム -- メール送信 -> モックメールサービス
    • テスト対象システム <- メール取得 -- スタブデータベース

スタブはテストを壊れやすくする

スタブは実装の詳細を知っているので。

  • CQS(Command Query Separation, コマンドクエリ分離)原則いうところの、
    • コマンド -> モック
      • 値を返さないので
      • 註: function (...): void {...} のメソッドはコマンドといえる。値は返さないが、副作用を持ちうる処理( $this->prop = $arg; など)を行っているため
    • クエリ -> スタブ
      • 値を返すだけなので

壊れやすいテストとは

「操作」または「状態」から実装の詳細が漏れ出ているもの。

  • 操作の例は、 User.name = User.normalizeName('aaaa......aaaa') のようなメソッドを呼び出さなければいけない。など
    • この場合 User.name = 'aaaa......aaaa' だけで完結するようにするべきで、normalizeや不変条件に関する処理は隠蔽するべき
    • 判断の指針として、「テスト時に何回メソッドを呼び出しているか」が(不正確ながらも)有用
      • 理想とすべきAPIの設計は、いかなる目標であれ、1つの操作で目標を達成できるようにする

  • 状態の例は、if (Order.status !== 'packing' && (Order.status === 'shipped' || Order.status === 'delivered')) {...} のように、本当に欲しい情報以外のものが外部から参照できる状態になっている、など
    • この場合 Order.isShipped() などに隠蔽するべき
  • 公開する操作と状態は最小限に抑えなくてはならない

デトロイトvsロンドン ふたたび

  • 2章でみたようにモック化する対象や範囲が異なる
    • デトロイトはテストケース間で共有される依存に対してのみテストダブルを使う
    • ロンドンは不変依存(値オブジェクトや列挙型(Enum)などの、値が変わらないもの) 以外すべて の依存に対してテストダブルを使う
      • 註: 前章でいうところの「偽陰性」が多発しそうで怖い

ヘキサゴナルアーキテクチャにみるモックすべき対象

たとえばこんなシステムで、「在庫が十分にあるとき商品の購入が成功する」テスト

  1. 顧客(customer)が店(store)から商品(product)を購入(purchase)しようとする。
  2. 店にある商品の数(quantity)が足りている場合、
    1. 店にある商品の数を減らす。
    2. 領収書(receipt)を顧客にメールで送る。
    3. 購入処理が成功したことを呼び出し元に伝える。
  • メールサービス(だけ)をモックにする

    • メール・サービスとのコミュニケーションは常に同じ仕様に従うことが期待されているため

      • 註: IEmailGatewayをモックするから?
    • メールの内容を検証することで購入した商品数が正しいか検証できる
    • storeをモックするとどうなるか
      • 購入に連動して在庫を減算する処理をモックに持たせなければならない
      • 実装の詳細が漏れ出る
  • 購入処理が成功したかどうかはプロダクションコードが返す値を検証すればよい

  • 共有依存(≒プロセス外依存)はモックする

    • 共有依存 ... テストケース間で共有される依存
      • データベースなど
    • プロセス外依存 ... テスト対象のシステムとは別のプロセスでホストされる依存
      • データベース(!)、メールサービス、メッセージパス、など

モックしない対象

テスト対象のアプリケーションから のみ アクセスされるプロセス外依存はモックしなくてよい。なぜならそれはもはやアプリケーションの(実装の)一部なので。

テスト対象のアプリケーションを、プロセス外依存との仲介役として、常にクライアントとプロセス外依存とのあいだに配置することができれば、どのようなクライアントであってもそのプロセス外依存に直接アクセスできないようになります。そうすると、テスト対象のアプリケーションはプロセス外依存との後方互換を維持しなくてもよくなります。なぜなら、テスト対象のアプリケーションはプロセス外依存と共にデプロイできるようになるからです。こうなると、仮に、プロセス外依存とのコミュニケーションの仕様を変えたとしても、クライアントに影響が出ることはありません。そのため、このようなプロセス外依存とのコミュニケーションは実装の詳細として扱えるようになります。
このようなプロセス外依存の例としてよくあるのがテスト対象のアプリケーションからしかアクセスされないデータベースです。もし、データベースに対してテスト対象のアプリケーションを除くすべてのアプリケーションからアクセスされることが決してないのであれば、テスト対象のアプリケーションとデータベースのコミュニケーションに関する仕様を(既存の機能を破綻させない限り)好きなように変えられることになります。つまり、このようなデータベースはクライアントから完全に隠されるため、まったく別のデータ・ストレージに置き換えたとしても、クライアントにその影響が出ることはないのです。

第6章

  • まず単体テストが3種類ある点
    • 意識していなかった
    • 名前を知らなかった
      • 出力値ベーステスト
      • 状態ベーステスト
      • コミュニケーションベーステスト
  • 今までは(おそらく)ドメイン層の単体テストで出力値ベーステスト、インフラ層の単体テストでは出力値+コミュニケーションベーステストをやっていたのかなと思う
    • 呼び出し回数を厳密に管理していたわけではないので違うかも
      • インフラ層ではモックを多用しがちではある
  • p185: 副作用をビジネス・オペレーションの最初や最後に持っていく
    • どうやって?
  • unix哲学的なところに通ずるものがあるかも

メソッド・シグネチャ: e.g.) public decimal CalculateDiscount (Product[] products): p181

隠れた入力と出力の種類には、次のものがあります。: p183

  • 副作用 ― 副作用とは、メソッド・シグネチャには表現されていない出力のことである。つまり、副作用は 隠れた出力 を意味する。副作用を起こす操作の例として、オブジェクトの状態を変更することやディスク上のファイルを更新することなどがある。
  • 例外 ― メソッドが例外をスローすることはメソッド・シグネチャで確立された契約を無視したプログラムの流れが作られることを意味する。そして、このスローされた例外は何層にもなった呼び出しの中のどこかでキャッチされることになる。つまり、例外はメソッド・シグネチャには定義されていない 隠れた出力 を意味することになる。
  • 内部もしくは外部の状態への参照 ― メソッドの中には、その内部でDateTime.Nowのような静的(static)なプロパティを介してその時点での日時を取得したり、データベースからデータを取得したり、プライベートな可変のフィールドを参照したりすることを行っているものがある。これらの情報の取得や参照はすべてメソッド・シグネチャには定義されていない入力である。つまり、このようなことをするメソッドには 隠れた入力が存在することになる。

第9章

モックよりもスパイ: p316

ここでいう「スパイ」は広く知られているスパイとはことなるかも?ここでは「手書きのモック」のこと

プロダクション・コードを信頼すべきではない: p318

この例では、発せられるメッセージ自体を検証しようとしているため、「メッセージ自体が変わるとテストは失敗するようにな」るのが想定されて挙動のように見える(つまりスパイが正しく機能している)。 モックではメソッドの呼び出しの確認にとどまるため、テストとして不十分(プロダクション・コードを信頼しすぎている)という主張と理解した。

モックの対象になる型は自身のプロジェクトが所有する型のみにする: p322

  • ライブラリのラッパーアダプタをモックする = プロジェクトが所有する型
    • ライブラリのラッパーは薄皮一枚でほとんど意味がないのでは?
    • ライブラリを使う意味がなくなるのでは?

IMessageBus(p312)、IBus(p316)について

  • IMessageBusはどのようなメッセージを送るかをドメイン特有の用語をつかって規定している(SendEmailChangedMessage)
  • IBusはライブラリ利用に関する複雑さ(接続時のクレデンシャルの使用、必須リクエストヘッダーの追加など)を隠すライブラリラッパーインターフェース

このとき、アプリケーションの境界に位置するのはIBusなのでIBusをモックする(p313図9.1)

モックは統合テストに限定する: p320

バリバリの古典/デトロイト学派としての意見。管理外の依存のみモックするという主張からは当然の意見でもある。

モックを使うのはコントローラを検証するとき、つまり、統合テストを行うときだけとなる。言い換えると、単体テストでは、モックを使ってはならない。 -- p324

モックはアプリケーションの境界に限りなく近い部分のみにする -- p311-

モックの呼び出し回数を常に確認する -- p321

想定している呼び出しが行われていること、および、想定していない呼び出しが行われていないこと の両方を確認しなくてはならない。 -- p325

「呼び出し回数を確認するテストをやらない」派だが、この本のベストプラクティスから外れていた(むしろアンチパターン)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment