【読書記録】単体テストの考え方/使い方

2024-01-17

Book

Test

情報

感想

単体テストについて体系的に学びたいならこの本が一番

Memo

## About 単体テスト(Unit Test)

### 世間の認識
 「単体テストを書くことは良いことだ」という認識は、開発者の共通認識になった。
しかし、なぜ単体テストを行うのか、どうすれば最大の利益を得られる単体テストがかけるのかについての認識はまだ甘い。

### なぜ、単体テストを行うのか
- プロダクトの成長を持続可能なものにするため。
    - 放っておくと人間が把握できる範囲の散らかりを軽々と超える。
        - > ソフトウェア・エントロピーの増加
        - > エントロピーとは無秩序の量のこと
- 既存機能の改修時に思わぬところに影響が生じてしまう問題を防ぐ
    - 安心して開発できる
 
### そもそも単体テストって?

#### 定義
下記の条件をすべて満たすもの
- 自動で実行されること
- 「単体(unit)」と呼ばれる少量のコードを検証すること
- 実行時間が短いこと
- 隔離された状態で実行されること

#### 2つの派閥が存在
- 古典学派(デトロイト学派)
    - 「古典学派」という名前の由来は、この学派が単体テストとテスト駆動開発(Test-Driven Development: TDD)の元来の取り組みを採用していることから来ている。
- ロンドン学派(モック主義者)
    - 「ロンドン学派」という名前の由来は、この学派がロンドンのプログラミング・コミュニティで生まれたことから来ている。
 
#### 2つの派閥の違い
1. 隔離の範囲
2. テスト対象となる単体(Unit)の定義
3. テスト・ダブル(モック)に置き換える依存


##### 依存についての説明
依存には、「共有依存・プライベート依存・プロセス外依存」の3種類ある。
共有依存は、DBやファイルなど、テストを実行するときにテスト・ケースごとに共有される依存。(テスト・ケースごとにDBが復数存在していたり、DBを初期化している場合は共有依存にはならない。)
プロセス外依存は、外部APIやDBなどプロセス外のものへの依存。
    - TODO: ライブラリはここに入る?
プライベート依存は、プロセス内の依存。可変依存(Entity)と不変依存(Value object)がある。識別子があるものを可変依存とし、ないものを不変依存とする。


- 古典学派(デトロイト学派)
    1. 隔離の範囲
        - テスト・ケースごと
            - 各テスト・ケースはお互いに影響を与えることなく個別に実行できるようにしなくてはならない。
            - 1単位のコード(a unit of code)を検証するのではなく、1単位の振る舞い(a unit of behavior)を検証
    2. テスト対象となる単体(Unit)の定義
        - 目的を達成するための1つの振る舞い
            - 1つのクラス、またはクラスのグループ
            - 振る舞いを言い換えると、「解決しようとしている物語やユースケース」
    3. テスト・ダブル(モック)に置き換える依存
        - 共有依存
        - プロセス外依存
    4. TDDを用いたシステム設計
        - 内から作り外に向かう
- ロンドン学派(モック主義者)
    1. 隔離の範囲
        - 1つクラスごと
    2. テスト対象となる単体(Unit)の定義
        - 1つのクラス
    3. テスト・ダブル(モック)に置き換える依存
        - 不変依存(value object)を除く全ての依存
            - 共有依存
            - プロセス外依存
            - 共有依存・プロセス外依存・プライベート依存(うち可変依存のみ)

すごくざっくりいうと、ロンドン学派は大量にモックを使うということ

// ECサイトのサンプルコードでどのような違いがあるかをみてみる
// 顧客が商品を購入する際、その店に十分な商品があれば(在庫があれば)、取引を成立させ、それから、その店の商品を購入された分だけ減らす

// 古典学派の例
public function testPurchaseSucceedsWhenEnoughInventory()
{
    // Arrange
    $store = new Store();
    $store->addInventory(Product::SHAMPOO, 10);
    $customer = new Customer();

    // Act
    $success = $customer->purchase($store, Product::SHAMPOO, 5);

    // Assert
    $this->assertTrue($success);
    $this->assertEquals(5, $store->getInventory(Product::SHAMPOO), "店にある商品の数は「5」に減っていること");
}

