Mocking Objects for Unit Testing in PHP Using Go! AOP

These have been always very important questions in software development industry; what should I test in unit testing? And how can I make tests as small as possible, and how to avoid testing already tested code? or 3rd party libraries?

In fact, there are many approaches, but almost all of them agree that, you don’t have to test everything, and that, you have to mock some methods/objects those which you don’t own (for example, 3rd party libraries/code, or another example could be your own tested code). In principle, unit test must fail for one and ONLY ONE reason, this makes sense actually, as it complies with the S in the SOLID object-oriented design principles, which makes the following test, for example, a bad test as it stands:

And here is the bad test …

The test in PostTest::testCreatePost is bad unit test for many reasons, most importantly, it may fail for more than one reason. For example, getting the user by ID getUser in Post::createPost() may fail for some reason, then the testCreatePost will fail, for a reason that is not related to the name of the test. In other words, testCreatePost must only fail if the creating a post failed, not because of anything else, like failure to get user from DB. Second reason why this is a bad unit test, is that, there will be many DB calls, and that is not cheap resource to use in the test.

How to Make It Good Unit Test

The solution here that makes most sense is to use Mock Objects. What is a mock object? According to Wikipedia:

In object-oriented programming, mock objects are simulated objects that mimic the behavior of real objects in controlled ways.

In other words, in some cases, we need to just return some already prepared values instead of calculating the real values, because the real value maybe very complex to calculate or takes too long time to be calculated or may take a lot of resources.

How This Applies To Our Problem

We can use mocks in the problem listed above, to make the unit test, real unit test, that is testing only the things that needs to be tested. To do this there are many good libraries for all the programming languages, for example Mockery for PHP, Mockito for Java, and many other libraries.

However, what we are going to do today is something different. OK! Why should we re-invent the wheel? the answer is : because it is simpler wheel ! also it is a wheel that uses different technology which is AOP.

In this blog post I will use mainly Laravel 4.2 with the Go! AOP Framework by Alexander Lisachenko, which is the framework that I use in my work (For caching, monitoring, security, and mocking), and it showed very high efficiency.

Step-by-Step Manual

You can find all the codes listed in this step-by-step manual in this GitHub repository.

First things first, we need to add the required libraries to the composer.json file. The require part of the composer file should look like:

The following was added:

  • MongoDB support to Laravel (Jenssengers), since the DB that I am using for this example is MongoDB.
  • Go AOP framework, which is the AOP support in this project.
  • Finally, I added the PHPUnit framework dependency to gain the advantage of auto-complete in my IDE when I am changing the unit test cases.

Then after adding these dependencies to your composer.json, you need to run composer update command in the root directory of your project.

Start Using Go AOP

The core of our mocking mechanism is the AOP provided by the Go AOP framework. So after adding the dependency to your project, you have to configure the project to use the AOP library, everything you need is written here (Thanks to Alexander Lisachenko 🙂 ).

In the configuration used in this project, ApplicationAspectKernel.php was put inside app/aspect directory, along with the cache directory and our main and only aspect the MockAspect.php. For bootstrapping the AOP framework, the bootstrap/autoload.php was used as follows:

Now, everything is set, and can be used. So we start writing actual code. First, we add a config file app/config/aopmock.php and app/config/testing/aopmock.php, these two files, will be used to check for two things: first, whether the environment where the application is running is testing or production environment, second, whether the method being invoked now is a method that must be mocked or not. Both of these two checks will be done in MockAspect.php , which is the core of our work (its code is listed below).

In the MockAspect class, there are two interceptors, one of them is intercepting static methods, the other is intercepting the dynamic ones. However, both of them are intercepting any methods invocations that happen in any namespace, in any file located inside app directory or any of its subdirectory.

What happens when it intercepts a method invocation is the following:

  • Checks if the method is mockable
  • … and return the mocks if it is, or proceed with the invocation otherwise.

To do this, basically the interceptors call MockingAspect::isMockable() which checks for the current runtime environment, and returns null if it is not testing environment. Then, it checks if the method invocation is being done on a class and a method that was listed in the aopmock.php or was added to the file by the AopMocker class in the runtime.

The AopMocker class is a utility helper class, that will be used to mock methods by adding them to the configuration file aopmock.php in the runtime. This class is the interface to the mocking mechanism, and here is its code:

Improve Our Unit Test

Now, it is time to improve our unit test listed above. As we said, we have to use mocks. So all we need to do is just mock the enormous, and very expensive operations as follows:

What we did is very simple, we just told the application that from now on, in the unit test, don’t execute the actual User::getUser() method, just return the sample user right away, same goes for Post->save(), the save will be considered done successfully and return true always. This is now a unit test, that is only testing one task, that will fail only for business logic related issues in creating post, like passing an empty title for example. One last thing needs to be mentioned here, is that the Post model will be using the MockableModel trait.

That is because, Go AOP cannot intercept the inherited methods if they are not in the directory specified in the initial configuration. So we put all the methods that are inherited from classes outside the AOP directory, but we want them to be intercepted and mocked in the MockableModel trait, in our case the save() and delete() of the Jenssegers/Mongodb/model.php.

Possible Future Improvements

It is possible to use Annotations instead of config files to intercept methods that needs to be mocked, just like in the caching example introduced by Alexander Lisachenko in his article title Caching Like A Pro where he, used @cacheable annotation on the methods that must be cached. Similarly we can create an annotation called @mockable to intercept methods that must be mocked in testing environment.

Maybe we talk about this in another post. For now enjoy AOP using Go! Framework, and enjoy unit testing, proper unit testing 🙂

Don’t forget to check the GitHub Repo for the complete project.

Thanks for reading 🙂


2 thoughts on “Mocking Objects for Unit Testing in PHP Using Go! AOP

    • Hi Davert,

      Thanks for the note, in the beginning I did not know that the idea is implemented in AspectMock (Go AOP “Alexander Lisachenko” author told me that later). Actually, I was working on something in my work, so the idea came to my mind, I implemented it, then I blogged about it. However, I can tell you that this is little bit simpler implementation 🙂

      thanks again, have a good day 🙂


Leave a Reply

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

You are commenting using your 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 )

Google+ photo

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

Connecting to %s