防御式编程:使用测试预测错误

发布于 作者:

Defense Programming: Anticipating Failures with Tests image

当你开始开发一个新功能时,明智的做法不仅要规划它的预期工作方式,还要规划如果出现故障会发生什么。提前花时间预测失败是优秀开发人员的品质。

举个简单的例子,考虑一个由第三方服务提供数据的博客。例如,Laravel News 的首页会从 LaraJobs 获取职位信息。如果 LaraJobs 宕机或停止工作会怎样?

由于我们不知道依赖项何时会失效,所以最好通过编写测试来预测失败,以便我们对失败状态更有信心。

Laravel 可以帮助我们编写使用实时门面的测试来预测失败,但在我们深入探讨之前,让我们创建一个虚构的实现来获取我们网站的文章列表。

让我们从以下 ApiArticleRepository 类示例开始,它以 Guzzle HTTP 客户端作为依赖项。

<?php
 
namespace AppRepositories;
 
use GuzzleHttpClient;
 
 
class ApiArticleRepository implements ArticleRepository
{
public function __construct(Client $client)
{
$this->client = $client;
}
 
public function get($id)
{
return $this->client->get('posts', ['query' => ['id' => $id]]);
}
}

如果你不熟悉 Guzzle

Guzzle 是一个 PHP HTTP 客户端,它简化了发送 HTTP 请求的过程,并易于与 Web 服务集成。

我们在 ApiArticleRepository 类中可以采取的一种方法是,将此实现隐藏在接口后面,你可以使用 Laravel 的服务容器来实例化此类。

<?php
 
namespace AppContracts;
 
interface ArticleRepository
{
public function get();
}

现在,让我们将实现绑定到接口,以便每次我们尝试使用控制器中的依赖注入实例化接口时,容器都可以解析此类。

将接口绑定到具体实现可以在 AppProvidersAppServiceProvider 类中完成。

<?php
 
use GuzzleHttp/Client;
 
class RepositoryServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton('PostRepository', function () {
return new ApiPostRepository(new GuzzleClient ([
'base_uri' => config('api.url')
]);
});
}

以下是如何在控制器中实现存储库的示例。

<?php
 
namespace AppHttpControllers;
 
use AppRepositoriesApiArticleRepository as Repository;
 
class RegisterController extends Controller
{
protected $repository;
 
public function __construct(Repository $repository)
{
$this->repository = $repository;
}
 
public function view($id)
{
return view('artice.view', ['article' => $this->repository->get($id)]);
}
}

测试失败

考虑到我们的 ApiArticleRepository 类,我们可以首先考虑如果 API 返回异常而不是响应会发生什么。

当 Guzzle 尝试发出请求但失败时会发生什么?

Laravel 有一个名为 实时门面 的概念,我们可以使用它来模拟异常并测试我们的代码如何响应。

使用实时门面

如果你不熟悉实时门面,可以将其定义为以下内容。

门面提供应用程序服务容器中可用类的“静态”接口。

使用门面的优势之一是,你可以访问一些方法,这些方法可以帮助你在测试环境中创建任何类的模拟对象。

如何使用实时门面实例化类?

你需要做的就是像这样在 use 语句中添加 Facades 前缀。

<?php
 
use FacadesGuzzleHttpClient as GuzzleClient;

在这种情况下,我们可以不模拟 ApiArticleRepository::class,而是退一步,模拟 GuzzleHttpClient::class。我们可以强制此类的每个方法返回所需响应,这样一来,我们就不需要更改存储库类的任何实现。

第一步是更新 ApiArticleRepository 类,使其使用门面而不是依赖注入。

<?php
 
namespace AppRepositories;
 
use FacadesGuzzleHttpClient;
 
 
class ApiArticleRepository implements ArticleRepository
{
 
public function get($id)
{
return Client::get('posts', ['query' => ['id' => $id]]);
}
}

模拟 Guzzle 响应

<?php
 
class ClientTest extends TestCase
/**
* @test
*/
public function testing_guzzle_exception()
{
FacadesGuzzleHttpClient::shouldReceive('get')->andThrow(
new GuzzleHttpExceptionRequestException(
"Error Communicating with Server",
new GuzzleHttpPsr7Request('GET', 'test')
)
);
 
$this->expectException(GuzzleHttpExceptionRequestException::class);
 
$repository = resolve('PostRepository');
$response = $repository->where(['limit' => 1]);
}
}

测试结果。

$ phpunit --filter=testing_guzzle_exception
PHPUnit 6.5.5 by Sebastian Bergmann and contributors.
 
. 1 / 1 (100%)
 
Time: 131 ms, Memory: 12.00MB
 
OK (1 test, 2 assertions)

使用 shouldReceive() 方法会返回 MockeryExpectation::class 的一个实例,因此我们可以链接 andThrown() 方法来指定每次应用程序尝试在 GuzzleHttpClient 实例上运行 get() 方法时抛出的异常。

以下行本身就是一个断言,如果预期异常从未被触发,它将在我们的测试中返回错误。

$this->expectException(GuzzleHttpExceptionRequestException::class);

