LoginSignup
0
0

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

Posted at

この記事について

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

簡単な計算機の例

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

2つの実装パターン

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

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

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

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

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

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

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

1つのクラスで実装した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パターンを適用したケースについて見てみましょう。こちらも1対1にする慣例に従って書いています。

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クラスのテストを記載します。

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));
    }
}

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

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

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

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

トレードオフを整理する

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

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

まとめ

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

参考

Clean Craftsmanship 規律、基準、倫理』 p.154 テスト設計

単体テストの考え方/使い方』 p.96 良い単体テストを構成する4本の柱

  1. 『単体テストの考え方/使い方』

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0