Rock Out With Your Mock Out: Using the PHPUnit at() Method

Having established how to create mock methods with stubs that run multiple times, I was pretty certain that I had finally slain the unit testing dragon, was king of the PHPUnit castle and a lifetime of finely tested code and buxom wenches awaited me (because buxom wenches go weak at the knees for well tested code. FACT!). Pride comes before a fall, as smug people who I wish would fall down a tall building like to remind you. Fortunately, if experience has taught me nothing else, it has taught me how to execute many google searches while falling towards earth at a rapid rate. This particular fall was cushioned by the discovery of the at() method.

Consider the following method:

public function generate_booking_payments_received( $data ) {

	foreach ( $data as $line ) {
		$received = $this->amount_from_payment_line( $line );
		$this->set_payments_received( $received );
	}
}

public function set_payments_received( $payment_received ) {
	$this->payments_received += $payment_received;
}

We are parsing an array, establishing what was received, and incrementing a variable by the value. A simple test for the set_payments_received method would establish that it is doing its job and incrementing rather than overwriting, but we want to be sure that the generate_booking_payments_received is also behaving. We’ve already tested the amount_from_payment_line method, so we can stub it, and just to show off, we’ll get it to return different values each time. Using the at() method, we would achieve this using something like this:

$our_class = $this->getMockBuilder( 'Our_Class' )
	->setMethods( [ 'amount_from_payment_line' ] )
	->getMock();

$our_class->expects( $this->at( 0 ) )
	->method( 'amount_from_payment_line' )
	->will( $this->returnValue( 99.99 ) );

$our_class->expects( $this->at( 1 ) )
	->method( 'amount_from_payment_line' )
	->will( $this->returnValue( 149.98 ) );

$our_class->generate_payments_received( [
	[ 'foo' ],
	[ 'bar' ],
], 'fee' );

$this->assertEquals( 249.97, $our_class->get_payments_received() );

We’re passing an array with two arrays stuffed inside so we know the foreach loop will run twice calling the stubbed amount_from_payment_line each time. The first time it runs we return a value of 99.99, the second, 149.98. We test the value of get_payments_received and establish that the method has run correctly. Too easy.

What was that old adage about pride I mentioned? I’m finding it hard to remember because the floor seems to be rushing towards me at high velocities.

Eager to re-use the latest tool I’d added to my toolbox, I turned my attention to testing a more complex method:

public function parse_query_results() {

	// Set some initial data.
	$this->set_foo();
	$this->set_bar();
	$this->set_foo_bar();

	while( $this->get_report_query()->have_posts() ) {

		$this->get_report_query()->the_post();
		$this->set_id( get_the_id() );

		// Load in data.
		$this->set_data();

		$id = $this->get_id();

		// Get amount due.
		$this->report_data[ $id ]['due'] = $this->due();
	}
}

In this method, we set some properties via their setter methods, and loop through a stored WP_Query object and run a method to generate a value, which we then add to an array.

All we want to test is that the $report_data array is being correctly updated. The initial setter methods have all been tested already, its pretty safe to assume that the good people at WordPress have tested “the loop ™” and WP_Query ad infinitum so we can safely stub this. We’ve tested the due() method so it can be stubbed. The key is the get_id() method. If we pass in a stubbed WP_query that runs 3 times and simply use the expects method to pass the same ID back to the get_id() method, we will end up with each iteration of the while loop overwriting the data from the previous iteration.

I allowed myself a smug and knowing smile at this point as I confidently reached for my trusty new at() method, and quickly drafted a test.

$our_class = $this->getMockBuilder( 'Our_Class' )
	->setMethods( [
		'set_foo',
		'set_bar',
		'set_foo_bar',
		'get_report_query',
		'set_id',
		'set_data',
		'get_id',
		'due',
	] ) )
	->getMock();

$our_class->expects( $this->exactly( 7 ) )
	->method( 'get_report_query' )
	->will( $this->returnValue( new Mock_WP_Query() ) );

// This can be the same on each pass.
$our_class->expects( $this->exactly( 3 ) )
	->method( 'due' )
	->will( $this->returnValue( '9.99' ) );

$our_class->expects( $this->at( 0 ) )
	->method( 'get_id' )
	->will( $this->returnValue( 123 ) );

$our_class->expects( $this->at( 1 ) )
	->method( 'get_id' )
	->will( $this->returnValue( 124 ) );

$our_class->expects( $this->at( 2 ) )
	->method( 'get_id' )
	->will( $this->returnValue( 125 ) );

$this->assertEquals( [
	123 => [ 'due' => 9.99 ],
	124 => [ 'due' => 9.99 ],
	125 => [ 'due' => 9.99 ],	
], $our_class->report_data );

Safe to say that a failed unit test makes for a great cure for smugness!
I checked my earlier tests, still working.
I ran the test again just in case I’d momentarily fallen into an alternate reality and then returned.
Same result.
Eventually, I resorted to the last thing any self-respecting developer should have to resort too, I read the instructions.

The $index parameter for the at() matcher refers to the index, starting at zero, in all method invocations for a given mock object.

When I see words like “invocations”, I tend to glaze over a bit, but in less succinct English, this means that you start at 0 with the first instance of any of the methods you are stubbing (in our example above, this would be set_foo()) and add one every time one of the stubbed methods is referenced in the code.

This would mean:

0: set_foo
1: set_bar
2: set_foo_bar
3: get_report_query (called in while loop with have_posts)
4: get_report_query (called with the_post)
5: set_id
6: set_data
7: get_id
8: due
9: get_report_query (called in while loop with have_posts)
10: get_report_query (called with the_post)
11: set_id
12: set_data
13: get_id
14: due
15: get_report_query (called in while loop with have_posts)
16: get_report_query (called with the_post)
17: set_id
18: set_data
19: get_id
20: due
21: get_report_query (called in while loop with have_posts to establish that the clause is falsey)

So our test would read:

$our_class = $this->getMockBuilder( 'Our_Class' )
	->setMethods( [
		'set_foo',
		'set_bar',
		'set_foo_bar',
		'get_report_query',
		'set_id',
		'set_data',
		'get_id',
		'due',
	] ) )
	->getMock();

$our_class->expects( $this->exactly( 7 ) )
	->method( 'get_report_query' )
	->will( $this->returnValue( new Mock_WP_Query() ) );

// This can be the same on each pass.
$our_class->expects( $this->exactly( 3 ) )
	->method( 'due' )
	->will( $this->returnValue( '9.99' ) );

$our_class->expects( $this->at( 7 ) )
	->method( 'get_id' )
	->will( $this->returnValue( 123 ) );

$our_class->expects( $this->at( 13 ) )
	->method( 'get_id' )
	->will( $this->returnValue( 124 ) );

$our_class->expects( $this->at( 19 ) )
	->method( 'get_id' )
	->will( $this->returnValue( 125 ) );

$this->assertEquals( [
	123 => [ 'due' => 9.99 ],
	124 => [ 'due' => 9.99 ],
	125 => [ 'due' => 9.99 ],	
], $our_class->report_data );

Smugness successfully returned, although heed must be paid to the second part of the note on the at() method:

Exercise caution when using this matcher as it can lead to brittle tests which are too closely tied to specific implementation details.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.