Symfony 的 DomCrawler 与 Laravel HTTP 测试
发布时间 作者 Paul Redmond
您是否曾经需要在 Laravel 的 HTTP 测试 中断言 HTML 响应的一部分?我最近需要验证响应的一部分以确认重要的内容是否已渲染。例如,假设您有一个关键的 JavaScript 文件,您希望确保它包含在 DOM 中?
以下是我们将构建的测试 API
$node = $response->get('/') ->crawl() ->filter('a') ->reduce(function (Crawler $node): bool { return $node->attr('href') === 'https://news.laravel.net.cn'; }); $this->assertCount(1, $node);
在 PHP 中解析和遍历 DOM 有几种选择,例如 PHP 的 DOMDocument、PHPUnit 的 DOM 断言(已弃用)、Symfony 的 DOMCrawler 组件以及其他各种组件。我个人比较喜欢 Symfony 的 DOMCrawler 组件,它具有非常强大的过滤、遍历等功能。
让我们看看如何快速将 DOMCrawler 组件集成到我们的 Laravel HTTP 测试中!
示例
使用 DOMCrawler 的关键是使用传递给构造函数的 HTML 内容创建一个新的 Crawler
实例
use Symfony\Component\DomCrawler\Crawler; $crawler = new Crawler($response->getContent()); foreach ($crawler as $domElement) { var_dump($domElement->nodeName);}
直接使用 Crawler 实例可以工作,但会很重复。鉴于 Laravel 强大的宏功能,我们可以快速定义一个宏来爬取 TestResponse
。
以下是我的宏的使用方式
$response = $this->get('/'); $crawler = $response->crawl(); // DOM traversal// Assertions
如果要尝试一下,请在服务提供者的 boot()
方法中定义以下宏
use Illuminate\Support\ServiceProvider;use Illuminate\Testing\TestResponse;use Symfony\Component\DomCrawler\Crawler;use PHPUnit\Framework\Assert as PHPUnit; // ... public function boot(): void{ TestResponse::macro('crawl', function(?callable $callback = null): Crawler { if (empty($content = $this->getContent())) { PHPUnit::fail('The HTTP response is empty.'); } $callback ??= fn ($c): Crawler => $c; return call_user_func($callback, new Crawler($content)); });}
我们的宏执行以下操作
- 获取响应内容,如果内容为空,则立即使测试失败
- 如果用户没有使用空合并赋值运算符提供回调,则创建一个直通回调
- 通过回调传递一个新的
Crawler
实例,最终应返回一个Crawler
实例
使用空合并赋值运算符,我们可以通过提供一个默认的直通回调来避免任何 if
检查,即使用户没有传递回调。
callable
的想法是,用户可以遍历 DOM,执行一些断言,并最终通过 crawl()
返回 DOM 的子集,如果只需要 DOM 的一部分。
// Return the full content$crawler = $response->crawl(); // Filter and return the filtered crawler instance$card = $response->crawl(function (Crawler $c) { return $c->filter('a') ->reduce(function (Crawler $node) { return $node->attr('href') === 'https://news.laravel.net.cn'; });});
也许这个例子有点过头了,但让我们验证一下 Laravel 默认安装中包含在 welcome.blade.php
中的 Laravel 新闻 HTML
use Symfony\Component\DomCrawler\Crawler; // ... /** * A basic test example. */public function test_the_application_returns_a_successful_response(): void{ $response = $this->get('/'); $response->assertStatus(200); // Find the Laravel News node $card = $response->crawl() ->filter('a') ->reduce(function (Crawler $node) { return $node->attr('href') === 'https://news.laravel.net.cn'; }); $this->assertNotEmpty($card, 'The Laravel News homepage card was not found!'); // Validate that the $card node has an H2 with `Laravel News` $this->assertEquals( 'Laravel News', $card->filter('h2')->first()->text() ); // Validate that the page shows the blurb $blurb = $card ->filter('p') ->reduce(function (Crawler $n) { return str($n->text())->startsWith('Laravel News is a community driven portal'); }); $this->assertCount(1, $blurb);}
奖励
您可能想要编写更多便利助手来验证测试中的 DOM 元素。例如,以下是一个验证节点是否存在的基本断言
$response ->assertStatus(200) ->assertNodeExists('a[href="https://news.laravel.net.cn"]');
以下是我定义的宏,它还允许与 TestResponse
实例链式调用
TestResponse::macro('assertNodeExists', function(string $selector): static { $node = $this->crawl()->filter($selector); $message = "Failed asserting the node exists with selector \"{$selector}\"."; PHPUnit::assertGreaterThan(0, $node->count(), $message); return $this;});
如果节点不存在,以下是宏在测试输出中的示例
1) Tests\Feature\ExampleTest::test_the_application_returns_a_successful_response Failed asserting the node exists with selector "a[href="https://laravelnews.com"]". Failed asserting that 0 is greater than 0.
我建议您查看 Symphony DOMCrawler 组件 的功能。它包括强大的 XPath 和 CSS 选择器、节点遍历等等!