はむはむエンジニアぶろぐ

365日エンジニアリング

頻出して使うテスト用のインスタンスの生成に関する話

この記事はオブジェクト指向 Advent Calendar 2015 - Qiita 1日目の記事です。
担当は@secret_hamuhamu です。

今回は、「Object Motherパターン」と「Test Data Builderパターン」についてまとめました。
これら、2つのパターンはテスト時に用いるパターンです。
異なるテストケースで、頻出して使うテスト用のインスタンスの生成に関するパターンです。

コードはPHPで書きました。

環境構成

  • PHP 5.6.13


概要

例えば、このようなバリュー・オブジェクトがあるとします。

<?php

class Human
{
    private $firstName;
    private $lastName;
    private $age;
    private $sex;
    private $address;

    public function __construct($firstName, $lastName, $age, $sex, $address)
    {
        $this->firstName = $firstName;
        $this->lastName  = $lastName;
        $this->age       = $age;
        $this->sex       = $sex;
        $this->address   = $address;
    }

    public function __get($propertyName)
    {
        return $this->$propertyName;
    }
}


このHumanクラスのインスタンスを生成しようとすると こうなります。
(ワンラインで、書くことも出来ますがわかりやすさのため変数に代入してます)

$firstName = '太郎';
$lastName  = '田中';
$age       = '20';
$sex       = '男';
$address   = '東京都渋谷区';

$human = new Human($firstName, $lastName, $age, $sex, $address);


もしこのHumanオブジェクトを利用した、テストが複数ある場合、毎回このようにテスト用のインスタンスを生成するのは手間ですよね?
さらに、テストで使用するインスタンスが引数が少ししか違わない場合でも、引数は必須なので似たような引数のインスタンスがテストに散らかります。

例えばfirstNameとlastNameとsexだけ異なるオブジェクトがほしい場合。
男のHumanと女のHumanのインスタンスがほしい。

    /** @test */
    public function 男であればOKを返すこと()
    {
        $firstName = '太郎';
        $lastName  = '田中';
        $age       = '20';
        $sex       = '男';
        $address   = '東京都渋谷区';

        $human = new Human($firstName, $lastName, $age, $sex, $address);
        $coupon = new Coupon($human);

        // assertは省略
    }

    /** @test */
    public function 女であればNGを返すこと()
    {
        $firstName = '花子';
        $lastName  = '山田';
        $age       = '20';
        $sex       = '女';
        $address   = '東京都渋谷区';

        $human = new Human($firstName, $lastName, $age, $sex, $address);
        $coupon = new Coupon($human);

        // assertは省略
    }

「Object Motherパターン」と「Test Data Builderパターン」は、これらの重複した引数によるテスト用のインスタンス生成を排除するFactoryパターンです。

Object Motherパターン

Object Motherパターンのサンプルコード。

<?php

class TestHuman
{
    private static $age     = 20;
    private static $address = '東京都渋谷区';

    public static function createMan()
    {
        $firstName = '太郎';
        $lastName  = '田中';
        $sex       = '';

        return new Human($firstName, $lastName, self::$age, $sex, self::$address);
    }

    public static function createWoman()
    {
        $firstName = '花子';
        $lastName  = '山田';
        $sex       = '';

        return new Human($firstName, $lastName, self::$age, $sex, self::$address);
    }
}

// 使い方
$human = TestHuman::createMan();
$human = TestHuman::createWoman();

男のHumanと女のHumanのインスタンスがほしい場合、Factoryメソッドにすることですっきりする。
ただし、Object Motherパターンには、弱点があり様々な組み合わせの引数に対応しようとすると、Object Motherが太っていき散らかってしまう。

例えば、以下の様なパターンのHumanクラスのインスタンスを生成しようとするとキリがなくなってくる。

  • 男かつ未成年かつ住所が大阪
  • 男かつ成年かつ住所が福岡
  • 女かつ未成年かつ住所が大阪
  • 女かつ成年かつ住所が福岡
  • etc

なので、Object Motherパターンは、最大公約数的なよく使われるインスタンス生成のパターンにとどめておくと良い。

Test Data Builderパターン

Test Data Builderパターンは、その名の通りBuilderパターンである。
Object Motherパターンに比べ、柔軟にHumanクラスのインスタンスを生成できる。

Test Data Builderパターンのサンプルコード。

<?php

class HumanBuilder
{
    private $firstName = '太郎';
    private $lastName  = '田中';
    private $age       = '20';
    private $sex       = '';
    private $address   = '東京都渋谷区';

    public function build()
    {
        return new Human($this->firstName, $this->lastName, $this->age, $this->sex, $this->address);
    }

    public function withAge($age)
    {
        $this->age = $age;

        return $this;
    }

    public function withAddress($address)
    {
        $this->address = $address;

        return $this;
    }
}

// 35才で、住所が北海道札幌市
$human = (new HumanBuilder())->
    withAge('35')->
    withAddress('北海道札幌市')->
    build();

// 19才
$human = (new HumanBuilder())->
    withAge('19')->
    build();

Object Motherパターンほど、少ない記述でインスタンスの生成は出来ないが、代わりに柔軟性が高く拡張性も高い。
いわゆるfluent interfaceというやつで、どういうインスタンスを作りたいのか表現力が高まる。


まとめ

Object MotherパターンとTest Data Builderパターンは、どちらがよいというわけではないと思う。
目的に対して一番効率がよい物を選べばよいと思う。

Test Data Builderパターンの方が、拡張しやすいので迷ったらTest Data Builderパターンにするとよいかな。

参考