phpspecで学ぶlondon school tdd

49
phpspecで学ぶ London School TDD オープンソースカンファレンス 2015 HOKKAIDO 2015.6.13

Upload: akio-ishida

Post on 27-Jul-2015

1.122 views

Category:

Technology


1 download

TRANSCRIPT

phpspecで学ぶLondon School TDDオープンソースカンファレンス2015 HOKKAIDO

2015.6.13

最初に告知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とな

Example=例

どのように使うのか、という例を書くつもりで

デモここからしばらく自分用のカンペ(あるいはスライド公開用)

{

"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クラスが無いので作りますか?と聞

かれます

<?php

class Markdown

{

}

src/Markdown.php

空のMarkdowクラスができます

...

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)

得られたものMarkdownクラスのSpec

Markdownクラスの実装

Readerインターフェース

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に関するスクリーンキャスト。一部無料