防御式编程:使用测试预测错误
发布于 作者: Jeff
当你开始开发一个新功能时,明智的做法不仅要规划它的预期工作方式,还要规划如果出现故障会发生什么。提前花时间预测失败是优秀开发人员的品质。
举个简单的例子,考虑一个由第三方服务提供数据的博客。例如,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_exceptionPHPUnit 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_exceptionPHPUnit 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 故障做出响应。