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 …
https://gist.github.com/ashajjar/f4d031c6160f5185acdd
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:
https://gist.github.com/ashajjar/a172c12cc5ff189e57ac
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.
https://gist.github.com/ashajjar/f2f4fde9126cdf7c9800
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 🙂
You’ve got the same idea as AspectMock! ) https://github.com/Codeception/AspectMock
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 🙂