phpunit でよりよくテストを書くために

Post on 13-Jan-2015

9.951 Views

Category:

Technology

3 Downloads

Preview:

Click to see full reader

DESCRIPTION

第56回PHP勉強会@関東で PHPUnit について話してきた http://blog.yuyat.jp/archives/1386

TRANSCRIPT

PHPUnit でよりよくテストを

書くために@yuya_takeyama

自己紹介•@yuya_takeyama

•LAMP でお仕事

•メタラー (Black Sabbath, 聖飢魔II, Eyehategod)•Openpear でライブラリ公開してます•\Text\Ngram•HTTP_Parallel•Cache_Casual

•テンプレートエンジン•PHP array markup Language

•[] (array) で HTML が書ける

•PHP 5.4 でしか動かない•元ネタは Clojure の Hiccup

Pamlhttps://github.com/yuya-takeyama/php-HTML_Paml

['begin', ['define', 'fib', ['lambda', ['x'], ['if', ['<', ':x', 2], ':x', ['+', ['fib', ['-', ':x', 2]], ['fib', ['-', ':x', 1]]]]]], ['print', 'fib(10) = '], ['println', ['fib', 10]]] => fib(10) = 55

LisPHPhttps://github.com/yuya-takeyama/LisPHP

テストと私•2010/02東京 RubyKaigi 03 で RSpec について教わる

•2010/03 頃PHP の assert() でテストコードを書いてみる

•2010/04 頃PHPUnit を業務で使い始める

•そして今に至る

アジェンダ•PHPUnit 概説•よりよくテストを書くとはどういうことか

•よりよくテストを書くために•書法編

•パターン編

PHPUnit 概説

PHPUnit とは•テスティングフレームワーク•ユニットテストを書く•比較的簡単に書ける•多機能

何故 PHPUnit か

•豊富なドキュメント•日本語訳も充実•豊富な利用実績 (ZF, Symfony2, etc)

class CalculatorTest extends PHPUnit_Framework_TestCase{    public function setUp()    {        $this->calc = new Calculator;    }

    public function test_add_引数の和を返す()    {        $result = $this->calc->add(1, 2);        $this->assertSame(3, $result);    }}

class CalculatorTest extends PHPUnit_Framework_TestCase{    public function setUp()    {        $this->calc = new Calculator;    }

    public function test_add_引数の和を返す()    {        $result = $this->calc->add(1, 2);        $this->assertSame(3, $result);    }}

1

テストに必要な物の用意

class CalculatorTest extends PHPUnit_Framework_TestCase{    public function setUp()    {        $this->calc = new Calculator;    }

    public function test_add_引数の和を返す()    {        $result = $this->calc->add(1, 2);        $this->assertSame(3, $result);    }}

2

テスト対象の実行

class CalculatorTest extends PHPUnit_Framework_TestCase{    public function setUp()    {        $this->calc = new Calculator;    }

    public function test_add_引数の和を返す()    {        $result = $this->calc->add(1, 2);        $this->assertSame(3, $result);    }}

3

実行結果の検証 (アサーション)

よりよくテストを書くとは

どういうことか

何故テストを書くか•ドキュメント•回帰テスト•リファクタリング•設計

ドキュメントとしてのテスト

•Tests as Documentation

•API の一覧•動作するサンプルコード

回帰テストとは•ある修正が新たなバグを生んでいないか

•リグレッションテストとも言う•同じ過ちを繰り返さないため

リファクタリングとは•振る舞いを変えること無く•ソースコードの内部構造を•変更すること•ソースコードの体質改善

テストと設計

•Test Driven Design?•ライブラリは API が 9 割•API のユーザビリティテスト•リファクタリングで継続的改善

テストで設計を考える•テスタビリティ•オブジェクト指向•デザインパターン•リファクタリング

よりよくテストを

書くために

書法編

public function test_isValid_ユーザの状態が正常であればtrue(){    $user = new User;    $user->setName('Yuya');    $user->setUrl('http://yuyat.jp/');    $user->setAge(24);    $this->assertTrue($user->isValid());}

public function test_isValid_ユーザの状態が異常であればfalse(){    $user = new User;    $user->setName('Yuya');    $user->setUrl('http://yuyat.jp/');    $user->setAge(NULL);    $this->assertFalse($user->isValid());}

