単体テストにおけるテストの対応関係について考える

## この記事について

単体テストにおいてテスト対象のクラスとテストクラスの関係は基本的に１対１にすることが多いと思いますが、その対応関係とトレードオフについての個人的な考察です。（考察というよりもメモに近いかもしれません）

## 簡単な計算機の例

足し算、引き算、掛け算を行うことができる計算機を例に２つのパターンの実装を考えてみます。
言語はPHPを想定しています。

### ２つの実装パターン

ひとつのパターンは、１つのクラスの中にプライベートメソッドとしてそれぞれの処理を持つ例です。

```mermaid
classDiagram
    class Calculator {
        + calculation(Operation, int, int) int
        - plus(int, int) int
        - minus(int, int) int
        - times(int, int) int
    }
```

ひとつのパターンは、Strategyパターンを適用した例です。

```mermaid
classDiagram
    class Calculator {
        - Strategy[] strategies
        + calculation(Operation, int, int) int
    }
    class Strategy {
        <<interface>>
        + calculation(int, int) int
    }
    class PlusStrategy {
        + calculation(int, int) int
    }
    class MinusStrategy {
        + calculation(int, int) int
    }
    class TimesStrategy {
        + calculation(int, int) int
    }
    PlusStrategy --|> Strategy
    MinusStrategy --|> Strategy
    TimesStrategy --|> Strategy
    Calculator o--> Strategy
```

この２つは実装の方法は違えど、`Calculator`に期待される振る舞いは同じです。

ちなみに`Operation`は列挙型です。

```php
enum Operation: string{
    case Plus = '+';
    case Minus = '-';
    case Times = '*';
}
```

### 実装パターンに対するユニットテスト

まずは１つのクラスで実装したパターンに対するテストです。`Calculator`クラスに１対１で対応する`CalculatorTest`クラスにテストを書いています。

```php:１つのクラスで実装したCalculatorクラスに対するテスト
class CalculatorTest extends TestCase
{
    private Calculator $sut;

    protected function setUp(): void
    {
        $this->sut = new Calculator();
    }

    public function testPlus(): void
    {
        $this->assertSame(3, $this->sut->calculation(Operation::Plus, 1, 2));
        $this->assertSame(7, $this->sut->calculation(Operation::Plus, 3, 4));
    }

    public function testMinus(): void
    {
        $this->assertSame(1, $this->sut->calculation(Operation::Minus, 3, 2));
        $this->assertSame(-3, $this->sut->calculation(Operation::Minus, 7, 10));
    }

    public function testTimes(): void
    {
        $this->assertSame(6, $this->sut->calculation(Operation::Times, 2, 3));
        $this->assertSame(21, $this->sut->calculation(Operation::Times, 3, 7));
    }
}
```

こちらのパターンのテストについて特に異論は無いと思います。（細かいテストケースの妥当性についてはご容赦ください）

次にStrategyパターンを適用したケースについて見てみましょう。こちらも１対１にする慣例に従って書いています。

```php:Strategyパターンを適用したCalculatorクラスに対するテスト
class CalculatorTest extends TestCase
{
    private Calculator $sut;

    protected function setUp(): void
    {
        $this->sut = new Calculator();
    }

    public function testCalculation(): void
    {
        $this->assertSame(3, $this->sut->calculation(Operation::Plus, 1, 2));
        $this->assertSame(1, $this->sut->calculation(Operation::Minus, 3, 2));
        $this->assertSame(6, $this->sut->calculation(Operation::Times, 2, 3));
    }
}
```

それぞれのStrategyクラスにもテストを書きます。例として`PlusStrategy`クラスのテストを記載します。

```php:PlusStrategyクラスに対するテスト
class PlusStrategyTest extends TestCase
{
    public function testCalculation(): void
    {
        $sut = new PlusStrategy();
        $this->assertSame(3, $sut->calculation(1, 2));
        $this->assertSame(7, $sut->calculation(3, 4));
    }
}
```

１対１に従ってテストを書くとこの様になりました。`CalculatorTest`クラスでは３つ演算ができるかテストし、３つのStrategyクラスではそれぞれの演算についての詳細をテストしています。

このように実装によってテストが変わってしまっていますが、ここで期待される振る舞いは同じという点に注目します。

１つのクラスで実装したパターンからStrategyパターンを適用したとすれば、振る舞いは変わらないのでリファクタリングです。リファクタリングであればテストはそのままで、リファクタリングの後も引き続きテストが通ることを確認したいはずです。例えばStrategyパターンを適用してから、また元に戻したり、あるいは別のパターンを適用してリファクタリングしたとすれば、テストが壊れてしまうことになります。

これはprivateメソッドをテストしているのと同じような状態であり、実装の詳細に踏み込んだテストであると考えます。そして、それによって「リファクタリングへの耐性[^1]」が低下した状態になっています。
[^1]:『単体テストの考え方/使い方』

## トレードオフを整理する

さて、先程の例では「１対１の対応関係にこだわるとリファクタリングへの耐性が低下し、テストが壊れやすくなる」という点が出てきました。

では先程のようにStrategyパターンのそれぞれのクラスに対してテストを書くのは避けるべきでしょうか？これもそうとは言えないかもしれません。先程のコードはとてもシンプルな例ですが、より複雑になったケースではどうでしょうか？振る舞いを提供するクラスのテストが膨大になるかもしれませんし、詳細に対してテストを書いていきたいかもしれません。（PHP言語はテストの構造化が難しい・・・）
このあたりは引き続き考察していきたいと思います。

## まとめ

はっきりとした結論には至りませんでしたが、少なくとも、慣例に従い１対１でテストを書く前にリファクタリングへの耐性（テストの壊れやすさ）について考える必要がありそうです。

## 参考

『[Clean Craftsmanship 規律、基準、倫理](https://www.kadokawa.co.jp/product/302206001224/)』 p.154 テスト設計


『[単体テストの考え方/使い方](https://book.mynavi.jp/ec/products/detail/id=134252)』 p.96 良い単体テストを構成する４本の柱