Test The Untestable - Using AspectMock

Ahmed Abid
tajawal
Published in
5 min readApr 1, 2018

--

https://pixabay.com/en/mechanical-engineering-gear-2993233/

Introduction

Testing PHP code, which was not coded to be testable from start, turns to be a bit annoying, especially when you want to improve your test to cover more code and assure the quality of your deliverable. So sometimes we refactor our code to make it testable, but, in most cases, it turns out to be a risky approach that probably produces a lot of bugs or it’s just not easy to change the code base as you maintain a very big legacy code.

In this post i will go through the main challenges and how to handle them and I am going to use AspectMock to achieve that.

There are many challenges you are always facing when you try to cover all your code base with test, for example, we can’t mock static method or override standard PHP functions behavior with your PHPUnit or Codeception frameworks, for example:

  • Testing the static methods:
  • In case you don’t have dependency injection:
  • Standard PHP functions:

In this article, we will try to use the power of AspectMock, by making some tests for the above examples.

What is Codeception?

Codeception is a tool for writing unit, functional, and acceptance tests using the same framework, it helps also in the generation of code coverage report by tested lines percentage, tested Functions percentage, tested Methods percentage, and tested Classes and Traits percentage. codeception.com

Codeception is a modern full-stack testing framework for PHP. Inspired by BDD, it provides an absolutely new way of writing acceptance, functional and even unit tests. Powered by PHPUnit. Codeception

What is AspectMock?

AspectMock is not an ordinary PHP mocking framework. With the power of Aspect Oriented programming and the awesome Go-AOP library, AspectMock allows you to stub and mock practically anything in your PHP code. AspectMock

What can AspectMock do?

AspectMock allows you to stub and mocks practically anything in your PHP code:

  • How would you fake the time() function to produce the same result for each test call?
  • Is there any way to stub a static method of a class?
  • Can you redefine a class method at runtime?

Usually to be able to test something without the use of AspectMock you need to use dependency injection wherever possible. With AspectMock you can unit-test practically any OOP code without the need to refactor it.

Installation and configuration

1. Install the AspectMock package:

Run the following:

composer require codeception/aspect-mock

2. Configuration:

Go to your Codeception test folder and edit this file “_bootstrap.php” (for example “/project/tests/_bootstrap.php”) by adding the following code :

The Codeception test folder where the “_bootstrap.php” file located is referenced by :

_DIR_

Then we are defining the root of our web application which is being tested.

'appDir' => _DIR_ . '/..',

Then we are including all files that needs to be intercepted

'includePaths' => [ 
_DIR_ . '/../vendor/laravel',
_DIR_ . '/../app'
],

Then we are excluding all Codeception test folder files from interception

'excludePaths' => [_DIR_],

Some examples:

Let us figure out the cases that have been mentioned previously:

1. Static method:

Simple example for warm up : we need to test this code for example

A normal use of Codeception will end up with this tests :

$tableName = UserModel::tableName(); 

$this->assertEquals('users', $tableName);

But what if we want the “tableName” function to return something else other than what it really returns?

That will be very useful to simulate some function behavior, that you do not want them to returns what they really return, and just want to verify that they are hit or reached by a normal run of the code, for example, a function that hit the database, and you want only to verify that your running code is reaching that function, and it is hit, without messing your database.

Here is the AspectMock way to do it :

$userModel = test::double('NameSpace\UserModel', ['tableName' => 'my_users']);$name = UserModel::tableName(); 

$this->assertEquals('my_users', $name);

Then we can verify it is really invoked like this :

$userModel->verifyInvoked('tableName');

A full working code for this example is provided at the end.

Now we move to a more interesting example, that will put AspectMock face to face with more challenging code to test.

2. No dependency injection:

We want to test this class that uses the previous one “UserModel”

A normal test case, will check the expected return of the “getName” function like this :

$object = new NoDependencyInjectionNeeded(); 

$original = $object->getName();
$this->assertEquals('users — No Dependency Injection Needed', $original);

Now what if we want to test our function on another tableName ?

Since we do not have access the “$user” variable the first idea that comes to mind is to use the Dependency Injection and pass the “$user” as parameter to the “getName()” function, That will make us search for all the call of this function and edit them and try to pass the correct argument.

Here is an easy solution for this, using AspectMock we can test this function without changing any single line of it. here is how.

test::double('NameSpace\UserModel', ['tableName' => 'my_users']); 

$mokedName = $object->getName();

$this->assertEquals('my_users — No Dependency Injection Needed', $mokedName);

Note: A full working code for this example is provided at the end.

3. Standard PHP functions:

we want to test the “time()” function and we need to use it in different places than verify it return the expected, or the same value, every time.

Usually when we call the time() function in our code in two different places , we get two different value, and we can’t use any expected value to verify our code is working fine, to inject the value we want for any Standard PHP functions, we just need to do this.

$timeProxy = Moke::func('tests\AspectMock', 'time', 'now'); $now = time(); $now1 = time(); $this->assertEquals($now, $now1); $timeProxy->verifyInvokedMultipleTimes(2);

How To Evolve It In Your Application

  1. I created a full running example with some more useful example, you just need to add this file inside the Codeception tests/unit folder, check it here.
  2. Then add this file inside the app folder, check the file here.
  3. Then run this command: composer dump-autoload
  4. Then run the tests using: ./vendor/codeception/codeception/codecept run -v tests/unit/AspectMockTest.php

Note: the examples, the installation and the configuration provided in this article are running on Laravel framework.

AspectMock is really a good way to improve PHP testing experience and coverage, but still, reaching 100% coverage seems to be impossible for example, testing a PHP console command script that uses the exit() functions were really disturbing.

--

--