使用 HTTP 测试测试 Laravel 中间件
发布于 作者: Paul Redmond
在这篇文章中,我想演示一个使用 HTTP 测试测试中间件的实际示例。在 HTTP 层面进行测试可以使您的测试更具弹性,更易于阅读。
在最近的一期 Full Stack Radio(#72)中,Adam Wathan 和 Taylor Otwell 发现 HTTP 测试具有很多实际价值,这让我耳目一新。我发现 HTTP 测试更容易编写和维护,但我确实感觉自己是在“错误地进行测试”™,或者是在通过不模拟和隔离所有内容来作弊。如果您还没有听过这期节目,请听一下,它充满了良好的、实用的测试建议。
介绍
今年早些时候,我在其中一个项目中构建了一个用于验证和保护 Mailgun Webhook 的中间件,并在 Laravel 新闻上发表了有关 Laravel 中的电子邮件入站处理 的文章。简而言之,我演示了如何使用 Laravel 中间件验证 Mailgun Webhook(以确保 webhook 确实 来自 Mailgun),同时处理入站电子邮件。
在设置 Mailgun Webhook 的核心,建议您通过使用请求中提供的签名、时间戳和令牌验证 HTTP POST 负载中包含的签名来保护您的 Webhook。以下是来自我文章的完整中间件
<?php namespace App\Http\Middleware; use Closure;use Illuminate\Http\Response; class ValidateMailgunWebhook{ public function handle($request, Closure $next) { if (!$request->isMethod('post')) { abort(Response::HTTP_FORBIDDEN, 'Only POST requests are allowed.'); } if ($this->verify($request)) { return $next($request); } abort(Response::HTTP_FORBIDDEN, 'The webhook signature was invalid.'); } protected function buildSignature($request) { return hash_hmac( 'sha256', sprintf('%s%s', $request->input('timestamp'), $request->input('token')), config('services.mailgun.secret') ); } protected function verify($request) { if (abs(time() - $request->input('timestamp')) > 15) { return false; } return $this->buildSignature($request) === $request->input('signature'); }}
此中间件仅接受 POST
请求,并将传入的签名与使用 Mailgun 密钥作为密钥生成的签名进行比较。
我见过各种测试中间件的方法,例如在单元测试中直接构建它,根据需要模拟对象,以及直接运行中间件。在这篇文章中,我将向您展示如何使用更高级别的 HTTP 测试来测试此中间件。您的整个堆栈将在测试中运行,让您更有信心您的应用程序按预期工作。
您要理解的一个重要好处是,您的测试不直接绑定到特定的中间件实现。我们可以完全重构中间件,而无需更改任何测试或更新模拟来验证中间件是否正常工作。我相信您会发现这些测试非常健壮。
设置
让我们使用一个示例 Laravel 5.5 项目快速构建上述中间件的测试。
$ laravel new middleware-tests # Change to the middleware-tests/ folder$ cd $_ $ php artisan make:middleware ValidateMailgunWebhook
将上面的中间件代码复制并粘贴到此中间件文件中。
接下来,将此中间件添加到 app/Http/Kernel.php
文件中
protected $routeMiddleware = [ // ... 'mailgun.webhook' => \App\Http\Middleware\ValidateMailgunWebhook::class,];
编写 HTTP 测试
我们准备针对此中间件编写一些测试,我们甚至不需要在 routes/api.php
中定义任何路由来测试它!
首先,让我们创建功能测试文件
$ php artisan make:test SecureMailgunWebhookTest
查看 Mailgun 中间件,以下是我们要测试的内容,以确保中间件按预期工作
- 任何除
POST
之外的 HTTP 动词都应该导致403 Forbidden
响应。 - 无效签名应该产生
403 Forbidden
响应。 - 有效签名应该通过并命中路由可调用对象。
- 旧时间戳应该导致
403 Forbidden
响应。
测试无效 HTTP 方法
有了这些介绍,让我们编写第一个测试并设置我们的测试。
使用以下内容更新 SecureMailgunWebhookTest
文件
<?php namespace Tests\Feature; use Tests\TestCase;use Symfony\Component\HttpFoundation\Response;use Symfony\Component\HttpKernel\Exception\HttpException; class SecureMailgunWebhookTest extends TestCase{ protected function setUp() { parent::setUp(); config()->set('services.mailgun.secret', 'secret'); \Route::middleware('mailgun.webhook')->any('/_test/webhook', function () { return 'OK'; }); } /** @test */ public function it_forbids_non_post_methods() { $this->withoutExceptionHandling(); $exceptionCount = 0; $httpVerbs = ['get', 'put', 'patch', 'delete']; foreach ($httpVerbs as $httpVerb) { try { $response = $this->$httpVerb('/_test/webhook'); } catch (HttpException $e) { $exceptionCount++; $this->assertEquals(Response::HTTP_FORBIDDEN, $e->getStatusCode()); $this->assertEquals('Only POST requests are allowed.', $e->getMessage()); } } if (count($httpVerbs) === $exceptionCount) { return; } $this->fail('Expected a 403 forbidden'); }}
在 setUp()
方法中,我们定义了一个假的 Mailgun 密钥,以便我们可以针对它编写测试,然后使用 any()
路由方法定义一个通配符路由。我们的路由将允许我们使用假的测试路由来使用中间件发出 HTTP 请求。
Laravel 5.5 引入了 withoutExceptionHandling()
方法,这意味着我们将在测试中获得抛出的异常,而不是代表异常的 HTTP 响应。
try/catch 将确保我们捕获每个 HTTP 动词的 HttpException
,然后增加捕获异常计数器。如果捕获异常的数量与测试的 HTTP 动词数量匹配,则测试通过。否则,如果我们的请求没有引起异常,则调用 $this->fail()
方法。
我喜欢捕获和断言异常的方法,而不是使用注释。对我来说,这感觉更清晰,我还可以对异常进行断言,以确保异常是我预期的。
您可以使用以下 PhpUnit 命令直接运行中间件功能测试
# Run all tests in the file$ ./vendor/bin/phpunit tests/Feature/SecureMailgunWebhookTest.php # Filter a specific method$ ./vendor/bin/phpunit \ tests/Feature/SecureMailgunWebhookTest.php \ --filter=it_forbids_non_post_methods
测试无效签名
下一个测试验证无效签名会导致 403 Forbidden
错误。此测试与第一个测试不同,因为它使用 POST
方法,但发送无效的请求数据
/** @test */public function it_aborts_with_an_invalid_signature(){ $this->withoutExceptionHandling(); try { $this->post('/_test/webhook', [ 'timestamp' => abs(time() - 100), 'token' => 'invalid-token', 'signature' => 'invalid-signature', ]); } catch (HttpException $e) { $this->assertEquals(Response::HTTP_FORBIDDEN, $e->getStatusCode()); $this->assertEquals('The webhook signature was invalid.', $e->getMessage()); return; } $this->fail('Expected the webhook signature to be invalid.');}
我们传递会导致无效签名的假数据,然后断言正确的响应状态和消息已设置在 HttpException
中。
测试有效签名
当 Webhook 发送有效签名时,路由将处理响应,而不会使中间件中止。如果签名匹配,中间件将调用 verify()
,然后调用 $next()
if ($this->verify($request)) { return $next($request);}
为了编写此测试,我们需要发送有效的签名、时间戳和令牌。我们将在测试类中构建我们自己的 SHA-256 哈希版本,它几乎是中间件中相同方法的复制。中间件和我们的测试都将使用我们在 setUp()
方法中配置的 services.mailgun.secret
密钥
/** @test */public function it_passes_with_a_valid_signature(){ $this->withoutExceptionHandling(); $timestamp = time(); $token = 'token'; $response = $this->post('/_test/webhook', [ 'timestamp' => $timestamp, 'token' => $token, 'signature' => $this->buildSignature($timestamp, $token), ]); $this->assertEquals('OK', $response->getContent());} protected function buildSignature($timestamp, $token){ return hash_hmac( 'sha256', sprintf('%s%s', $timestamp, $token), config('services.mailgun.secret') );}
我们的测试使用中间件中的相同代码构建签名,因此我们可以生成中间件期望的有效签名。在测试结束时,我们断言返回的响应内容等于“OK”,这就是我们在测试路由中返回的内容。
测试使用旧时间戳失败
我们的中间件采取的另一项预防措施是不允许请求在 timestamp
有效期过长的情况下继续。此测试类似于我们其他测试断言失败,但这次我们让所有内容(签名和令牌)都有效,除了 时间戳
/** @test */public function it_fails_with_an_old_timestamp(){ try { $this->withoutExceptionHandling(); $timestamp = abs(time() - 16); $token = 'token'; $response = $this->post('/_test/webhook', [ 'timestamp' => $timestamp, 'token' => $token, 'signature' => $this->buildSignature($timestamp, $token), ]); } catch (HttpException $e) { $this->assertEquals(Response::HTTP_FORBIDDEN, $e->getStatusCode()); $this->assertEquals('The webhook signature was invalid.', $e->getMessage()); return; } $this->fail('The timestamp should have failed verification.');}
请注意 $timestamp = abs(time() - 16);
,这将使中间件的时间戳比较无效。
了解更多
这只是对在 HTTP 层面测试中间件的快速概述。我更喜欢这种级别的测试,因为在中间件上使用模拟可能很乏味,并且绑定到特定实现。如果我选择以后重构,我的测试很可能需要重写以匹配新的中间件。使用 HTTP 测试,我可以自由地重构中间件,并应该期望得到相同的结果。
在 Laravel 中编写 HTTP 测试 非常容易和方便,我发现自己在越来越多的情况下使用这种级别的测试。我认为我编写的测试很容易理解,因为我们没有模拟任何东西。您应该熟悉通过 Laravel 可用于测试套件的断言。这些工具使您的测试工作更容易,我敢说,更有趣。
如果您是测试新手,我们也在 Laravel News 上回顾了 Test Driven Laravel。我个人已经完成了这门课程;如果您刚开始测试 Web 应用程序,它是一个非常棒的资源。