// ロンドン学派の例
public function purchaseSucceedsWithMockWhenEnoughInventory()
{
    // Arrange
    $storeMock = $this->createMock(IStore::class);
    $storeMock->method('hasEnoughInventory')
              ->willReturn(true);
    $customer = new Customer();

    // Act
    $success = $customer->purchase($storeMock, Product::SHAMPOO, 5);

    // Assert
    $this->assertTrue($success);
    // メソッドが呼ばれること
    $storeMock->expects($this->once())
              ->method('removeInventory')
              ->with(Product::SHAMPOO, 5);
}

- ロンドン学派の利点
    - より細かな粒度で検証できる
    - 依存関係が複雑になってきても簡単にテストすることができる
    - テストが失敗した際、どの機能に問題があったかを正確に見つけられるようになる
- ロンドン学派の課題
    - 検証内容が詳細になりすぎてしまう。

- 統合テスト(integration test)とは、単体テストが持つべき3つの性質を一つでも書いたテストのこと。


## 質の良い単体テスト

### 良い単体テストを構成する4本の柱
- 退行(regression)に対する保護
    - 既存のコードをバグから守る
        - 偽陽性(false negative): テストは全て大丈夫だが退行(バグ)が発生した。テストの信頼性が失われる。
- リファクタリングへの耐性
    - テストを失敗させることなく、どのくらいプロダクション・コードのリファクタリングを行えるのか。
- 迅速なフィードバック
- 保守のしやすさ

#### 「退行(regression)に対する保護」を得るには
- テスト時に実行されるプロダクション・コードの量を増やす
- 複雑なコード・重要性の高いドメインはテストを書く

#### 「リファクタリングへの耐性」を得るには
- 対象の振る舞いのみに焦点を当て、実装の詳細はテストしない。
    - 例: privateメソッド

#### 「迅速なフィードバック」を得るには
- テストの実行が遅くなるDBなどの依存をモックに置き換える

#### 「保守のしやすさ」を得るには
- コードベースの特に重要な部分のみがテスト対象となるよう意識する
- 最小限の保守コストで最大限の価値を生み出すよう意識する

#### 偽陽性と偽陰性
##### 偽陽性(false positive)とは
- 嘘の反応
- 検査結果は陽性(反応した)が、実際は陰性なこと
- ワンピースのウソップが「海賊が攻めてきたぞー!」と叫んだが、実際は海賊はいなかったこと。この場合、ウソップは偽陽性を発した。

##### 偽陰性(false negative)とは
- 無反応
- 検査結果は陰性(反応しなかった)だが、実際は陽性なこと
- ワンピースのウソップが「海賊が攻めてきたぞー!」と叫ばなかったが、実際は海賊がいたこと。この場合、ウソップは偽陰性を発した。

##### 偽陽性があるとどうなるか
- リファクタリングへの耐性が損なわれる
    - 意図通りの振る舞いであるにも関わらずテストが失敗すると、信頼ができないので、リファクタリングができなくなる。

##### 偽陰性があるとどうなるか
- 退行(regression)に対する保護が損なわれる
    - 意図しない振る舞いであるにも関わらずテストが成功すると、信頼ができないので、退行(バグ)が発生しても気づけなくなる。
- テストが失敗しないので、間違いに気づきにくい。

### モックとスタブの違い

#### モック
- 外部に向かうコミュニケーション(出力)を模範し、そして、検証するのに使われる。
- 保存、更新、削除、出力(メール送信)など
#### スタブ
- 内部に向かうコミュニケーション(入力)を模範するのに使われる。
- 取得
- 検証しない

#### 検証対象
モックは副作用が存在し、確実性が必要なため、どのメソッドが何回呼ばれているかを検証する。スタブは検証しない。

![](./img/5-2.png)

#### 単体テストの3つの手法
- 出力値ベース・テスト
    - 戻り値を確認するテスト
    - 最も質の高いテストスイート
- 状態ベース・テスト
    - 状態を確認するテスト
    - 出力ベース・テストの次に室が高い
- コミュニケーション・ベース・テスト
    - オブジェクト間のやり方を確認するテスト

#### テストの可読性を高める
- テストが書きやすい設計にする
- 1テストを1つの振る舞いに留める
    - AAAパターンが一セット(準備(Arrange)、実行(Act)、確認(Assert))
    - Actは1つのみにする
- テストのメソッドをわかりやすくする
- ドメイン・エキスパートやビジネス・アナリストにどのような検証をするのかが伝わるような名前にする
    - `{テスト対象メソッド}_何を検証しているのか

お気に入り度

⭐️⭐️⭐️⭐️⭐️