phpspecで学ぶlondon school tdd
TRANSCRIPT
最初に告知Sapporo.php勉強会やります
7/17(金) 19:00~21:00インフィニットループ会議室
http://sapporophp.connpass.com/
https://github.com/sapporophp/sapporophp
こんな勉強会があったらいいなといったご意見はgithubまで
自己紹介石田朗雄
http://iakio.hatenablog.com/
https://twitter.com/iakio
http://qiita.com/iakio
http://www.slideshare.net/iakio
MotivationPHPの偉い人でもTDDの偉い人でもありません
あと特別英語が得意なわけでもありません
でもphpspecもLondon School TDDも日本ではあまり情報量が多くない(海外のPHPコミュニティでは度々話題になっている模様)
本当はこういう話を自分で話すんじゃなく誰かにしてほしい
ガチ勢から見ると突っ込みどころあるかもしれませんが出来るだけ自分の言葉で説明したいと思います
最後に沢山参考資料へのリンクを付たよ お客様の中でLondon PHPコミュニティの方はいらっしゃいますか
London school TDDロンドンのXPコミュニティが発祥
モックを積極的に使い、状態ではなく振る舞いに注目したTDDの手法
Growing Object-Oriented Software Guided By Tests(2009)通称The GOOS Book
実践テスト駆動開発(2012)
phpspecの作者「London school TDDを学ぶ上でベストなリソース」(Full Stack Radio #15)
ただし結構難しい本◦ GUIあり、ネットワーク通信あり、マルチスレッドあり
◦ 難しい問題を解決する手法を解説しようとすると、例も難しくなるという技術書のジレンマ
個人的な体験としてThe GOOS Bookを読む → 「なるほど(わからん)」
phpspecを知る → 「なるほど(わからん)」
phpspecについて調べる → 「それThe GOOS Bookに書いてあるよ」
The GOOS Bookを読む → 「ホンマや!」
💡 phpspecを学ぶことでLondon School TDDが理解できた(気がする)
phpspecを使えばLondon School TDDを上手く説明できるのでは?
依存関係Bが無いとAを作れない
CとDが無いとBを作れない
どのように単体テストを行うか
class A {function __construct(B $b) {}
}
class B {function __construct(C $c, D $d) {}
}
class C {}
class D {}
Outside-In/Inside-Outclass A {
function __construct(B $b) {}
}
class B {function __construct(C $c, D $d) {}
}
class C {}
class D {}
Outside-In◦ Bをダミー(Mock, Stub)に置き換えてAをテストする
◦ AがBに要求しているものが明確になる
Inside-Out◦ C,Dを先に実装してからBを実装する
◦ テストの粒度は次第に大きくなる
◦ ダミーを実装する手間はかからない
◦ Bを実装中はAが存在しないので、AがBに何を要求するかわからない
モックを積極的に使うあるメソッドを呼び出したとき、最終的には値を返すか他のオブジェクトのメソッドを呼び出しているはず
テスト対象のオブジェクトと、関連するオブジェクトの間のやり取りだけ確認できれば、テスト対象のオブジェクト内で起きていることは気にする必要は無いのでは?(プライベートメソッド、プライベート変数)
もう全部モックでいいんじゃないかな
MockMock
Mock Mock
phpspec大雑把にいえばPHPUnitのようなもの(だいぶ違う)
大雑把にいえばRSpecのようなもの(だいぶ違う)
phpspec(ver1)は2011年ごろからあった(RSpecを参考にしたものだったらしい)
2013年ごろ0から書き直され、2014年にver2.0.0がリリースされる
現在の最新は2.2.1
2013年以前のphpspecに関する情報は全く別のものだと思ってください
Spec, Example
class MarkdownSpec extends ObjectBehavior{
}
function it_is_initializable(){
$this->shouldHaveType('Markdown');}
function its_xxxxxx(){
....}
そのオブジェクトをどのように使うかという例を集めたものがSpecとな
る
{
"require-dev": {
"phpspec/phpspec": "~2.2"
},
"autoload": {
"psr-4": {
"": "src/"
}
}
}標準構成ではソースをsrc/、スペックをspec/に配置します。それ以外の場合や.phpspec.ymlで
設定が必要です
PSR-0でも問題ありません
composer.json
Getting StartedMarkdownをHTMLに変換するMarkdownクラスのSpecを例に説明します
といっても"Hi, there"を"<p>Hi, there</p>"に変換するだけ
公式ドキュメントと同じサンプルですいません
C:¥Users¥ishida¥src¥phpspec>vendor¥bin¥phpspec desc Markdown
Specification for Markdown created in C:¥Users¥ishida¥src¥phpspec¥spec¥MarkdownSpec.php.
MarkdownクラスのSpecのひな形を作成しますphpspecコマンドを使わずに作ってもよいです
<?php
namespace spec;
use PhpSpec¥ObjectBehavior;
use Prophecy¥Argument;
class MarkdownSpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shouldHaveType('Markdown');
}
}
spec/MarkdownSpec.php
MarkdownクラスのSpecはMarkdownSpecとなります。各exampleメソッドはit_又はits_から始ま
ります
C:¥Users¥ishida¥src¥phpspec>vendor¥bin¥phpspec runMarkdown10 - it is initializable
class Markdown does not exist.
/ skipped: 0% / pending: 0% / passed: 0% / failed: 0% / broken: 100%/ 1 examples1 specs1 example (1 broken)24ms
Do you want me to create `Markdown` for you?[Y/n]
Class Markdown created in C:¥Users¥ishida¥src¥phpspec¥src¥Markdown.php.
/ skipped: 0% / pending: 0% / passed: 100% / failed: 0% / broken: 0%/ 1 examples1 specs1 example (1 passed)50ms
スペックを実行すると、スペックの対象であるMarkdownクラスが無いので作りますか?と聞
かれます
...
class MarkdownSpec extends ObjectBehavior
{
function it_converts_plain_text_to_paragraph()
{
// $this->toHtml("Hi, there")が
// "<p>Hi, there</p>" を返却すること
}
}
spec/MarkdownSpec.php
...
class MarkdownSpec extends ObjectBehavior
{
function it_converts_plain_text_to_paragraph()
{
$this->toHtml("Hi, there")
->shouldReturn("<p>Hi, there</p>");
}
}
spec/MarkdownSpec.php
shouldBe, shouldBeEqualTo, shouldEqualToとも書けます
C:¥Users¥ishida¥src¥phpspec>vendor¥bin¥phpspec run/ skipped: 0% / pending: 0% / passed: 100% / failed: 0% / broken: 0%Markdown
15 - it converts plain text to paragraphmethod Markdown::toHtml not found.
/ skipped: 0% / pending: 0% / passed: 50% / failed: 0% / broken: 50%/ 2 examples1 specs2 examples (1 passed, 1 broken)32ms
Do you want me to create `Markdown::toHtml()` for you?[Y/n]
Method Markdown::toHtml() has been created.
/ skipped: 0% / pending: 0% / passed: 100% / failed: 0% / broken: 0%Markdown
15 - it converts plain text to paragraphexpected "<p>Hi, there</p>", but got null.
/ skipped: 0% / pending: 0% / passed: 50% / failed: 50% / broken: 0%/ 2 examples1 specs2 examples (1 passed, 1 failed)38ms
toHtml()メソッドがないので作りますか?
C:¥Users¥ishida¥src¥phpspec>vendor¥bin¥phpspec run --fake/ skipped: 0% / pending: 0% / passed: 100% / failed: 0% / broken: 0%Markdown
15 - it converts plain text to paragraphexpected "<p>Hi, there</p>", but got null.
/ skipped: 0% / pending: 0% / passed: 50% / failed: 50% / broken: 0%/ 2 examples1 specs2 examples (1 passed, 1 failed)67ms
Do you want me to make `Markdown::toHtml()` always return '<p>Hi,there</p>' for you?
[Y/n]
Method Markdown::toHtml() has been modified.
/ skipped: 0% / pending: 0% / passed: 100% / failed: 0% / broken: 0%/ 2 examples1 specs2 examples (2 passed)27ms
toHtml()メソッドが常に"<p>Hi, there</p>"を返すようにしますか?
<?php
class Markdown
{
public function toHtml($argument1)
{
return '<p>Hi, there</p>';
}
}
src/Markdown.php
仮実装ができました。
PointMarkdownクラスのSpecはMarkdownSpecクラスに書く
各exampleのメソッド名は、'it_'または'its_'
expectationは$thisに対して書くつまりMarkdownSpecの中で$thisはMarkdownクラスのインスタンスをラップしたオブジェクトとなる
このため、1つのSpecには1つのクラスのテストしか書けない
phpspec descクラス名で、Specのひな形を作る
phpspec run --fakeで、仮実装を作る
スタブファイル等から読み出したMarkdownをHTMLに変換するメソッド
Markdown::toHtmlFomReader(Reader $reader)
$reader->getMarkdown()を呼び出し、取得したMarkdownをHtmlに変換する
指定した値を返却するダミーのReaderオブジェクトを作成する(スタブ)
class MarkdownSpec extends ObjectBehavior
{
...
function it_convers_text_from_an_external_source()
{
// $reader->getMarkdown()が"Hi, there"を返すとする
$this->toHtmlFromReader($reader)
->shouldReturn("<p>Hi, there</p>");
}
spec/MarkdownSpec.php
class MarkdownSpec extends ObjectBehavior
{
...
function it_convers_text_from_an_external_source()
{
$reader->getMarkdown()->willReturn("Hi, there");
$this->toHtmlFromReader($reader)
->shouldReturn("<p>Hi, there</p>");
}
spec/MarkdownSpec.php
use Reader;
class MarkdownSpec extends ObjectBehavior
{
...
function it_convers_text_from_an_external_source(Reader $reader)
{
$reader->getMarkdown()->willReturn("Hi, there");
$this->toHtmlFromReader($reader)
->shouldReturn("<p>Hi, there</p>");
}
spec/MarkdownSpec.php
exampleに仮引数を書くとPhpSpecが$readerを渡してくれる
C:¥Users¥ishida¥src¥phpspec>vendor¥bin¥phpspec run --fake...
Would you like me to generate an interface `Reader` for you?[Y/n]
...Would you like me to generate a method signature`Reader::getMarkdown()` for you?
[Y/n]...
Do you want me to create `Markdown::toHtmlFromReader()` for you?[Y/n]
...
Do you want me to make `Markdown::toHtmlFromReader()` alwaysreturn '<p>Hi, there</p>' for you?
[Y/n]
Method Markdown::toHtmlFromReader() has been modified.
/ skipped: 0% / pending: 0% / passed: 100% / failed: 0% / broken: 0%/ 3 examples1 specs3 examples (3 passed)43ms
Reader::getMarkdown()とMakkdown::toHtmlFromReader()の仮実装の
自動生成
モック変換したHTMLをファイル等に出力するメソッド
Makdown::outputHtml($text, Writer $writer);
生成したHTMLをWriter::writeText($text)メソッドに出力する
指定したメソッドが呼ばれたかを検査可能なダミーのWriterオブジェクトを作成する(モック)
use Writer;
class MarkdownSpec extends ObjectBehavior
{
...
function it_outputs_converted_text(Writer $writer)
{
// $writer->writeText("<p>Hi, there</p>")
// が呼び出されること
$this->outputHtml("Hi, there", $writer);
}
spec/MarkdownSpec.php
use Writer;
class MarkdownSpec extends ObjectBehavior
{
...
function it_outputs_converted_text(Writer $writer)
{
$writer->writeText("<p>Hi, there</p>")
->shouldBeCalled();
$this->outputHtml("Hi, there", $writer);
}
spec/MarkdownSpec.php
できました
class MarkdownTest extends ¥PHPUnit_Framework_TestCase
{
function test_output_converted_text()
{
$writer = $this->getMockBuilder('Writer')
->setMethods(['writeText'])
->getMock();
$writer->expects($this->once())
->method('writeText')
->with($this->equalTo("<p>Hi, there</p>"));
$markdown = new Markdown();
$markdown->outputHtml("Hi, there", $writer);
}
但しPHPUnit4.5以降では、ProphecyというphpspecのMockフレームワークを使うこともで
きます
ちなみにPHPUnitで書くと
コンストラクタWriterをメソッドの引数ではなくコンストラクタの引数として渡し
$markdown = new Markdown();
$markdown->outputHtml($text, $writer); ではなく
$markdown = new Markdown($writer);
$markdown->outputHtml($text); にする
use Writer;
class MarkdownSpec extends ObjectBehavior
{
...
function it_outputs_converted_text(Writer $writer)
{
$writer->writeText("<p>Hi, there</p>")
->shouldBeCalled();
$this->outputHtml("Hi, there", $writer);
}
spec/MarkdownSpec.php
$writerをoutputHtmlの引数ではなく、コンストラクタで渡したい
use Writer;
class MarkdownSpec extends ObjectBehavior
{
...
function let(Writer $writer)
{
$this->beConstructedWith($writer);
}
function it_outputs_converted_text($writer)
{
$writer->writeText("<p>Hi, there</p>")
->shouldBeCalled();
$this->outputHtml("Hi, there");
}
spec/MarkdownSpec.php
letは各exampleの前に実行されるletとexampleの引数には同じ$writerが渡される
(変数名が重要)
Pointスタブのメソッドが値を返すこと
$reader->getMarkdown()->willReturn("Hi, There")
モックのメソッドが呼び出されること
$writer->writeText("<p>Hi, there</p>")->shouldBeCalled()
コンストラクタに引数が渡されること
$this->beConstructedWith($writer)
なぜinterface?class FileStorage implements Reader, Writer {...}
class Markdown {function toHtmlFromReader(Reader $reader) {...}function toHtmlFromReader(FileStorage $reader) {...}
}
このメソッドに最低限必要なものを表せている
列車事故を防ぐ
((EditSaveCustomer) master.getModelisable().getDockablePanel().getCustomizer()).getSaveItem().setEnabled(Boolean.FALSE.booleanValue());
master.allowSaveingOfCustomisations();
Specで設計を導き出せばこのようにはなりづらい
The GOOS Bookに載っている例
サイクルを回し続けるスペック→実装により隣接オブジェクトを設計し、さらに隣接オブジェクトのスペック→実装を行う
が、隣接オブジェクトが既存のもの(ライブラリ、フレームワーク、Webサービス)の場合はどうする?
突然のシステム境界!!
Hexagonal-Architecture自分書かない部分のアダプタを作り、その先は結合テスト等粒度の大きいテストを行う
この時phpspecは使わない(PHPUnit、Behat、Codeceptionなどを使う)
今日話さなかったこと今日はUnit Testの話だけしましたが、本来はAcceptance Testありきです
単体テストをすれば必ず良い設計になるわけではありません。しかし、単体テストは良い設計にするための手助けをしてくれます
phpspecはどんなテストにも有効なわけではありません。例えば学習テストのようなケースではPHPUnitなどを使った方が良いでしょう
まとめLondon school TDDでは、モックを使って、テスト対象と隣接するオブジェクトとのコミュニケーションをテストする
外から中へアプローチすることで、必要なインターフェースを導き出す
phpspecを使うとモックを簡単に作成することができる
モック出来ないものに対してはアダプタを作成する
phpspecは、London school TDDの考え方を上手く表現したツール
phpspecで書かれたThe GOOS Bookみたいな本が出るとうれしい
参考資料モックによるインターフェースの発見http://d.hatena.ne.jp/digitalsoul/20110927/1317079751
テスト駆動開発の進化http://d.hatena.ne.jp/digitalsoul/20120920/1348104079
モックとスタブの違いhttp://d.hatena.ne.jp/devbankh/201002
実践テスト駆動開発(GOOS)読んだhttp://qiita.com/kenjihiranabe/items/b951b6d98672167347fd
http://iakio.hatenablog.com/archive/category/phpspec
http://qiita.com/tags/phpspec
参考資料phpspec2: SUS and collaborators(2012)(http://everzet.com/post/33178339051/sus-collaborators)
phpspec2のalphaリリース時の作者によるコンセプトの解説
Konstantin Kudryashov - Design How Your Objects Talk Through Mocking at Laracon EU 2014(https://www.youtube.com/watch?v=X6y-OyMPqfw)
Laracon EUでの作者によるモックを使った開発手法に関する講演。phpspec自体は使われていない
Does TDD really lead to good design?(http://codurance.com/2015/05/12/does-tdd-lead-to-good-design/)
Detroit schoolとLondon Schoolの比較など
Laracast(https://laracasts.com/index/phpspec)
LaravelやPHPに関するスクリーンキャスト。一部無料