Verifying the order of calls across different mock objects in PHPUnit

Recently, I’ve faced this problem and I ended up with a (relatively) neat solution that, I think, might be worth sharing with the world. Imagine that you are testing a complex class with a large number of dependencies and that you need to verify that some methods on those dependencies will be called in an exactly defined order.

This is an excerpt of the initial implementation that just verifies these calls happen at all:

// So, these calls need to happen in $subject in the exact order they are defined here.
//
//
$table_names_mock->expects( $this->once() )
	->method( 'get_full_table_name' )
	->with( TableNames::ASSOCIATIONS )
	->willReturn( self::ASSOCIATIONS_TABLE_NAME );
$orderby_mock->expects( $this->once() )
	->method( 'register_joins' );
$element_selector_mock->expects( $this->once() )
	->method( 'initialize' );
$root_condition_mock->expects( $this->once() )
	->method( 'get_join_clause' )
	->willReturn( self::JOIN_FROM_ROOT_CONDITION );
$root_condition_mock->expects( $this->once() )
	->method( 'get_where_clause' )
	->willReturn( self::WHERE_FROM_ROOT_CONDITION );
// ... and so on

// Now, we test that the subject produces the expected result and all expectations are met.
//
//
$subject = new SqlExpressionBuilder( $table_names_mock );
$subject->setup( $join_manager_mock );
$query = $subject->build(
	$root_condition_mock,
	self::QUERY_OFFSET,
	self::QUERY_LIMIT,
	$orderby_mock,
	$element_selector_mock,
	$need_found_rows,
	$result_transformation_mock
);

$this->assertStringsEqualExceptWhitespace( $expected_query, $query );

Of course, this will be good enough in most situations, but if the order of calls is really important and you want to have truly thorough tests, read on…

$this->at()

At first, I thought it going to be easy. The test case has a method at( $index ) that provides an expectation of a call to happen on a mock at a specific order. But, as it turns out, the index is relative to each mock. Imagine we’re testing these two lines of code by providing mocks for both objects:

$first_object->do_something();
$second_object->do_something_else();

And the test:

$first_mock->expects( $this->at( 0 ) )->method( 'do_something' );
$second_mock->expects( $this->at( 1 ) )->method( 'do_something_else' );

The test will fail on the second line because the call on $second_mock also happens on index 0 from its perspective. Obviously, there’s no way we can verify that one call happens before the other using $this->at().

Closure within closure

What I ended up with is a solution that makes use of closures and the use() keyword with passing by reference (or not). First, let’s define two variables within our test method:

$current_call_index = 0;
$expected_at = 0;

The first one, $current_call_index, will be increased each time one of our “monitored” methods on a mock is called during testing. $expected_at will be used when setting up the expectations and it will hold the latest expected index of a method call.

Next, we define a closure that returns another closure, using those two variables. The “inner” closure will then then be used as a callback on mock’s method expectation, and it will assert it’s being called at the right time:

$verify_call_index = function( $return = null ) use ( 
	&$current_call_index, 
	&$expected_at 
) {
	$verification_callback = function() use( 
		$expected_at, 
		$return, 
		&$current_call_index 
	) {
		// ...
	};
	$expected_at++;

	return $verification_callback;
};

There are several important things happening here:

  • $verify_call_index uses both above-defined variables by reference. It builds the inner closure and increases $expected_at, so each time it’s called, this value will be higher.
  • The inner closure $verification_callback doesn’t use $expected_at by reference, so that value becomes fixed within the closure.
  • But $current_call_index is still used by reference.
  • We also pass around the $return argument in case the mocked method needs to return a value.

Now, let’s look into the $verification_callback body as well:

$verify_call_index = function( $return = null ) use ( 
	&$current_call_index, 
	&$expected_at 
) {
	$verification_callback = function() use( 
		$expected_at, 
		$return, 
		&$current_call_index 
	) {
		$this->assertSame( $expected_at, $current_call_index );
		$current_call_index++;

		return $return;
	};
	$expected_at++;

	return $verification_callback;
};

First, we assert that the (fixed) $expectected_at value matches the (passed by reference) $current_call_index, which means the method on the mock object has been called in the order in which the $verify_call_index has been used.

Then, we increase it and return a value the mocked method should return (if there is any).

Finally, let’s put this contraption to use when setting up the expectations:

$table_names_mock->expects( $this->once() )
	->method( 'get_full_table_name' )
	->with( TableNames::ASSOCIATIONS )
	->willReturnCallback( $verify_call_index( self::ASSOCIATIONS_TABLE_NAME ) );
$orderby_mock->expects( $this->once() )
	->method( 'register_joins' )
	->willReturnCallback( $verify_call_index() );
$element_selector_mock->expects( $this->once() )
	->method( 'initialize' )
	->willReturnCallback( $verify_call_index() );
$root_condition_mock->expects( $this->once() )
	->method( 'get_join_clause' )
	->willReturnCallback( $verify_call_index( self::JOIN_FROM_ROOT_CONDITION ) );
// ... and so on

When setting up the expectations, we call $verify_call_index, so that the willReturnCallback() method actually receives an inner closure $verify_callback, but each time with a value of $expected_at increased by one.

When the $subject is doing its thing, each call to one of these mocked methods will cause the $current_call_index value to be compared with the expected call index and then increased by one. And if the expected call index doesn’t match the current call index… the test fails.

Leave a Reply

Your email address will not be published.


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