PHP

Mock Objects in PHP – Testing


 Introduction

In unit tests, mock objects simulate the behaviour of real objects. They are commonly utilised to offer test isolation, to stand in for objects which do not yet exist, or to allow for the exploratory design of class APIs without requiring actual implementation up front. A good example, where mock objects would be required, is any point where communication is made to the database. Fine, we want to test those methods that depend on communication to the database, we wouldn’t want test data to be stored along with our real data or, as a worst case scenario, risk the loss of our precious data. During the course of this post, I will provide a setup guideline for mockery, and walk you through a pragmatic example of PHPUnit testing using Mockery’s mock objects. Without further ado, let’s get to it.

Mockery  and PHPUnit Setup with Composer

Mockery is a simple yet flexible PHP testing framework for use in unit testing with PHPUnit, PHPSpec or any other testing framework.

To install, run this on the terminal

OR

Open your composer.json and add this:

then run composer update.

Example Class

Model class houses the method to be tested.

Let’s test the method destroy($id) which deletes a record with supplied $id in the database. Apparently, we wouldn’t want it deleted while testing for the functionality of the method. On the first line, a static method called getTable() of class Backbone is called on to get the name of the database table mapped to the class Model. Though you might have thought a mock object of Backbone is required here, mock objects don’t support static methods. So we have to dive into class Backbone to see what method getTable() entails, probably we could find something to be mocked.

Class Backbone is a helper class that contains some of the functions used in the Model class.

Apparently getTable() depends on mapClassToTable() in the same class and so does mapClassToTable() depend on checkForTable(). So let’s focus on method checkForTable() which requires a database connection to perform queries on. Finally we found what could and must be mocked: the class DbConn instantiated and returned method makeConn().

It is important to know that any mock object expected to be used in a method should be passed in as an argument to the method. In anticipation for that, a parameter should be provided to take care of that in method checkForTable(). So the refactored version of checkForTable() looks like this:

Note: parameter $dbConn is set to have a default value of NULL for the method to work as expected when not being tested since it would be called normally with argument only for $table parameter. So in that case, the real database connection could be used rather than the mocked one.

Next is to make provision for $dbConn parameter everywhere checkForTable() has been called. The refactored version of all other methods are:

Same goes for this method in Model class:

Testing

For effective explanation, I have chosen to use a top-down approach in writing the test. Let’s start by calling the method to be tested;

This requires instance of PDO class for database connection to be stored in $dbConnMock, so we have;

Tracing the link of method calls starting from Backbone::getTable() to mapClassToTable() to checkForTable(), you would notice that only checkForTable() connects to the database and that occurs upon call to method query() on the PDO connection object. So the $dbConnMock mock object we have created needs a method query() to possess that functionality. Let’s make provision for that:

Don’t get freaked out, I will explain. shouldReceive() adds method query() to $dbConnMock mock object. with() takes care of argument expected to be passed into query parameter, and andReturn() specifies what should be the outcome.

It’s good to know what a method does to set the accurate expectations for testing purposes. Normally, the query method connects to an SQL database to run the SQL statement argument provided. In this case, the supplied statement checks for the existence of a table; a PDO statement is returned on existence of the table, while false is returned on the contrary. This leads us to creating a mock object of PDO statement and so comes a new line added to the test.

Since we have made provision for the database connection used deep inside the long chain of method calls, let’s come back to the actual method we are testing: Model::destroy().

Next method prepare() prepare should be added to the $dbConnMock to handle its call right in method destroy(). So we have a new line to the test:

Next, two methods are called on the returned PDO statement from the database query right inside method destroy(); appropriate provision needs to be made for the two. And so we have this:

And that’s it; a complete test for method destroy(). Notice that rowCount() returns the number of records affected by the SQL statement prepared and executed. So 1 is expected on successful deletion of a record. In view of that, a custom exception RecordNotFoundException should be thrown upon failure to delete a record because of its nonexistence. So the complete code for that case is:

These two tests completely test the method destroy().

Conclusion

Mock objects are simulated copies of real objects with the capability of possessing the public properties and methods of the real objects. However, mock objects of Mockery can not possess static methods. Amidst several cases of using mock objects, any point where communication has to be made to a database is a practical pointer for mock object usage. Start mocking up your database connection in all your tests to save your precious data, today! For more examples, checkout the tests directory on my github repo.