2011年10月26日 星期三

Introducing Phake Mocking Framework

I have used PHPUnit heavily now for the last 4 years. As anyone that is heavily involved in writing Unit Tests knows, test doubles (commonly referred to as mock objects) are a necessary part of your toolbox. The mocking options that we used to have for PHP unit testing have traditionally been fairly limited and most all of them in some form or another were ports of JMock. The way PHP operates as well as some decisions made to more closely emulate how JMock does things lead to functionality in the existing mock library for PHPUnit that for some are a hassle. This ranges from PHPUnit implicitly calling the “mockee’s” constructor (you have to explicitly specify that you do not want to call the constructor) to the pain of trying to stub or verify multiple invocations of the same method with different parameters.

Over the last three years, my experience as well as the musing of some of my colleagues has led me to believe that a lot of what I don’t like about mocking in php is the result of the fundamental notions of combining stubbing with verification and setting expectations ahead of method calls instead of verifying that what you expected to happen has indeed happened. This was essentially proven to me over the last year and a half as I have been heavily working with Java code and as a result have been using the Mockito mocking library for Java. The result of this work is the Phake Mocking Framework.

Now I am fairly certain that at least 5 or 6 people (which may constitute everyone who reads this) are rolling their eyes by now. So instead of going further into why I like this style of mocking I’ll just show you how to use it. Phake was designed with PHPUnit in mind, however I don’t really see any reason why it couldn’t be used in other testing frameworks as well. It is on my roadmap to confirm support for other frameworks. In any case, I will be using PHPUnit for my examples.

This document assumes you already have a good understanding of the basics of mocking. If the terms ‘Mocking’, ‘Stubbing’, and ‘Test Doubles’ mean nothing to you, I would recommend checking out the following links:

PHPUnit on Mock Objects
Wikipedia on Mock Objects
The Universe on Mock Objects
Getting Started

You can get a copy of Phake from Github. If you are familiar and comfortable with with git you can just clone my repository. If you would rather avoid the trappings of Github, you can also download Phake’s latest release tarball. Either way will result in a directory with two subdirectories: src and test. You will want to move the contents of the src directory to somewhere in your include path (such as /usr/share/php). Once you have done this, you can simply include “Phake.php” in any of your tests or in your bootstrap script and you will be off to the races.

UPDATE!!!

I just set up a pear channel to distribute phake as well. So if you would like to install Phake using pear, just do the following:

pear channel-discover pear.digitalsandwich.com
pear install channel://pear.digitalsandwich.com/Phake-1.0.0alpha
To show you some of the basics of how to use this framework, I am going to write various bits of codes to mock, verify, stub, etc the following class:


class MyClass
{
private $value;

public function __construct($value)
{
$this->value = $value;
}

public function getValue()
{
return $this->value;
}

public function subtract($int)
{
return $this->value - $int;
}
}

?>
Stubbing


require_once 'Phake.php';
class Test extends PHPUnit_Framework_TestCase
{
public function testStubbingGetValue()
{
// Sets up the mock object.
// Analogous to $this->getMock() in PHPUnit
$mock = Phake::mock('MyClass');

// Builds the stub for getValue.
// Essentially any call to getValue will return 42
Phake::when($mock)->getValue()->thenReturn(42);

$this->assertEquals(42, $mock->getValue());
}
}
You can also do conditional stubbing based on passed in parameters.

public function testStubbingGetValue2()
{
$mock = Phake::mock('MyClass');

// You can pass parameters into the stubbed method to indicate you
// only want to stub matching invocations. By default, anything
// passed in must be loosely matched ('==')
Phake::when($mock)->subtract(42)->thenReturn(30);

$this->assertEquals(30, $mock->subtract(42));

// It is important to note that any unstubbed calls will return null.
// Since 41 != 42 this call will return null.
$this->assertNull($mock->subtract(41));
}
If a specific invocation or call of a mock object has not been stubbed, it will return null. This behavior is different than the default behavior of PHPUnit’s mocking framework. If you need the default PHPUnit behavior then you could use something called partial mocks. Partial mocks are setup to call the constructor of the class being mocked and for any call that has not been stubbed, the parent method will be called.

public function testStubbingGetValue3()
{
// Creates a mock object whose constructor will call
// MyClass::__construct(42);
$mock = Phake::partMock('MyClass', 42);

Phake::when($mock)->subtract(42)->thenReturn(0);

// Since 18 != 42, the real method gets call
$this->assertEquals(24, $mock->subtract(18));
}
You can also specify on a per call basis that you want to call the parent, using the thenCallParent() method instead of thenReturn(). The different values you can use for stubbing are referred to as ‘Answers’. Here are a list of them and what they will do when a matching invocation is called:

