symfony / panther
PHP 和 Symfony 的浏览器测试和网页抓取库。
Requires
- php: >=8.0
- ext-dom: *
- ext-libxml: *
- php-webdriver/webdriver: ^1.8.2
- symfony/browser-kit: ^5.3 || ^6.0 || ^7.0
- symfony/dependency-injection: ^5.3 || ^6.0 || ^7.0
- symfony/deprecation-contracts: ^2.4 || ^3
- symfony/dom-crawler: ^5.3 || ^6.0 || ^7.0
- symfony/http-client: ^5.3 || ^6.0 || ^7.0
- symfony/http-kernel: ^5.3 || ^6.0 || ^7.0
- symfony/process: ^5.3 || ^6.0 || ^7.0
Requires (Dev)
- symfony/css-selector: ^5.3 || ^6.0 || ^7.0
- symfony/framework-bundle: ^5.3 || ^6.0 || ^7.0
- symfony/mime: ^5.3 || ^6.0 || ^7.0
- symfony/phpunit-bridge: ^5.3 || ^6.0 || ^7.0
README
PHP 和 Symfony 的浏览器测试和网页抓取库
Panther 是一个方便的独立库,用于抓取网站并使用真实浏览器运行端到端测试。
Panther 功能强大。它利用 W3C 的 WebDriver 协议 来驱动原生浏览器,如 Google Chrome 和 Firefox。
Panther 非常易于使用,因为它实现了 Symfony 流行的 BrowserKit 和 DomCrawler API,并包含您测试应用程序所需的所有功能。如果您曾经为 Symfony 应用创建过 功能测试,那么它将听起来非常熟悉:因为 API 完全相同!请注意,Panther 可以用于任何 PHP 项目,因为它是一个独立的库。
Panther 自动找到您本地的 Chrome 或 Firefox 安装并启动它们,因此您不需要在计算机上安装任何其他东西,不需要 Selenium 服务器!
在测试模式下,Panther 会自动使用 PHP 内置的 web 服务器 启动您的应用程序。您可以专注于编写测试或网页抓取场景,Panther 将处理其他所有事情。
功能
与您习惯的测试和网页抓取库不同,Panther
- 执行网页中包含的 JavaScript 代码
- 支持 Chrome(或 Firefox)实现的全部内容
- 允许截图
- 可以等待异步加载的元素出现
- 让您在加载页面的上下文中运行自己的 JS 代码或 XPath 查询
- 支持自定义 Selenium 服务器 安装
- 支持包括 SauceLabs 和 BrowserStack 在内的远程浏览器测试服务
文档
安装 Panther
使用 Composer 在项目中安装 Panther。如果您只想将 Panther 用于测试,而不是在生产环境中进行网页抓取,则可能希望使用 --dev
标志
composer req symfony/panther
composer req --dev symfony/panther
安装 ChromeDriver 和 geckodriver
Panther 使用 WebDriver 协议来控制用于抓取网站的浏览器。
在所有系统上,您都可以使用 dbrekelmans/browser-driver-installer
在本地安装 ChromeDriver 和 geckodriver
composer require --dev dbrekelmans/bdi
vendor/bin/bdi detect drivers
Panther 将自动检测并使用存储在 drivers/
目录中的驱动程序。
或者,您可以使用操作系统的包管理器来安装它们。
在 Ubuntu 上运行
apt-get install chromium-chromedriver firefox-geckodriver
在 Mac 上使用 Homebrew
brew install chromedriver geckodriver
在 Windows 上使用 chocolatey
choco install chromedriver selenium-gecko-driver
最后,您可以手动下载 ChromeDriver(适用于Chromium或Chrome)和 GeckoDriver(适用于Firefox),并将它们放在您的 PATH
或项目的 drivers/
目录中的任何位置。
注册PHPUnit扩展
如果您打算使用Panther测试您的应用程序,我们强烈建议注册Panther PHPUnit扩展。虽然不是强制性的,但这个扩展通过提高性能并允许使用交互式调试模式来显著提高测试体验。
当使用扩展并结合 PANTHER_ERROR_SCREENSHOT_DIR
环境变量时,使用Panther客户端失败或出错的测试(在客户端创建之后)将自动截图以帮助调试。
要注册Panther扩展,请将以下行添加到 phpunit.xml.dist
<!-- phpunit.xml.dist --> <extensions> <extension class="Symfony\Component\Panther\ServerExtension" /> </extensions>
没有扩展,Panther用于运行测试应用程序的Web服务器在需要时启动,并在调用 tearDownAfterClass()
时停止。另一方面,当注册扩展时,Web服务器仅在使用完所有测试后才会停止。
基本用法
<?php use Symfony\Component\Panther\Client; require __DIR__.'/vendor/autoload.php'; // Composer's autoloader $client = Client::createChromeClient(); // Or, if you care about the open web and prefer to use Firefox $client = Client::createFirefoxClient(); $client->request('GET', 'https://api-platform.com'); // Yes, this website is 100% written in JavaScript $client->clickLink('Getting started'); // Wait for an element to be present in the DOM (even if hidden) $crawler = $client->waitFor('#installing-the-framework'); // Alternatively, wait for an element to be visible $crawler = $client->waitForVisibility('#installing-the-framework'); echo $crawler->filter('#installing-the-framework')->text(); $client->takeScreenshot('screen.png'); // Yeah, screenshot!
测试用法
PantherTestCase
类允许您轻松编写端到端测试。它将自动使用内置PHP Web服务器启动您的应用程序,并让您使用Panther爬取它。为了提供您习惯的所有测试工具,它扩展了PHPUnit 的 TestCase
。
如果您正在测试一个Symfony应用程序,PantherTestCase
将自动扩展 WebTestCase
类。这意味着您可以轻松创建功能测试,这些测试可以直接执行应用程序的内核并访问所有现有服务。在这种情况下,您可以使用Panther与Symfony提供的所有爬虫测试断言。
<?php namespace App\Tests; use Symfony\Component\Panther\PantherTestCase; class E2eTest extends PantherTestCase { public function testMyApp(): void { $client = static::createPantherClient(); // Your app is automatically started using the built-in web server $client->request('GET', '/mypage'); // Use any PHPUnit assertion, including the ones provided by Symfony $this->assertPageTitleContains('My Title'); $this->assertSelectorTextContains('#main', 'My body'); // Or the one provided by Panther $this->assertSelectorIsEnabled('.search'); $this->assertSelectorIsDisabled('[type="submit"]'); $this->assertSelectorIsVisible('.errors'); $this->assertSelectorIsNotVisible('.loading'); $this->assertSelectorAttributeContains('.price', 'data-old-price', '42'); $this->assertSelectorAttributeNotContains('.price', 'data-old-price', '36'); // Use waitForX methods to wait until some asynchronous process finish $client->waitFor('.popin'); // wait for element to be attached to the DOM $client->waitForStaleness('.popin'); // wait for element to be removed from the DOM $client->waitForVisibility('.loader'); // wait for element of the DOM to become visible $client->waitForInvisibility('.loader'); // wait for element of the DOM to become hidden $client->waitForElementToContain('.total', '25 €'); // wait for text to be inserted in the element content $client->waitForElementToNotContain('.promotion', '5%'); // wait for text to be removed from the element content $client->waitForEnabled('[type="submit"]'); // wait for the button to become enabled $client->waitForDisabled('[type="submit"]'); // wait for the button to become disabled $client->waitForAttributeToContain('.price', 'data-old-price', '25 €'); // wait for the attribute to contain content $client->waitForAttributeToNotContain('.price', 'data-old-price', '25 €'); // wait for the attribute to not contain content // Let's predict the future $this->assertSelectorWillExist('.popin'); // element will be attached to the DOM $this->assertSelectorWillNotExist('.popin'); // element will be removed from the DOM $this->assertSelectorWillBeVisible('.loader'); // element will be visible $this->assertSelectorWillNotBeVisible('.loader'); // element will not be visible $this->assertSelectorWillContain('.total', '€25'); // text will be inserted in the element content $this->assertSelectorWillNotContain('.promotion', '5%'); // text will be removed from the element content $this->assertSelectorWillBeEnabled('[type="submit"]'); // button will be enabled $this->assertSelectorWillBeDisabled('[type="submit"]'); // button will be disabled $this->assertSelectorAttributeWillContain('.price', 'data-old-price', '€25'); // attribute will contain content $this->assertSelectorAttributeWillNotContain('.price', 'data-old-price', '€25'); // attribute will not contain content } }
要运行此测试
bin/phpunit tests/E2eTest.php
一种多态的猫科动物
Panther还为您提供了即时访问其他基于BrowserKit的Client
和Crawler
实现。与Panther的本地客户端不同,这些替代客户端不支持JavaScript、CSS和截图捕获,但它们是超级快速
的!
有两个替代客户端可用
- 第一个直接操作由
WebTestCase
提供的Symfony内核。这是最快速的客户端,但仅适用于Symfony应用程序。 - 第二个利用了Symfony的HttpBrowser。它是Symfony内核和Panther测试客户端之间的中间体。HttpBrowser使用Symfony的HttpClient组件发送真实的HTTP请求。它速度快,可以浏览任何网页,而不仅仅是测试应用程序的网页。然而,由于它完全用PHP编写,HttpBrowser不支持JavaScript和其他高级功能。这个客户端甚至适用于非Symfony应用程序!
有趣的是,这3个客户端实现了完全相同的API,因此您只需调用适当的工厂方法即可从一种切换到另一种,这为每个测试案例都提供了一个很好的权衡(我需要JavaScript吗?我需要通过外部SSO服务器进行身份验证吗?我想访问当前请求的内核吗?……等等)。
以下是获取这些客户端实例的方法
<?php namespace App\Tests; use Symfony\Component\Panther\PantherTestCase; use Symfony\Component\Panther\Client; class E2eTest extends PantherTestCase { public function testMyApp(): void { $symfonyClient = static::createClient(); // A cute kitty: Symfony's functional test tool $httpBrowserClient = static::createHttpBrowserClient(); // An agile lynx: HttpBrowser $pantherClient = static::createPantherClient(); // A majestic Panther $firefoxClient = static::createPantherClient(['browser' => static::FIREFOX]); // A splendid Firefox // Both HttpBrowser and Panther benefits from the built-in HTTP server $customChromeClient = Client::createChromeClient(null, null, [], 'https://example.com'); // Create a custom Chrome client $customFirefoxClient = Client::createFirefoxClient(null, null, [], 'https://example.com'); // Create a custom Firefox client $customSeleniumClient = Client::createSeleniumClient('http://127.0.0.1:4444/wd/hub', null, 'https://example.com'); // Create a custom Selenium client // When initializing a custom client, the integrated web server IS NOT started automatically. // Use PantherTestCase::startWebServer() or WebServerManager if you want to start it manually. // enjoy the same API for the 3 felines // $*client->request('GET', '...') $kernel = static::createKernel(); // If you are testing a Symfony app, you also have access to the kernel // ... } }
使用Mercure或WebSocket创建隔离的浏览器以测试应用程序
Panther 提供了一种方便的方法来测试具有实时能力并使用 Mercure、WebSocket 以及类似技术的应用程序。
PantherTestCase::createAdditionalPantherClient()
创建额外的、隔离的浏览器,它们可以相互交互。例如,这可以用来测试一个同时有多个用户连接的聊天应用程序。
<?php use Symfony\Component\Panther\PantherTestCase; class ChatTest extends PantherTestCase { public function testChat(): void { $client1 = self::createPantherClient(); $client1->request('GET', '/chat'); // Connect a 2nd user using an isolated browser and say hi! $client2 = self::createAdditionalPantherClient(); $client2->request('GET', '/chat'); $client2->submitForm('Post message', ['message' => 'Hi folks 👋😻']); // Wait for the message to be received by the first client $client1->waitFor('.message'); // Symfony Assertions are always executed in the **primary** browser $this->assertSelectorTextContains('.message', 'Hi folks 👋😻'); } }
访问浏览器控制台日志
如有需要,您可以使用 Panther 访问控制台的内容
<?php use Symfony\Component\Panther\PantherTestCase; class ConsoleTest extends PantherTestCase { public function testConsole(): void { $client = self::createPantherClient( [], [], [ 'capabilities' => [ 'goog:loggingPrefs' => [ 'browser' => 'ALL', // calls to console.* methods 'performance' => 'ALL', // performance data ], ], ] ); $client->request('GET', '/'); $consoleLogs = $client->getWebDriver()->manage()->getLog('browser'); // console logs $performanceLogs = $client->getWebDriver()->manage()->getLog('performance'); // performance logs } }
传递参数给 ChromeDriver
如有需要,您可以配置传递给 chromedriver
二进制文件的参数
<?php use Symfony\Component\Panther\PantherTestCase; class MyTest extends PantherTestCase { public function testLogging(): void { $client = self::createPantherClient( [], [], [ 'chromedriver_arguments' => [ '--log-path=myfile.log', '--log-level=DEBUG' ], ] ); $client->request('GET', '/'); } }
检查 WebDriver 连接的状态
使用 Client::ping()
方法检查 WebDriver 连接是否仍然活跃(对长时间运行的任务很有用)。
附加文档
由于 Panther 实现了流行库的 API,它已经有了一份详尽的文档。
- 关于
Client
类,请参阅 BrowserKit 文档 - 关于
Crawler
类,请参阅 DomCrawler 文档 - 关于 WebDriver,请参阅 PHP WebDriver 文档
环境变量
以下环境变量可以设置以更改 Panther 的某些行为
PANTHER_NO_HEADLESS
:禁用浏览器的无头模式(将显示测试窗口,对调试很有用)PANTHER_WEB_SERVER_DIR
:更改项目的文档根目录(默认为./public/
,相对路径 必须 以./
开头)PANTHER_WEB_SERVER_PORT
:更改 web 服务器的端口(默认为9080
)PANTHER_WEB_SERVER_ROUTER
:使用在每次 HTTP 请求开始时运行的 web 服务器路由脚本PANTHER_EXTERNAL_BASE_URI
:使用外部 web 服务器(不会启动 PHP 内置的 web 服务器)PANTHER_APP_ENV
:覆盖传递给运行 PHP 应用程序的 web 服务器的APP_ENV
变量PANTHER_ERROR_SCREENSHOT_DIR
:为您的失败/错误截图设置基本目录(例如./var/error-screenshots
)PANTHER_DEVTOOLS
:切换浏览器的开发者工具(默认启用
,对调试很有用)PANTHER_ERROR_SCREENSHOT_ATTACH
:将上述截图添加到测试输出的.junit 附件格式中
更改内置 Web 服务器的主机名和端口
如果您想更改内置 web 服务器使用的主机名和/或端口,请将 hostname
和 port
传递给 createPantherClient()
方法的 $options
参数
// ... $client = self::createPantherClient([ 'hostname' => 'example.com', // Defaults to 127.0.0.1 'port' => 8080, // Defaults to 9080 ]);
Chrome 特定的环境变量
PANTHER_NO_SANDBOX
:禁用 Chrome 的沙箱(不安全,但允许在容器中使用 Panther)PANTHER_CHROME_ARGUMENTS
:自定义 Chrome 参数。您需要设置PANTHER_NO_HEADLESS
以完全自定义。PANTHER_CHROME_BINARY
:使用另一个google-chrome
二进制文件
Firefox 特定的环境变量
PANTHER_FIREFOX_ARGUMENTS
:自定义 Firefox 参数。您需要设置PANTHER_NO_HEADLESS
以完全自定义。PANTHER_FIREFOX_BINARY
:使用另一个firefox
二进制文件
访问隐藏文本
根据规范,WebDriver 实现默认情况下只返回 显示 的文本。当您对 head
标签(如 title
)进行筛选时,text()
方法返回空字符串。使用 html()
方法获取标签的完整内容,包括标签本身。
交互模式
Panther可以在测试套件失败后进行暂停。这是一个通过网页浏览器调查问题而真正受欢迎的休息时间。要启用此模式,您需要使用--debug
PHPUnit选项,但不能使用无头模式。
$ PANTHER_NO_HEADLESS=1 bin/phpunit --debug
Test 'App\AdminTest::testLogin' started
Error: something is wrong.
Press enter to continue...
要使用交互模式,PHPUnit扩展 必须 已注册。
使用外部Web服务器
有时,重新使用现有的Web服务器配置而不是启动内置的PHP服务器会更方便。为此,设置external_base_uri
选项。
<?php namespace App\Tests; use Symfony\Component\Panther\PantherTestCase; class E2eTest extends PantherTestCase { public function testMyApp(): void { $pantherClient = static::createPantherClient(['external_base_uri' => 'https://localhost']); // the PHP integrated web server will not be started } }
拥有多域名应用程序
您的PHP/Symfony应用程序可能需要为多个不同的域名提供服务。
由于Panther在测试之间在内存中保存客户端以提高性能,因此如果您使用Panther为不同域名编写多个测试,您将必须在单独的进程中运行测试。
为此,您可以使用原生的@runInSeparateProcess
PHPUnit注解。
注意:使用external_base_uri
选项并在后台启动自己的Web服务器非常方便,因为Panther不需要在每个测试中都启动和停止您的服务器。可以使用Symfony CLI快速且轻松地完成此操作。
以下是一个使用external_base_uri
选项确定客户端使用的域名的示例
<?php namespace App\Tests; use Symfony\Component\Panther\PantherTestCase; class FirstDomainTest extends PantherTestCase { /** * @runInSeparateProcess */ public function testMyApp(): void { $pantherClient = static::createPantherClient([ 'external_base_uri' => 'http://mydomain.localhost:8000', ]); // Your tests } }
<?php namespace App\Tests; use Symfony\Component\Panther\PantherTestCase; class SecondDomainTest extends PantherTestCase { /** * @runInSeparateProcess */ public function testMyApp(): void { $pantherClient = static::createPantherClient([ 'external_base_uri' => 'http://anotherdomain.localhost:8000', ]); // Your tests } }
使用代理
要使用代理服务器,设置以下环境变量:PANTHER_CHROME_ARGUMENTS='--proxy-server=socks://127.0.0.1:9050'
接受自签名SSL证书
要强制Chrome接受无效和自签名的证书,设置以下环境变量:PANTHER_CHROME_ARGUMENTS='--ignore-certificate-errors'
此选项不安全,仅在生产环境中使用它进行测试,决不在生产中使用(例如,用于网络爬虫)。
对于Firefox,实例化客户端如下
$client = Client::createFirefoxClient(null, null, ['capabilities' => ['acceptInsecureCerts' => true]]);
Docker集成
这是一个可以同时运行Chrome和Firefox的Panther的最小Docker镜像。
FROM php:alpine # Chromium and ChromeDriver ENV PANTHER_NO_SANDBOX 1 # Not mandatory, but recommended ENV PANTHER_CHROME_ARGUMENTS='--disable-dev-shm-usage' RUN apk add --no-cache chromium chromium-chromedriver # Firefox and GeckoDriver (optional) ARG GECKODRIVER_VERSION=0.28.0 RUN apk add --no-cache firefox libzip-dev; \ docker-php-ext-install zip RUN wget -q https://github.com/mozilla/geckodriver/releases/download/v$GECKODRIVER_VERSION/geckodriver-v$GECKODRIVER_VERSION-linux64.tar.gz; \ tar -zxf geckodriver-v$GECKODRIVER_VERSION-linux64.tar.gz -C /usr/bin; \ rm geckodriver-v$GECKODRIVER_VERSION-linux64.tar.gz
使用docker build . -t myproject
构建它。使用docker run -it -v "$PWD":/srv/myproject -w /srv/myproject myproject bin/phpunit
运行它。
GitHub Actions集成
与GitHub Actions配合使用,Panther开箱即用。以下是一个最小的.github/workflows/panther.yml
文件,用于运行Panther测试。
name: Run Panther tests on: [ push, pull_request ] jobs: tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Install dependencies run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist - name: Run test suite run: bin/phpunit
Travis CI集成
如果您添加Chrome插件,Panther将与Travis CI无缝配合。以下是一个最小的.travis.yml
文件,用于运行Panther测试。
language: php addons: # If you don't use Chrome, or Firefox, remove the corresponding line chrome: stable firefox: latest php: - 8.0 script: - bin/phpunit
Gitlab CI集成
以下是一个最小的.gitlab-ci.yml
文件,用于使用Gitlab CI运行Panther测试。
image: ubuntu before_script: - apt-get update - apt-get install software-properties-common -y - ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime - apt-get install curl wget php php-cli php7.4 php7.4-common php7.4-curl php7.4-intl php7.4-xml php7.4-opcache php7.4-mbstring php7.4-zip libfontconfig1 fontconfig libxrender-dev libfreetype6 libxrender1 zlib1g-dev xvfb chromium-chromedriver firefox-geckodriver -y -qq - export PANTHER_NO_SANDBOX=1 - export PANTHER_WEB_SERVER_PORT=9080 - php -r "copy('https://getcomposer.org.cn/installer', 'composer-setup.php');" - php composer-setup.php --install-dir=/usr/local/bin --filename=composer - php -r "unlink('composer-setup.php');" - composer install test: script: - bin/phpunit
AppVeyor集成
只要安装了Google Chrome,Panther就可以与AppVeyor无缝配合。以下是一个最小的appveyor.yml
文件,用于运行Panther测试。
build: false platform: x86 clone_folder: c:\projects\myproject cache: - '%LOCALAPPDATA%\Composer\files' install: - ps: Set-Service wuauserv -StartupType Manual - cinst -y php composer googlechrome chromedriver firfox selenium-gecko-driver - refreshenv - cd c:\tools\php80 - copy php.ini-production php.ini /Y - echo date.timezone="UTC" >> php.ini - echo extension_dir=ext >> php.ini - echo extension=php_openssl.dll >> php.ini - echo extension=php_mbstring.dll >> php.ini - echo extension=php_curl.dll >> php.ini - echo memory_limit=3G >> php.ini - cd %APPVEYOR_BUILD_FOLDER% - composer install --no-interaction --no-progress test_script: - cd %APPVEYOR_BUILD_FOLDER% - php bin\phpunit
与其他测试工具的使用
如果您想使用Panther与像LiipFunctionalTestBundle这样的其他测试工具,或者如果您只需要使用不同的基类,Panther可以满足您的需求。它为您提供了Symfony\Component\Panther\PantherTestCaseTrait
,您可以使用它来使用一些Panther的奇妙功能来增强现有的测试基础设施。
<?php namespace App\Tests\Controller; use Liip\FunctionalTestBundle\Test\WebTestCase; use Symfony\Component\Panther\PantherTestCaseTrait; class DefaultControllerTest extends WebTestCase { use PantherTestCaseTrait; // this is the magic. Panther is now available. public function testWithFixtures(): void { $this->loadFixtures([]); // load your fixtures $client = self::createPantherClient(); // create your panther client $client->request('GET', '/'); } }
限制
以下功能目前不受支持
- 爬取XML文档(仅支持HTML)
- 更新现有文档(浏览器主要用于消费数据,而不是创建网页)
- 使用多维PHP数组语法设置表单值
- 返回
\DOMElement
实例的方法(因为此库内部使用WebDriverElement
) - 在下拉列表中选择无效选项
欢迎提交Pull Requests以填补剩余的差距!
故障排除
与Bootstrap 5一起运行
如果您正在使用 Bootstrap 5,那么您可能会遇到测试问题。Bootstrap 5 实现了一个滚动效果,这往往会误导 Panther。
为了解决这个问题,我们建议您在您的样式文件中将 Bootstrap 5 的 $enable-smooth-scroll 变量设置为 false 来禁用此效果。
$enable-smooth-scroll: false;
拯救 Panthers
许多野生猫科动物物种都面临着高度威胁。如果您喜欢这款软件,请通过向 Panthera 组织捐赠 来帮助拯救(真正的)美洲豹。
致谢
由 Kévin Dunglas 创建。由 Les-Tilleuls.coop 赞助。
Panther 是基于 PHP WebDriver 和 其他几个 FOSS 库 构建的。它受到了 Nightwatch.js 的启发,这是一个基于 WebDriver 的 JavaScript 测试工具。