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.

How to run Varying Vagrant Vagrants aka VVV on Windows 10

If you ever need to install Varying Vagrant Vagrants on Windows 10, this is what worked for me (three times on three different computers).

Tested with Windows 10 64bit, VirtualBox 5.0.20, Vagrant 1.8.4, Cygwin 2.5.1 and VVV 1.3.0.

Requirements

  • Administrator privileges
  • Good enough hardware
  • Basic knowledge of VVV
  • Basic knowledge of linux shell
  • Patience
  • Luck (or a bloody sacrifice to some dark god)
  • User name that doesn’t contain spaces in C:\Users\%YOUR_NAME%

Note: If you do have spaces in your username, Cygwin is going to create your home directory in C:\Cygwin64\%YOUR USERNAME% which will later cause problems with running Vagrant. So, right after installing Cygwin, you need to change your home directory to something without spaces and other controversial characters.

Steps to follow

  1. Install Virtualbox.
  2. Install Vagrant.
  3. Install Cygwin.
    1. While installing, select (at least) these additional packages: git, curl and nano.
    2. Choose to create the desktop shortcut.
  4. Install the Microsoft Visual C++ 2010 SP1 Redistributable Package (x86) and don’t ask me why.
  5. Locate the file C:\Windows\System32\drivers\etc\hosts and change its privileges so that you (the current user) can read and write to it.
  6. Open Cygwin and continue working in the terminal.
  7. Get VVV: git clone https://github.com/Varying-Vagrant-Vagrants/VVV.git
  8. Adjust the Customfile to get over a nasty Vagrant/Windows issue:
    cd VVV
    nano Customfile
    and then add these two lines to the Customfile:
    config.vm.box = "trusty32"
    config.vm.box_url = "https://cloud-images.ubuntu.com/vagrant/trusty/current/trusty-server-cloudimg-i386-vagrant-disk1.box"

    and save with Ctrl+O, Ctrl+X.
  9. Install the hosts updater plugin: vagrant plugin install vagrant-hostsupdater.
  10. Install the triggers plugin: vagrant plugin install vagrant-triggers.
  11. Start the whole thing by vagrant up.
  12. Go grab a coffee or six, it will take a while even if your hardware is good.
  13. Let me know how it worked for you.