thenReturn(mixed $var) – Will return the exact value passed in.
thenCallParent() – Will return the results of calling the mocked parent method.
thenThrow(Exception $e) – Will throw $e.
captureReturnTo(&$variable) – Acts exactly like thenCallParent() however it also captures the value that the parent returned to $variable. This allows you to run assertions. This comes in very handy for testing legacy code with protected or private factory methods whose return values are never returned out of the tested method’s scope.
The other thing to take note of stubbing is that any PHPUnit constraints are supported.

public function testStubbingGetValue4()
{
$mock = Phake::mock('MyClass');

//Matches any call to subtract() where the passed in value equals 42
Phake::when($mock)->subtract(42)->thenReturn(30);

//Matches any call to subtract() where the passed in value is less
// than 42. Notice that this is a phpunit constraint
Phake::when($mock)->subtract($this->lessThan(42))->thenReturn(29);

$this->assertEquals(30, $mock->subtract(42));
$this->assertEquals(29, $mock->subtract(41));
}
This gives you the same kind of stubbing flexibility that you have present in PHPUnit.

Verification

Verifying that methods on your stub are called is starkly different then how it is done in PHPUnit. The most apparent symptom of this difference is that you verify calls after the calls to your test methods have been made.

public function testVerify1()
{
// You can apply stubbings and verifications to the same mock objects
$mock = Phake::mock('MyClass');

$mock->getValue();

//Notice, getValue() has already been called
Phake::verify($mock)->getValue();
}
You of course have the same matching functionality at your disposal.

public function testVerify2()
{
$mock = Phake::mock('MyClass');

$mock->subtract(40);

//Notice the constraint
Phake::verify($mock)->subtract($this->lessThan(42));
}
By default, verify only allows a single matching invocation. You can also specify that a specific number of invocations should be allowed.

public function testVerify3()
{
$mock = Phake::mock('MyClass');

$mock->subtract(40);
$mock->subtract(39);

//The number of times is passed as the second parameter of
//Phake::verify()
Phake::verify($mock, Phake::times(2))->subtract($this->lessThan(42));
}
You can also use Phake::atLeast($n) and Phake::atMost($n) instead of Phake::times($n).

You can also specify that you don’t expect there to be any interactions with a mock.

public function testVerify4()
{
$mock = Phake::mock('MyClass');

// This will ensure that UP TO THIS POINT no methods on $mock have
// been called
Phake::verifyNoInteraction($mock);

// This would not result in an error, this can be prevented with
// another method explained below
$mock->getValue();

}
I am sure you noticed the comment that Phake::verifyNoInteraction() only verifies that no calls were made up to that point. You can essentially freeze a mock with another method

public function testVerify5()
{
$mock = Phake::mock('MyClass');

$mock->getValue();

// This will ensure that no methods on $mock will be called after this
// point
Phake::verifyNoFurtherInteraction($mock);
}
There are a few more advanced things you can do with something called argument captors.

public function testVerify6()
{
$mock = Phake::mock('MyClass');

$mock->subtract(42);

// Phake::capture tells Phake to store the parameter passed to
// subtract as the variable $val
Phake::verify($mock)->subtract(Phake::capture($val));

$this->assertEquals(42, $val);
}
This is a very pedestrian example, but it is not that uncommon for fairly complicated objects to passed in and out of methods. Argument capturing allows you to much more succinctly assert the state of those types of parameters. It is definitely overkill for asserting scalar types or simple objects.

More to Come

This really does cover the very basics of the Phake framework. In the coming days I will be putting out smaller more focused articles discussing some of the specific functionality. In the meantime I would love to get some feedback from anyone who is brave enough to play with this. My future roadmap basically involves shoring up the current code base a little bit more, adding a few pieces of missing or suboptimal functionality (I’m not so sure I have implemented ‘consecutive calls’) but I anticipate releasing an RC version no later than the end of January. Also, I am currently using and monitoring the issue tracker for Phake at github, so if you have some functionality you would like or find any bugs in your exploring, you can also open an issue there. Also, if you would like to help out with contributions, they are certainly welcome.

reference : http://digitalsandwich.com/archives/84-introducing-phake-mocking-framework.html

沒有留言:

wibiya widget