Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Integration tests mocking facades vs injecting mocks

We have some legacy laravel projects which use facades in the classes.

use Cache;

LegacyClass
{
    public function cacheFunctionOne()
    {
         $result = Cache::someFunction('parameter');

         // logic to manipulate result

         return $result;
    }

    public function cacheFunctionTwo()
    {
         $result = Cache::someFunction('parameter');

         // different logic to manipulate result

         return $result;
    }
}

Our more recent projects use dependency injection of the underlying laravel classes that the facades represent as has been hinted at by Taylor Otwell himself. (We use constructor injection for each class, but to keep the example short, here I use method injection and use a single class.)

use Illuminate\Cache\Repository as Cache;

ModernClass
{
    public function cacheFunctionOne(Cache $cache)
    {
         $result = $cache->someFunction('parameter');

         // logic to manipulate result

         return $result;
    }

    public function cacheFunctionTwo(Cache $cache)
    {
         $result = $cache->someFunction('parameter');

         // different logic to manipulate result

         return $result;
    }
}

I know facades can be mocked

public function testExample()
{
    Cache::shouldReceive('get')
                ->once()
                ->with('key')
                ->andReturn('value');

    $this->visit('/users')->see('value');
}

Which works nicely for unit tests. The problem I am trying to understand is if these facades are mocked 'globally'.

For example, lets imagine I am writing an integration test (testing a few interconnected classes while mocking services - not an end to end test using live services) which at some point, executes two separate classes which contain the same facade that calls the same method with the same parameters.

In between these classes being called, is some complex functionality that changes what data is returned by that facades method using the same parameter.*

$modernClass->cacheFunctionOne($cache); // easily mocked

// logic that changes data returned by laravel Cache object function 'someFunction'

$modernClass->cacheFunctionTwo($cache); // easily mocked with a different mock

Our modern classes are easy to test because the underlying class that the facade represents is injected into each class (in this example, each method). This means I can create two separate mocks and inject them into each class (method) to mock the different results.

$legacyClass->cacheFunctionOne();

// logic that changes data returned by laravel Cache object function 'someFunction'

$legacyClass->cacheFunctionTwo();

In the legacy systems though, it would seem that the mocked facade is 'global' so that when the facade is run in each class, the exact same value is returned.

Am I correct in thinking this?

*I understand this example may seem completely redundant from a code architecture and testing point of view, but I am stripping out all real functionality to try and give some sort of 'simple' example of what I am asking.

like image 966
myol Avatar asked Aug 31 '16 16:08

myol


People also ask

Do integration tests use mocks?

Unlike end-to-end tests, integration tests substitute unmanaged dependencies with mocks. The only out-of-process components for integration tests are managed dependencies.

How do you mock a facade?

Unlike traditional static method calls, facades may be mocked. We can mock the call to the static facade method by using the shouldReceive method, which will return an instance of a Mockery mock.

Should all dependencies be mocked in a unit test?

Mocking is a very popular approach for handling dependencies while unit testing, but it comes at a cost. It is important to recognize these costs, so we can choose (carefully) when the benefits outweigh that cost and when they don't.

Is Mockito used for integration testing?

Even if Mockito (see my previous post) usually helps us to cut all dependencies in our unit tests, it can be useful also in the integration tests where we often want to have all real applications layers.


1 Answers

Dependency Injection vs Facades

One of the major benefits of Dependency Injection is that code becomes a lot more testable once you start injecting dependencies into methods instead of instantiating/hardcoding them inside the method. This is because you can pass in the dependencies from inside unit tests and they will propagate through the code.

See: http://slashnode.com/dependency-injection/

Dependency Injection stands in stark contrast to Facades. Facades are static global classes, the PHP language does not allow one to overwrite or replace static functions on static classes. The Laravel facades use Mockery to provide mock functionality and they are limited by the same facts as above.

The issue for integration testing can come where you are hoping to retrieve data from a non-mocked Cache but once you use Facade::shouldReceive() then Facade::get() will be overridden by the mocked Cache. The reverse is also true. As a result, Facades are inappropriate where you are interleaving calls for mocked and unmocked data.

In order to test your code with the different data sets that you require, the best practice would be to refactor your legacy code to use DI.

Integration Tests

Easier method

An alternative is to call multiple Facade::shouldReceive() with expectations at the beginning of your integration test. Ensuring that you have the right numbers of expectations in the right order for each of the calls you will make in the integration test. This would probably be the faster way to write tests given your existing codebase.

Harder method

Whilst dependency injection is programming best practice. It could very well be that your codebase has so many legacy classes that it would take an unbelievable amount of time to refactor. In this case, it might be worthwhile considering end-to-end integration tests using a test database with fixtures.

Appendix:

  • For how mockery is called by Facade see - function createMockByName(): https://github.com/laravel/framework/blob/5.3/src/Illuminate/Support/Facades/Facade.php
  • For examples on chaining mockery calls see fhinkel commented on Feb 6, 2015: https://github.com/padraic/mockery/issues/401
like image 143
kdvy Avatar answered Oct 09 '22 13:10

kdvy