Upload
yuya-takeyama
View
9.965
Download
3
Embed Size (px)
DESCRIPTION
第56回PHP勉強会@関東で PHPUnit について話してきた http://blog.yuyat.jp/archives/1386
Citation preview
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?
ご清聴ありがとうございました