public function test_isValid_ユーザの状態が正常であればtrue(){    $user = new User;    $user->setName('Yuya');    $user->setUrl('http://yuyat.jp/');    $user->setAge(24);    $this->assertTrue($user->isValid());}

public function test_isValid_ユーザの状態が異常であればfalse(){    $user = new User;    $user->setName('Yuya');    $user->setUrl('http://yuyat.jp/');    $user->setAge(NULL);    $this->assertFalse($user->isValid());}

どこがどう違うのかわかりづらい

public function test_isValid_ユーザの状態が正常であればtrue(){    $user = $this->createValidUser();    $this->assertTrue($user->isValid());}

public function test_isValid_ユーザの状態が異常であればfalse(){    $user = $this->createValidUser();    $user->setAge(NULL);    $this->assertFalse($user->isValid());}

public function test_isValid_ユーザの状態が正常であればtrue(){    $user = $this->createValidUser();    $this->assertTrue($user->isValid());}

public function test_isValid_ユーザの状態が異常であればfalse(){    $user = $this->createValidUser();    $user->setAge(NULL);    $this->assertFalse($user->isValid());}

ここが違う

public function test_isValid_ユーザの状態が正常であればtrue(){    $user = $this->createValidUser();    $this->assertTrue($user->isValid());}

public function test_isValid_ユーザの状態が異常であればfalse(){    $user = $this->createValidUser();    $user->setAge(NULL);    $this->assertFalse($user->isValid());}

ここが違う

何が影響して結果に差が生まれたのかが明確

ヘルパーメソッドを使う•複雑なオブジェクトの生成•複雑な依存オブジェクトの生成•長くても説明的で明確な名前•エッジケースに注視しやすく

public function test_isValid_ユーザの状態が異常であればfalse(){    $user = $this->createValidUser();    $user->setAge(NULL);    $this->assertFalse($user->isValid());

    $user = $this->createValidUser();    $user->setName(NULL);    $this->assertFalse($user->isValid());}

public function test_isValid_ユーザの状態が異常であればfalse(){    $user = $this->createValidUser();    $user->setAge(NULL);    $this->assertFalse($user->isValid());

    $user = $this->createValidUser();    $user->setName(NULL);    $this->assertFalse($user->isValid());}

ひとつのテスト内で複数のアサーション

public function test_isValid_ユーザの状態が異常であればfalse(){    $user = $this->createValidUser();    $user->setAge(NULL);    $this->assertFalse($user->isValid());

    $user = $this->createValidUser();    $user->setName(NULL);    $this->assertFalse($user->isValid());}

ひとつのテスト内で複数のアサーション

何をテストしたいのかわかりづらい

public function test_isValid_ユーザの年齢がNullであればfalse(){    $user = $this->createValidUser();    $user->setAge(NULL);    $this->assertFalse($user->isValid());}

public function test_isValid_ユーザの名前がNullであればfalse(){    $user = $this->createValidUser();    $user->setName(NULL);    $this->assertFalse($user->isValid());}

public function test_isValid_ユーザの年齢がNullであればfalse(){    $user = $this->createValidUser();    $user->setAge(NULL);    $this->assertFalse($user->isValid());}

public function test_isValid_ユーザの名前がNullであればfalse(){    $user = $this->createValidUser();    $user->setName(NULL);    $this->assertFalse($user->isValid());} テストメソッドの名前も

より具体的に

テストメソッドを分割する

•何のテストなのかを明確に•エッジケースを意識する•1 テスト 1 アサーション

書法編 まとめ•Tests as Documentation

•何をテストしているのか明確に•ヘルパーメソッドの利用•テストメソッドの命名

パターン編

class Request{    public function isSsl()    {        return $_SERVER['HTTPS'] === 'on';    }}

class Request{    public function isSsl()    {        return $_SERVER['HTTPS'] === 'on';    }}

スーパーグローバル変数に依存している

public function test_isSsl_HTTPSであればtrue(){    $_SERVER['HTTPS'] = 'on';    $this->assertTrue($this->request->isSsl());}

public function test_isSsl_HTTPSでなければfalse(){    unset($_SERVER['HTTPS']);    $this->assertFalse($this->request->isSsl());}

