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
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.
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/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).
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.
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
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
delete() of the
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 🙂