ApiArticleRepository::get() 尝试访问 GuzzleHttpClient::get() 方法时,它会抛出指定的异常,而不是返回成功的响应。

使用这种方法,你就可以进行更高级别的测试,例如。

<?php
 
class ClientTest extends TestCase
/**
* @test
*/
public function testing_guzzle_exception()
{
FacadesGuzzleHttpClient::shouldReceive('get')->andThrow(
new GuzzleHttpExceptionRequestException(
"Error Communicating with Server",
new GuzzleHttpPsr7Request('GET', 'test')
)
);
 
$this->expectException(GuzzleHttpExceptionRequestException::class);
 
$response = $this->get('/');
$response->assertStatus(500);
}
}

在这种情况下,我们的测试也会通过。

$ phpunit --filter=testing_guzzle_exception
PHPUnit 6.5.5 by Sebastian Bergmann and contributors.
 
. 1 / 1 (100%)
 
Time: 131 ms, Memory: 12.00MB
 
OK (1 test, 2 assertions)

最后的想法

我认为,当需要使用 Guzzle 与外部服务和第三方 API 交互时,这是一个相对容易的测试应用程序并预测失败的方法。

此外,如果你刚开始学习测试,相信我,你会发现这种方法比尝试创建模拟对象、存根、使用替身等方法要容易得多。

就这样,现在你已经准备好编写一个由测试支持的 HTTP 客户端实现,该实现对外部 API 故障做出响应。

Jeff photo

我是一名全栈 Web 开发人员兼兼职作家。

你可以在 https://medium.com/@jeffochoa 找到我更多的作品。

Cube

Laravel 新闻稿

加入 40,000 多名其他开发人员,不错过任何新的提示、教程等。

Laravel Forge logo

Laravel Forge

轻松创建和管理服务器,并在几秒钟内部署你的 Laravel 应用程序。

Laravel Forge
Tinkerwell logo

Tinkerwell

Laravel 开发人员必备的代码运行器。使用 AI、自动完成和本地和生产环境中的即时反馈进行微调。

Tinkerwell
No Compromises logo

No Compromises

Joel 和 Aaron,两位来自 No Compromises 播客的经验丰富的开发人员,现在可以为你的 Laravel 项目提供服务。 ⬧ 固定费率为每月 7500 美元。 ⬧ 无需冗长的销售流程。 ⬧ 无需合同。 ⬧ 100% 退款保证。

No Compromises
Kirschbaum logo

Kirschbaum

提供创新和稳定性,以确保你的 Web 应用程序取得成功。

Kirschbaum
Shift logo

Shift

正在运行旧版本的 Laravel?即时、自动化的 Laravel 升级和代码现代化,使你的应用程序保持最新状态。

Shift
Bacancy logo

Bacancy

让你的项目充满活力,每月只需 2500 美元,即可获得拥有 4-6 年经验的经验丰富的 Laravel 开发人员。获取 160 小时的专业知识,享受 15 天无风险试用。立即安排通话!

Bacancy
Lucky Media logo

Lucky Media

立即获得幸运 - Laravel 开发的理想选择,拥有十多年的经验!

Lucky Media
Lunar: Laravel E-Commerce logo

Lunar: Laravel 电子商务

Laravel 的电子商务。一个开源软件包,将现代无头电子商务功能的强大功能带到 Laravel。

Lunar: Laravel 电子商务
LaraJobs logo

LaraJobs

官方 Laravel 职位招聘网站

LaraJobs
SaaSykit: Laravel SaaS Starter Kit logo

SaaSykit: Laravel SaaS 启动工具包

SaaSykit 是一个 Laravel SaaS 启动工具包,它包含运行现代 SaaS 所需的所有功能。支付、漂亮结账、管理面板、用户仪表板、身份验证、即用型组件、统计数据、博客、文档等等。

SaaSykit: Laravel SaaS 启动工具包
Rector logo

Rector

你无缝升级 Laravel、降低成本和加速创新的合作伙伴,助力公司取得成功

Rector
MongoDB logo

MongoDB

使用 MongoDB 和 Laravel 的强大集成来增强您的 PHP 应用程序,使开发人员能够轻松高效地构建应用程序。在使用熟悉的 Eloquent API 的同时,支持事务、搜索、分析和移动用例。探索灵活、现代的 MongoDB 数据库如何改变您的 Laravel 应用程序。

MongoDB
Maska is a Simple Zero-dependency Input Mask Library image

Maska 是一个简单的零依赖输入掩码库

阅读文章
Add Swagger UI to Your Laravel Application image

将 Swagger UI 添加到您的 Laravel 应用程序

阅读文章
Assert the Exact JSON Structure of a Response in Laravel 11.19 image

在 Laravel 11.19 中断言响应的精确 JSON 结构

阅读文章
Build SSH Apps with PHP and Laravel Prompts image

使用 PHP 和 Laravel Prompts 构建 SSH 应用程序

阅读文章
Building fast, fuzzy site search with Laravel and Typesense image

使用 Laravel 和 Typesense 构建快速、模糊的网站搜索

阅读文章
Add Comments to your Laravel Application with the Commenter Package image

使用 Commenter 包向您的 Laravel 应用程序添加评论

阅读文章