public function test_isSsl_HTTPSであればtrue(){    $_SERVER['HTTPS'] = 'on';    $this->assertTrue($this->request->isSsl());}

public function test_isSsl_HTTPSでなければfalse(){    unset($_SERVER['HTTPS']);    $this->assertFalse($this->request->isSsl());}

グローバル変数を無理矢理書き換え

public function test_isSsl_HTTPSであればtrue(){    $_SERVER['HTTPS'] = 'on';    $this->assertTrue($this->request->isSsl());}

public function test_isSsl_HTTPSでなければfalse(){    unset($_SERVER['HTTPS']);    $this->assertFalse($this->request->isSsl());}

グローバル変数を無理矢理書き換え

他のテストの影響を考慮する必要性

public function test_isSsl_HTTPSであればtrue(){    $_SERVER['HTTPS'] = 'on';    $this->assertTrue($this->request->isSsl());}

public function test_isSsl_HTTPSでなければfalse(){    unset($_SERVER['HTTPS']);    $this->assertFalse($this->request->isSsl());}

グローバル変数を無理矢理書き換え

他のテストの影響を考慮する必要性

複数のテストが影響し得る≒ テストが壊れやすい

class Request{ public function __construct($server) {     $this->_server = $server; }

public function isSsl() {     return $this->_server === 'on'; }}

class Request{ public function __construct($server) {     $this->_server = $server; }

public function isSsl() {     return $this->_server === 'on'; }}

サーバ変数を外から差し込んでいる

public function test_isSsl_HTTPSであればtrue(){    $request = new Request(array('HTTPS' => 'on'));    $this->assertTrue($request->isSsl());}

public function test_isSsl_HTTPSでなければfalse(){    $request = new Request(array());    $this->assertFalse($request->isSsl());}

public function test_isSsl_HTTPSであればtrue(){    $request = new Request(array('HTTPS' => 'on'));    $this->assertTrue($request->isSsl());}

public function test_isSsl_HTTPSでなければfalse(){    $request = new Request(array());    $this->assertFalse($request->isSsl());}

複数のテスト間の影響が排除された!

依存性は外から差し込む•Dependency Injection•差し込める ≒ 差し替えられる•再利用性の高い設計•テスタビリティを意識すると自然とそうなる

外部への依存を避ける•コンテキストを明確に•グローバル変数•ファイルシステム•ネットワーク/データベース

class User{    protected $_name;    protected $_birthday;

    public function setName($name) { /*~*/ }    public function getName() { /*~*/ }    public function getAge() { /*~*/ }

    public static function findByName($name) { /*~*/ }    public function save() { /*~*/ }}

ActiveRecord

$user = User::findByName('Bob');

$user->setName('Alice');

$user->save();

ActiveRecord

class User{    protected $_name;    protected $_birthday;

    public function setName($name) { /*~*/ }    public function getName() { /*~*/ }    public function getAge() { /*~*/ }}

class UserMapper{    public function findByName($name) { /*~*/ }    public function save(User $user) { /*~*/ }}

DataMapper

$mapper = new UserMapper;

$user = $mapper->findByName('Bob');

$user->setName('Alice');

$mapper->save($user);

DataMapper

class User{    protected $_name;    protected $_birthday;

    public function setName($name) { /*~*/ }    public function getName() { /*~*/ }    public function getAge() { /*~*/ }

    public static function findByName($name) { /*~*/ }    public function save() { /*~*/ }}

ActiveRecord

class User{    protected $_name;    protected $_birthday;

    public function setName($name) { /*~*/ }    public function getName() { /*~*/ }    public function getAge() { /*~*/ }

    public static function findByName($name) { /*~*/ }    public function save() { /*~*/ }}

ActiveRecord

ビジネスロジック

class User{    protected $_name;    protected $_birthday;

    public function setName($name) { /*~*/ }    public function getName() { /*~*/ }    public function getAge() { /*~*/ }

    public static function findByName($name) { /*~*/ }    public function save() { /*~*/ }}

ActiveRecord

ビジネスロジック

データアクセス

class User{    protected $_name;    protected $_birthday;

    public function setName($name) { /*~*/ }    public function getName() { /*~*/ }    public function getAge() { /*~*/ }

