Integration tests mocking facades vs injecting mocks

时间:2016-08-31 16:53:51

标签: php laravel testing integration-testing

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.

1 个答案:

答案 0 :(得分:7)

依赖注入与外墙

依赖注入的一个主要好处是,一旦开始将依赖项注入方法而不是在方法中实例化/硬编码它们,代码就会变得更加可测试。这是因为您可以从单元测试内部传入依赖项,它们将通过代码传播。

请参阅:http://slashnode.com/dependency-injection/

依赖注入与Facades形成鲜明对比。 Facade是静态全局类,PHP语言不允许在静态类上覆盖或替换静态函数。 Laravel外墙使用Mockery提供模拟功能,并且它们受到与上述相同的事实的限制。

集成测试的问题可能出现在您希望从非模拟缓存中检索数据的地方,但是一旦您使用Facade :: shouldReceive(),则模拟缓存将覆盖Facade :: get()。反之亦然。因此,Facades不适用于交换模拟和未模拟数据的调用。

为了使用您需要的不同数据集测试代码,最佳做法是重构遗留代码以使用DI。

整合测试

更简单的方法

另一种方法是在集成测试开始时调用多个Facade :: shouldReceive()。确保您在集成测试中为每个呼叫以正确的顺序获得正确数量的期望。考虑到现有的代码库,这可能是编写测试的更快捷方式。

更难的方法

虽然依赖注入是编程最佳实践。很可能你的代码库有这么多遗留类,重构需要不可思议的时间。在这种情况下,考虑使用带有夹具的测试数据库进行端到端集成测试可能是值得的。

<强>附录