2011年1月2日 星期日

The Mockery: PHP Mock Objects Made Simple

As I explored in my previous post, PHP Mock Objects: Sucking Ass Since Forever, Mock Objects in PHP have had difficulty gaining traction. One of the targets of this examination (in between variations of “I love PHPUnit”) was PHPUnit’s Mock Objects implementation. My main point, I suppose, was that with PHPUnit being the de-facto standard for unit testing, its mock objects implementation left a lot to be desired. As the later comments suggested, suger coating the situation won’t change the fact I’m criticising one of the most deeply embedded libraries in PHP practice (well, actually a teeny tiny bit of it).

So what is Mockery? Mockery is a mock object framework. You can use it to mock, stub and dazzle your classes into submission – even when they don’t exist yet . Mock objects are almost a given when it comes to Test-Driven Development, but even non-TDD unit testing can use mocks and stubs. They encourage test isolation, stand in for classes you haven’t implemented yet, and can replace resource intensive tasks with mocked versions (since we like our tests to run really fast). Using Mock Objects (or Stubs) is just incredibly useful.

So what does Mockery offer you, the unit tester?

1. It integrates with pretty much any conceivable testing framework with almost no effort.
2. It’s simple to use, intuitive, and specifically works with the language we use to describe object interactions.
3. The API is uncluttered and minimal.
4. Flexibility is its cornerstone allowing it to accurately and succintly express your requirements.
5. It enables simple one-off setups that can be later modified so you don’t need to re-specify entire mocks for every single test.
6. If you use Ruby and PHP, it will make your day (yes, I also write Ruby – sue me).

Why consider it instead of PHPUnit’s or (insert PHP testing framework)’s mock object implementation?

1. It ports to ANY testing framework with zero effort
2. It isn’t inspired by Java
3. It has no gotchas, requirements for custom callback code, or complicated and frustrating setups.
4. It’s capable of mocking real objects and performing mock object recordings of existing processes.
5. Expectations are not pre-constrained – you hold the rope.

Time for the inevitable API exploration… Mockery is written for PHP 5.3, so get your namespace booties on. If you cannot currently adopt PHP 5.3, read along anyway. You’ll be using PHP 5.3 eventually .

You can find Mockery on Github at http://github.com/padraic/mockery. Besides the code, there is one massive README that will go into the API and usage in far more detail than I can do in this article. It is currently not released to a PEAR channel, but I’ll be doing so once I feel happy that the framework has enough hard-core crazy features to last its lifetime in PHP. You can, however, clone from the git repository and install it via PEAR using the included package.xml file. I love issue reports – if you have them, don’t be stingy and keep them to yourself.

For those who find themselves curious about how, specifically, Mockery pits against PHPUnit Mock Objects, I’ll run a later article contrasting the two in terms of features and examples.

The basis of Mockery’s API is the English language. It’s a Domain Specific Language (DSL) in the form of a fluent interface. Given that basis, it’s not surprising that Mockery’s intent is to capture every possible object expectation within a DSL statement that reads like English.

For example, let’s assume we have the class Engineering which is utilised by the class Starship. Hey, I wrote a whole article based on a dodgy Klingon once so get with the programme. We’re testing the Starship class, and find that there is no Engineering class…yet. So we shrug, and decide to use a mocked object to replace Engineering. One of the consequences of this approach is that, down the line, the mock usage will actually specify the API our real Engineering class should use (this is test-driven DESIGN, afterall).

To boot up the creative juices, we start with a story:

Given our Starship has located a planet, it will instruct Engineering to turn off the warp drive, and then to divert 40% power to sensors, and then to divert 20% power to auxilliary engines, and finally to run a Level-1 diagnostic on itself. All instructions must be given once in this precise order. There may optionally be many diagnostic runs of any Level at any stage in the process. The Starship must report whether the operation was successful. Success is measured by successfully passing a Level 1 diagnostic.

Okay, so what exactly are we expecting?

[geshi lang=php]use \Mockery as M;

require_once ‘Starship.php’;

class StarshipTest extends PHPUnit_Framework_TestCase
{
public function testEngineeringResponseToEnteringOrbit()
{
$mock = M::mock(‘Engineering’);
$mock->shouldReceive(‘disengageWarp’)->once()->ordered();
$mock->shouldReceive(‘divertPower’)->with(0.40, ‘sensors’)->once()->ordered();
$mock->shouldReceive(‘divertPower’)->with(0.30, ‘auxengines’)->once()->ordered();
$mock->shouldReceive(‘runDiagnosticLevel’)->with(1)->once()->andReturn(true)->ordered();
$mock->shouldReceive(‘runDiagnosticLevel’)->with(M::type(‘int’))->zeroOrMoreTimes();

$starship = new Starship($mock);
$this->assertTrue($starship->enterOrbit());
}

public function teardown()
{
M::close();
}
}[/geshi]

There’s the test. Our mock has been setup according to the expectations implicit in the feature story. As you might notice, the final diagnostic calls are unordered (can occur at any time between ordered calls). For PHPUnit, the teardown() method is optional if you configure PHPUnit to use the bundled TestListener. I added it to show that it’s the sole integration needed with a testing framework (unless using the bundled TestListener for PHPUnit).

Now let’s write just enough code to pass this test in Starship.php:

[geshi lang=php]class Starship
{

protected $_engineering = null;

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

public function enterOrbit()
{
$this->_engineering->disengageWarp();
$this->_engineering->runDiagnosticLevel(5); // unordered call!
$this->_engineering->divertPower(0.40, ‘sensors’);
$this->_engineering->divertPower(0.30, ‘auxengines’);
$criticalResult = $this->_engineering->runDiagnosticLevel(1);
return $criticalResult;
}

}[/geshi]

Hurrah! The test passes. We’ve just tested Starship without needing a real Engineering object (or even a class of that name!). You can find this example in the examples directory on Github for Mockery, along with the phpunit.xml configuration and Bootstrap.php file used.

If something did go wrong with our expectations, Mockery would throw an Exception to complain about it.

The Mockery API also handles return values (including a return queue for multiple method invocations with the same arguments), of course, along with a host of other features and expectation term methods. Refer to the README up on Github. Like I said before, it’s very complete and informative. We’ve barely touched on the basics of Mockery in this article, so get reading.

reference : http://blog.astrumfutura.com/2010/05/the-mockery-php-mock-objects-made-simple/

沒有留言:

wibiya widget