    public static function findByName($name) { /*~*/ }    public function save() { /*~*/ }}

ActiveRecord

ビジネスロジック

データアクセス

異なる関心事が混交している

class User{    protected $_name;    protected $_birthday;

    public function setName($name) { /*~*/ }    public function getName() { /*~*/ }    public function getAge() { /*~*/ }}

class UserMapper{    public function findByName($name) { /*~*/ }    public function save(User $user) { /*~*/ }}

DataMapper

class User{    protected $_name;    protected $_birthday;

    public function setName($name) { /*~*/ }    public function getName() { /*~*/ }    public function getAge() { /*~*/ }}

class UserMapper{    public function findByName($name) { /*~*/ }    public function save(User $user) { /*~*/ }}

DataMapperビジネスロジック

class User{    protected $_name;    protected $_birthday;

    public function setName($name) { /*~*/ }    public function getName() { /*~*/ }    public function getAge() { /*~*/ }}

class UserMapper{    public function findByName($name) { /*~*/ }    public function save(User $user) { /*~*/ }}

DataMapperビジネスロジック

データアクセス

class User{    protected $_name;    protected $_birthday;

    public function setName($name) { /*~*/ }    public function getName() { /*~*/ }    public function getAge() { /*~*/ }}

class UserMapper{    public function findByName($name) { /*~*/ }    public function save(User $user) { /*~*/ }}

DataMapperビジネスロジック

データアクセス

異なる関心事が分離されている

class Config{    protected static $_instance;

    private function __construct() {}

    public static function getInstance()    {        if (empty(self::$_instance)) {            self::$_instance = new self;        }        return self::$_instance;    }}

public function __clone(){    throw new BadMethodCallException( 'Clone is not allowed.' );}

public function test_get_セットされた値を取得する(){    $config = Config::getInstance();    $config->set('message', 'Hello');    $this->assertSame( 'Hello', $config->get('message') );}

public function test_get_値がセットされていなければNull(){    $config = Config::getInstance();    $this->assertNull( $config->get('message') );}

public function test_get_値がセットされていなければNull(){    $config = Config::getInstance();    $this->assertNull( $config->get('message') );}

他のテストで値がセットされている可能性

public function test_get_値がセットされていなければNull(){    $config = Config::getInstance();    $config->init();    $this->assertNull( $config->get('message') );}

public function test_get_値がセットされていなければNull(){    $config = Config::getInstance();    $config->init();    $this->assertNull( $config->get('message') );}

状態の初期化

public function test_get_値がセットされていなければNull(){    $config = Config::getInstance();    $config->init();    $this->assertNull( $config->get('message') );}

状態の初期化

一応の解決にはなるが...

Singleton を避ける

•Singleton ≒ グローバル変数•状態を元に戻しにくい•テストが書きにくい

パターン編 まとめ•依存性は外から差し込む•外部への依存を避ける•テスタブルな単位にクラス分割•Singleton は避ける

まとめ

•可読性に留意してテストを書く•まずはパターンを身につける•テストで設計を考える

参考資料 (書籍)• xUnit Test Patterns: Refactoring Test Code

http://www.amazon.com/xUnit-Test-Patterns-Refactoring-Code/dp/0131495054

• Real-World Solutions for Developing High-Quality PHP Frameworks and Applicationshttp://www.amazon.com/Real-World-Developing-High-Quality-Frameworks-Applications/dp/0470872497

• Patterns of Enterprise Application Architecturehttp://www.amazon.com/Patterns-Enterprise-Application-Architecture-Martin/dp/0321127420

• PHPによるデザインパターン入門http://d.hatena.ne.jp/shimooka/20100301/1267436385

参考資料 (スライド)• Dependency Injection with PHP 5. 3

http://www.slideshare.net/fabpot/dependency-injection-with-php-53

• BEAR DIhttp://www.slideshare.net/akihito.koriyama/bear-di

• Clean PHPhttp://www.slideshare.net/sebastian_bergmann/clean-php-dpc11

• Singletons in PHP - Why they are bad and how you can eliminate them from your applicationshttp://www.slideshare.net/go_oh/singletons-in-php-why-they-are-bad-and-how-you-can-eliminate-them-from-your-applications

Questions?

ご清聴ありがとうございました

top related