1つのSubjectに集中せよ - phpspecのコンセプト

1つ前のエントリで「知らないうちに」と書きましたが、実際は2012年からphpspec2という名前で存在していて、途中でphpspecのリポジトリに統合されたようです。

commit f6b8f26c563cfd07af28a7d4bdcf41a1aee33556
Author: Marcello Duarte <marcello.duarte@gmail.com>
Date:   Mon Apr 29 16:49:28 2013 +0100

    Initial commit

commit 5e277377ae015f9fc226864edfbbb7dc47efea27
Author: Marcello Duarte <marcello.duarte@gmail.com>
Date:   Mon Apr 29 16:37:12 2013 +0100

    Wipe phpspec 1.x

phpspec2と呼ばれていたころのブログ記事で、コンセプトがわかりやすく紹介されていました。サンプルコードは今のAPIとはちょっと違います。

everzet's blog • phpspec2: SUS and collaborators

翻訳する英語力は無いので雰囲気だけ。

  • SUSとは、Subject Under Specificationのこと。1つのSpecificationでは1つのSUSの振る舞いについて記述することが超重要。複数に集中しようとするのは自殺行為だ
  • Specificationとは、これから実装しようとしているオブジェクトがどのように振る舞うべきかの例(example)のリスト
  • phpspec2は、1つのSpecificationに1つのSUTというスタイルを強制する。$thisはSUTをProphetという特殊なオブジェクトでラップしたもの。SUTのProxyをしつつ、Matcherで期待する振る舞いについて記述できたり、未実装のメソッドを呼び出すとfatal errorの代わりに色々(メソッドを生成するか聞いてきたりとか)してくれる。
  • なのでSpecification内で$thisの振る舞いの例を書いていけばよい
  • とはいえ、普通はSUSと協調する他のオブジェクト(collaborators)があるよね。そいつらは全部Mockして、実際の振る舞いについて考えるのは後回しにしよう。それがMockists approachだ
  • SpecBDDは言っている — 1つのオブジェクトに集中せよ。その唯一の方法がMockである — と

Mockists approachのことをLondonTDDとかLondon-styleとかいうらしいですよ。使ってみたいですね。TDD Bootcampとかで隣の人に「キミってLondon-style?」って。

Mockの書き方をPHPUnitと比較してみましょう。コードはPHPUnitのドキュメントのものです

<?php
class SubjectTest extends PHPUnit_Framework_TestCase
{
    public function testObserversAreUpdated()
    {
        $observer = $this->getMock('Observer', array('update'));
        $observer->expects($this->once())
                 ->method('update')
                 ->with($this->equalTo('something'));
        $subject = new Subject('My subject');
        $subject->attach($observer);
        $subject->doSomething();
    }
}

phpspecだとこうなります(説明面倒なのでコンストラクタの引数は省略してますが)

<?php
namespace spec;
use PhpSpec\ObjectBehavior;

class SubjectSpec extends ObjectBehavior
{
    function it_updates_observers(\Observer $observer) {
        $observer->update('something')->shouldBeCalledTimes(1);
        $this->attach($observer);
        $this->doSomething();
    }
}

シンプルですね。SubjectSpecの中では$thisはSubjectのインスタンスのラッパーになるという決まりになっています。また引数があればphpspecが自動的にMockを生成してくれます。

但しPASSするにはObserver#update()の空の実装かインターフェースが必要です。逆に言えば、PASSしたときには、Collaboratorsに何が要求されるかはわかっているので、次はCollaboratorsのどれかをSUSに選んでSpecificationを書きながら空の実装を埋めていけばいいわけです。

最後に注意事項

  • 正直phpspecのドキュメントはイマイチです。phpspec/prophecyは見ておいた方がいいです。他にもいじってみて色々わかったことがあるのでまた書くかも. まー上のコードみたいに常に簡単にかけるわけでもないです
  • 1つのやり方を強制するので、0から何かを作るのにはいいかもしれませんが、既存のレガシーコードと戦うのに向いているかどうかは疑問です