今回はダミーのオブジェクト
モックオブジェクトを使ったテスト
さて、
<?php
interface Item {
public function getName();
public function getCode();
public function getPrice();
}
しかし、
幸い、
次に、
PHPUnit3のモックオブジェクト生成機能
PHPUnitはPHPのリフレクション機能を使ってモックオブジェクトを生成します。また、
それでは、
モックオブジェクトの生成は、
object getMock($className, [array $methods, [array $arguments, [string $mockClassName]]])
パラメータ | 必須 | 意味 |
---|---|---|
$className | ○ | このクラスのモックオブジェクトを生成する |
array $methods | 生成するメソッド | |
array $arguments | コンストラクタの引数 | |
string $mockClassName | モックオブジェクト自身のクラス名。デフォルトは、 |
最も簡単なコードは以下のようになります。
$mock = $this->getMock('Item');
このコードを実行すると内部では以下のようなコードが生成され、
class Mock_Item_fe459e06 extends Item implements PHPUnit_Framework_MockObject_MockObject {
private $invocationMocker;
public function __construct() {
$this->invocationMocker = new PHPUnit_Framework_MockObject_InvocationMocker($this);
}
public function __clone() {
$this->invocationMocker = clone $this->invocationMocker;
}
public function getInvocationMocker() {
return $this->invocationMocker;
}
public function expects(PHPUnit_Framework_MockObject_Matcher_Invocation $matcher) {
return $this->invocationMocker->expects($matcher);
}
public function verify() {
$this->invocationMocker->verify();
}
}
お気づきの通り、
メソッドを指定してモックオブジェクトを生成する場合、
$mock = $this->getMock('Item', array('getName', 'getCode', 'getPrice'));
内部で生成されるコードは以下のようになり、
class Mock_Item_23571fea extends Item implements PHPUnit_Framework_MockObject_MockObject {
private $invocationMocker;
public function __construct() {
$this->invocationMocker = new PHPUnit_Framework_MockObject_InvocationMocker($this);
}
public function __clone() {
$this->invocationMocker = clone $this->invocationMocker;
}
public function getInvocationMocker() {
return $this->invocationMocker;
}
public function expects(PHPUnit_Framework_MockObject_Matcher_Invocation $matcher) {
return $this->invocationMocker->expects($matcher);
}
public function verify() {
$this->invocationMocker->verify();
}
public function getName() {
$args = func_get_args();
return $this->invocationMocker->invoke(
new PHPUnit_Framework_MockObject_Invocation($this, "Item", "getName", $args)
);
}
public function getCode() {
$args = func_get_args();
return $this->invocationMocker->invoke(
new PHPUnit_Framework_MockObject_Invocation($this, "Item", "getCode", $args)
);
}
public function getPrice() {
$args = func_get_args();
return $this->invocationMocker->invoke(
new PHPUnit_Framework_MockObject_Invocation($this, "Item", "getPrice", $args)
);
}
}
また、
/**
* Item.phpにはItemクラスもしくはItemインターフェースが定義してある
*/
include_once 'Item.php';
$mock = $this->getMock('Item', array(), array(), 'AnotherItem');
この場合、
PHPUnit3のモックオブジェクト機能は、
- 実行回数の制約を設ける
- メソッド名を指定する
- 具体的な振る舞いを記述する
を指定する必要があります。
1つ目の
なお、
また、
- メソッドの戻り値
- メソッドが投げる例外
のいずれかが指定可能で、
以下、
<?php
class MockTest extends PHPUnit_Framework_TestCase
{
public function testMock() {
:
$mock = $this->getMock('Item', array('getName', 'getCode', 'getPrice'));
/**
* getNameメソッドは一度だけ呼び出され、文字列「item001」を返す
*/
$mock->expects($this->once())
->method('getName')
->will($this->returnValue('item001'));
/**
* getCodeメソッドはゼロ回以上呼び出され、1回目の呼び出し時は「001」、
* 2回目の呼び出し時は「002」、3回目の呼び出し時は「100」を返す。
* なお、4回目以降はNULLを返す。
*/
$mock->expects($this->any())
->method('getCode')
->will($this->onConsecutiveCalls('001', '002', '100'));
/**
* getNameメソッドは少なくとも一度は呼び出され、呼び出されると
* RuntimeExceptionを投げる
*/
$mock->expects($this->atLeastOnce())
->method('getPrice')
->will($this->throwException(new RuntimeException()));
:
}
}
また、
以下、
<?php
class MockTest extends PHPUnit_Framework_TestCase
{
public function testMock() {
:
$mock = $this->getMock('Item', array('setCode', 'setData'));
/**
* setCodeメソッドは第1引数にオブジェクト型、第2引数に42より
* 大きな数を受け取る
*/
$mock->expects($this->any())
->method('setCode')
->with('item001'); // ->with($this->equalsTo('item001')) と等価
/**
* setDataメソッドは第1引数に「001」、第2引数にオブジェクト型、
* 第3引数に42より大きな数を受け取る
*/
$mock->expects($this->any())
->method('setData')
->with('001', $this->isType('object'), $this->greaterThan(42));
:
}
}
具体的な制約クラスについては、
CartTestクラスにモックオブジェクトを導入する
それでは、
<?php
interface Item {
public function getName();
public function getCode();
public function getPrice();
}
また、
旧)public function add($item_cd, $amount) 新)public function add(Item $item, $amount)
さらに、
public function getTotal()
まずCartTestクラスから修正していきます。CartTestクラス内でCartクラスのaddメソッドを使用している箇所を見てみると、
<?php
require_once 'PHPUnit/Framework.php';
require_once 'Cart.php';
class CartTest extends PHPUnit_Framework_TestCase
{
:
private function createItem001Stub() {
include_once 'Item.php';
$mock = $this->getMock('Item');
$mock->expects($this->any())
->method('getName')
->will($this->returnValue('item001'));
$mock->expects($this->any())
->method('getCode')
->will($this->returnValue('001'));
$mock->expects($this->any())
->method('getPrice')
->will($this->returnValue(1200));
return $mock;
}
private function createItem002Stub() {
include_once 'Item.php';
$mock = $this->getMock('Item');
$mock->expects($this->any())
->method('getName')
->will($this->returnValue('item002'));
$mock->expects($this->any())
->method('getCode')
->will($this->returnValue('002'));
$mock->expects($this->any())
->method('getPrice')
->will($this->returnValue(2000));
return $mock;
}
private function createItem003Stub() {
include_once 'Item.php';
$mock = $this->getMock('Item');
$mock->expects($this->any())
->method('getName')
->will($this->returnValue('item003'));
$mock->expects($this->any())
->method('getCode')
->will($this->returnValue('003'));
$mock->expects($this->any())
->method('getPrice')
->will($this->returnValue(1500));
return $mock;
}
}
そして、
$this->assertTrue($this->cart->add($this->createItem001Stub(), 1));
また、
- 第1キーとして商品コード
- 第2キーとして文字列
「amount」 もしくは 「object」 - 第2キーが
「amount」 の場合、 値として数量 - 第2キーが
「object」 の場合、 値としてItemオブジェクト
に変更します。具体的には、
$items['item_code_001']['amount']
$items['item_code_001']['object']
これに伴い、
最終的なCartTestクラスは以下のようになりました。
<?php
require_once 'PHPUnit/Framework.php';
require_once 'Cart.php';
class CartTest extends PHPUnit_Framework_TestCase
{
protected $cart;
protected function setUp() {
$this->cart = new Cart();
}
public function testInitCart() {
$this->assertTrue(is_array($this->cart->getItems()));
$this->assertEquals(0, count($this->cart->getItems()));
}
public function testAdd() {
$this->assertTrue($this->cart->add($this->createItem001Stub(), 1));
$this->assertTrue($this->cart->add($this->createItem001Stub(), 0));
$this->assertTrue($this->cart->add($this->createItem001Stub(), -1));
}
public function testAddNotNumeric() {
try {
$this->cart->add($this->createItem001Stub(), 'string');
} catch (UnexpectedValueException $e) {
return;
}
$this->fail();
}
public function testAddFloat() {
try {
$this->cart->add($this->createItem001Stub(), 1.5);
} catch (UnexpectedValueException $e) {
return;
}
$this->fail();
}
public function testGetAmount() {
$this->assertEquals(0, $this->cart->getAmount('001'));
$this->cart->add($this->createItem001Stub(), 1);
$this->assertEquals(1, $this->cart->getAmount('001'));
$this->assertEquals(0, $this->cart->getAmount('999'));
$this->cart->add($this->createItem002Stub(), 1);
$this->assertEquals(1, $this->cart->getAmount('001'));
$this->assertEquals(1, $this->cart->getAmount('002'));
$this->cart->add($this->createItem001Stub(), -1);
$this->assertEquals(0, $this->cart->getAmount('001'));
$this->assertEquals(1, $this->cart->getAmount('002'));
}
public function testGetItems() {
$this->cart->add($this->createItem001Stub(), 3);
$this->assertEquals(1, count($this->cart->getItems()));
$this->cart->add($this->createItem002Stub(), 2);
$this->assertEquals(2, count($this->cart->getItems()));
$items = $this->cart->getItems();
$this->assertEquals(3, $items['001']['amount']);
$this->assertEquals(2, $items['002']['amount']);
}
public function testClearCart() {
$this->cart->add($this->createItem001Stub(), 1);
$this->cart->add($this->createItem002Stub(), 2);
$this->cart->add($this->createItem003Stub(), 3);
$this->cart->clear();
$this->assertTrue(is_array($this->cart->getItems()));
$this->assertEquals(0, count($this->cart->getItems()));
}
public function testAddUpperLimit() {
$this->cart->add($this->createItem001Stub(), PHP_INT_MAX);
try {
$this->cart->add($this->createItem001Stub(), 1);
} catch (OutOfRangeException $e) {
return;
}
$this->fail();
}
public function testAddUnderLimit() {
$this->cart->add($this->createItem001Stub(), 1);
$this->assertEquals(1, count($this->cart->getItems()));
$this->cart->add($this->createItem001Stub(), -1);
$this->assertEquals(0, count($this->cart->getItems()));
$this->cart->clear();
$this->cart->add($this->createItem001Stub(), 1);
$this->assertEquals(1, count($this->cart->getItems()));
$this->cart->add($this->createItem001Stub(), -2);
$this->assertEquals(0, count($this->cart->getItems()));
}
public function testAddRemove() {
$this->cart->add($this->createItem001Stub(), 2);
$this->cart->add($this->createItem002Stub(), 3);
$this->assertEquals(2, count($this->cart->getItems()));
$this->cart->add($this->createItem001Stub(), -2);
$items = $this->cart->getItems();
$this->assertEquals(1, count($items));
$this->assertFalse(isset($items['001']));
$this->assertEquals(3, $items['002']['amount']);
$this->cart->add($this->createItem002Stub(), -3);
$items = $this->cart->getItems();
$this->assertEquals(0, count($items));
$this->assertFalse(isset($items['001']));
$this->assertFalse(isset($items['002']));
}
private function createItem001Stub() {
include_once 'Item.php';
$mock = $this->getMock('Item');
$mock->expects($this->any())
->method('getName')
->will($this->returnValue('item001'));
$mock->expects($this->any())
->method('getCode')
->will($this->returnValue('001'));
$mock->expects($this->any())
->method('getPrice')
->will($this->returnValue(1200));
return $mock;
}
private function createItem002Stub() {
include_once 'Item.php';
$mock = $this->getMock('Item');
$mock->expects($this->any())
->method('getName')
->will($this->returnValue('item002'));
$mock->expects($this->any())
->method('getCode')
->will($this->returnValue('002'));
$mock->expects($this->any())
->method('getPrice')
->will($this->returnValue(2000));
return $mock;
}
private function createItem003Stub() {
include_once 'Item.php';
$mock = $this->getMock('Item');
$mock->expects($this->any())
->method('getName')
->will($this->returnValue('item003'));
$mock->expects($this->any())
->method('getCode')
->will($this->returnValue('003'));
$mock->expects($this->any())
->method('getPrice')
->will($this->returnValue(1500));
return $mock;
}
public function testTotal() {
$this->assertEquals(0, $this->cart->getTotal());
$this->cart->add($this->createItem001Stub(), 1);
$this->cart->add($this->createItem002Stub(), 2);
$this->cart->add($this->createItem003Stub(), 3);
/**
* 9700 = 1200 * 1 + 2000 * 2 + 1500 * 3
*/
$this->assertEquals(9700, $this->cart->getTotal());
}
}
一方のCartクラス側ですが、
<?php
class Cart
{
:
public function getTotal() {
}
}
それでは、
$ phpunit CartTest PHPUnit 3.1.7 by Sebastian Bergmann. .PHP Warning: Illegal offset type in isset or empty in /home/shimooka/public_html/gihyo.jp/Cart.php on line 19 : Warning: Illegal offset type in unset in /home/shimooka/public_html/gihyo.jp/Cart.php on line 28 F Time: 0 seconds There were 6 failures: 1) testGetAmount(CartTest) Failed asserting that <integer:0> matches expected value <integer:1>. /home/shimooka/public_html/gihyo.jp/CartTest.php:45 2) testGetItems(CartTest) Failed asserting that <integer:0> matches expected value <integer:1>. /home/shimooka/public_html/gihyo.jp/CartTest.php:60 3) testAddUpperLimit(CartTest) /home/shimooka/public_html/gihyo.jp/CartTest.php:85 4) testAddUnderLimit(CartTest) Failed asserting that <integer:0> matches expected value <integer:1>. /home/shimooka/public_html/gihyo.jp/CartTest.php:90 5) testAddRemove(CartTest) Failed asserting that <integer:0> matches expected value <integer:2>. /home/shimooka/public_html/gihyo.jp/CartTest.php:104 6) testTotal(CartTest) Failed asserting that <null> matches expected value <integer:0>. /home/shimooka/public_html/gihyo.jp/CartTest.php:165 FAILURES! Tests: 11, Failures: 6. $
当然、
- addメソッドのシグネチャ
(引数) の変更 - addメソッドで、
商品コードをItemオブジェクトのgetCodeメソッドを使って取得する - getTotalメソッドを実装する
- 内部ハッシュの変更とそれに伴う修正
修正後のCartクラスは、
<?php
class Cart
{
private $items;
public function __construct() {
$this->clear();
}
public function getItems() {
return $this->items;
}
public function add(Item $item, $amount) {
$item_cd = $item->getCode();
if (!preg_match('/^-?\d+$/', $amount)) {
throw new UnexpectedValueException('Invalid amount');
}
if (!isset($this->items[$item_cd])) {
$this->items[$item_cd]['amount'] = 0;
$this->items[$item_cd]['object'] = null;
}
$this->items[$item_cd]['amount'] += (int)$amount;
$this->items[$item_cd]['object'] = $item;
if ($this->items[$item_cd]['amount'] > PHP_INT_MAX) {
throw new OutOfRangeException('the amount exceeded PHP_INT_MAX');
}
if ($this->items[$item_cd]['amount'] <= 0) {
unset($this->items[$item_cd]);
}
return true;
}
public function getAmount($item_cd) {
if (isset($this->items[$item_cd]['amount'])) {
return $this->items[$item_cd]['amount'];
} else {
return 0;
}
}
public function clear() {
$this->items = array();
}
public function getTotal() {
$total = 0;
foreach ($this->getItems() as $item => $arr) {
$total += $arr['object']->getPrice() * $arr['amount'];
}
return $total;
}
}
再度テストを実行します。
$ phpunit CartTest PHPUnit 3.1.7 by Sebastian Bergmann. ........... Time: 0 seconds OK (11 tests) $
少し大きめの修正でしたが、
次回は、