你好 👋
由于某种原因,Laravel Facades 并不受欢迎。我经常看到有关它们不是真正的外观实现的说法,让我告诉你们,它们确实不是 🤷。它们更像是代理而不是外观。如果你这样考虑,你会发现外观,如果使用得当,可以导致干净且可测试的代码。所以,不要过于纠结于名称,毕竟谁在乎它们叫什么?我们来看看如何利用它们。
一模一样,但不同...但仍相同 😎
在我写服务类时,我并不喜欢使用静态方法,因为它们使得测试依赖的类变得 非常艰难。然而,我喜欢它们提供的简洁调用的方式,比如 Service::action()
。借助 Laravel 实时外观,我们可以实现这一点。
让我们看看这个例子
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use App\Exceptions\CouldNotFetchRates;
use App\Entities\ExchangeRate;
class ECBExchangeRateService
{
public static function getRatesFromApi(): ExchangeRate
{
$response = Http::get('ecb_url'); // Avoid hardcoding URLs, this is just an example
throw_if($response->failed(), CouldNotFetchRates::apiTimeout('ecb'));
return ExchangeRate::from($response->body());
}
}
我们有一个高度简化的服务类,它尝试从API检索汇率并在一切顺利的情况下返回一个DTO(实体或任何让你高兴的东西)。
现在我们可以这样使用这个服务
<?php
namespace App\Classes;
use App\Services\ECBExchangeRateService;
class AnotherClass
{
public function action(): void
{
$rates = ECBExchangeRateService::getRatesFromApi();
// Do something with the rates
}
}
代码看起来很简洁,但它不好;它不可测试。我们可以编写特性测试或集成测试,但当涉及到对这个类进行单元测试时,我们却做不到。没有方法可以模拟 ECBExchangeRateService::getRatesFromApi()
,而单元测试 不应该 有任何依赖(与不同类的交互或与系统的交互)。
既然我们正在讨论单元测试,我想强调一点:不应该并不意味着你不必这样做。有时候在单元测试中进行数据库交互是有意义的,例如测试是否加载了关系 🤷。不要盲目遵循规则;有时候它们有意义,有时候则不然。
因此,为了解决这个问题,我们需要遵循一些步骤
- 将静态的
getRatesFromApi()
转换为普通的一个; - 创建一个新的接口,指定
ECBExchangeRateService
应该实现的哪些方法; - 将新定义的接口绑定到 Laravel 服务提供者中的服务类;
- 根据您想要您的 API 看起来的方式,使用构造函数或者直接在方法中使用依赖注入。
有人可能会认为这是正确做事的方式,但我的想法很简单。我认为这是过度杀鸡用牛刀,尤其是如果我很长时间内不会更改任何实现。
我的意思是,我今天就刚刚添加了自动链接到标题,所以我可以仅链接文章的实时部分。我真的想让事情变得简单 😂
使用 实时伪装(facades),我们可以将 4 个步骤简化为 2 个
- 将静态的
getRatesFromApi()
转换为普通的一个(只需移除static
关键字); - 给导入添加
Facades
前缀。
您的代码应该看起来像这样
<?php
namespace App\Classes;
use Facades\App\Services\ECBExchangeRateService; // The only change we need
class AnotherClass
{
public function action(): void
{
$rates = ECBExchangeRateService::getRatesFromApi();
// do something with the rates
}
}
这就完成了我们所需的所有工作!移除了一个关键字,添加了另一个。这真是太棒了!
以下是我们的代码现在可以如何进行测试
it('does something with the rates', function () {
ECBExchangeRateService::shouldReceive('getRatesFromApi')->once();
(new AnotherClass)->action();
});
我正在使用 Pest。
ECBExchangeRateService
将从容器中解析,正如我们上述 4 个步骤中所做的那样,无需创建额外的接口或添加更多代码。我们保持干净、简单的方法,并确保测试性。我知道有些人还是不会同意,将其视为 黑暗魔法。好吧,如果它在 文档中;孩子们,读读文档吧!
哇哦,可替换的 🔥
记得我提到的将伪装视为代理的想法吗?让我们来解释一下。
当使用 Laravel 队列时,我们在代码中分配作业。当你测试这段代码时,你并不关心作业是否按预期工作;这可以单独测试。相反,你感兴趣的是作业是否已经分配,分配了多少次,使用的有效负载等。为了实现这一点,我们需要两个实现,对吧? Dispatcher
和 DispatcherFake
- 一个将作业实际发送到 Redis、MySQL 或你设置的任何地方,另一个则不进行分配,而是捕获这些事件。
如果我们自己实现,我们需要遵循前面提到的 4 个步骤,并根据自己的上下文更改这些实现的绑定 - 如果我们在运行测试或运行实际代码。现在,伪装使这一切变得非常简单。让我们看看。
让我们首先定义我们的接口
<?php
namespace App\Contracts;
interface Dispatcher
{
public function dispatch(mixed $job, mixed $handler): mixed
}
然后我们可以有两个实现
<?php
namespace App\Bus;
use PHPUnit\Framework\Assert;
use App\Contracts\Dispatcher as DispatcherContract;
class Dispatcher implements DispatcherContract
{
public function dispatch(mixed $job, mixed $handler): mixed
{
// Actually dispatch this to the DB or whatever driver is set
}
}
class DispatcherFake implements DispatcherContract
{
protected $jobs = [];
public function dispatch(mixed $job, mixed $handler): mixed
{
// We are just recording the dispatches here
$this->jobs[$job] = $handler;
}
// We can add testing helpers
public function assertDispatched(mixed $job)
{
Assert::assertTrue(count($this->jobs[$job]) > 0);
}
public function assertDispatchedTimes(mixed $job, int $times = 1)
{
Assert::assertTrue(count($this->jobs[$job]) === $times);
}
// ... and more methods
}
现在,不再需要手动解析实现并绑定多个实现,我们可以使用伪装。它们 拦截 调用,我们可以选择将其转发到任何地方!
<?php
namespace App\Facades;
use App\Bus\DispatcherFake;
use Illuminate\Support\Facades\Facade;
use App\Contracts\Dispatcher as DispatcherContract;
class Dispatcher extends Facade
{
protected static function fake()
{
return tap(new DispatcherFake(), function ($fake) {
// This will set the $resolvedInstance to the faked one
// So every time we try to access the underlying
// implementation, the faked object will be returned instead
static::swap($fake);
});
}
protected static function getFacadeAccessor()
{
return DispatcherContract::class;
}
}
有兴趣了解伪装在底层的工作原理吗?我写了一篇关于这个的文章。
现在我们可以简单地在我们的应用容器中绑定我们的分发器。
<?php
namespace App\Providers;
use App\Bus\Dispatcher;
use Illuminate\Support\ServiceProvider;
use App\Contracts\Dispatcher as DispatcherContract;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(DispatcherContract::class, function ($app) {
return new Dispatcher;
});
}
// ...
}
就这样!现在我们可以优雅地在实现之间切换,我们的代码可以通过清晰的调用进行测试,而无需注入(但具有相同的效果)。
use App\Facades\Dispatcher; // import the facade
it('does dispatches a job', function () {
// This will set the fake implementation as the resolved object
Dispatcher::fake();
// An action that dispatches a job `Dispatcher::dispatch(Job::class, Handler::class);
(new Action)->handle();
// Now you can assert that it has been dispatched
Dispatcher::assertDispatched(Job::class);
});
这是一个超简化的例子,只是为了从一个新的角度来看待事物。有趣的是,这正是你最喜欢的框架是如何内部测试事物的。所以,门面可能不像你想象的那么反模式。它们可能被错误命名,但你可以看到它们是如何简化事物的。
结论
不要与框架抗争;接受它,并尝试利用已经存在的东西。每个问题都有多种方法,它们都可以是好的。不要因为有人持不同意见就否定事物;给它一个机会。对我来说,只要代码可测试,你就在正确的道路上,你不需要过于担心它是否遵循某些规则。
如果你有任何可以包含在文章中的想法,请随时联系我!
driesvints 赞同这篇文章