Can I use Symfony Panther with PHPUnit for automated testing?
Yes, Symfony Panther is specifically designed to work seamlessly with PHPUnit for automated browser testing. Panther is a powerful testing library that combines the capabilities of Symfony's DomCrawler with a real browser environment powered by ChromeDriver or Geckodriver, making it perfect for end-to-end testing scenarios.
What is Symfony Panther?
Symfony Panther is a browser testing library that allows you to write functional tests for web applications using a real browser. Unlike traditional unit tests that mock browser behavior, Panther executes JavaScript, handles AJAX requests, and provides a complete browser environment for comprehensive testing.
Installation and Setup
Installing Symfony Panther
First, install Symfony Panther via Composer in your PHP project:
composer require --dev symfony/panther
Installing Browser Drivers
Panther requires browser drivers to function. Install ChromeDriver for Chrome testing:
# Using Composer
composer require --dev dbrekelmans/bdi
vendor/bin/bdi detect drivers
# Or download manually
wget https://chromedriver.storage.googleapis.com/LATEST_RELEASE
For Firefox testing, install Geckodriver:
# macOS with Homebrew
brew install geckodriver
# Linux
wget https://github.com/mozilla/geckodriver/releases/latest/download/geckodriver-linux64.tar.gz
tar -xzf geckodriver-linux64.tar.gz
sudo mv geckodriver /usr/local/bin/
Basic PHPUnit Integration
Creating a Panther Test Class
Create a test class that extends Symfony's PantherTestCase
:
<?php
namespace App\Tests;
use Symfony\Component\Panther\PantherTestCase;
class WebScrapingTest extends PantherTestCase
{
public function testPageLoad(): void
{
$client = static::createPantherClient();
$crawler = $client->request('GET', 'https://example.com');
$this->assertSelectorTextContains('h1', 'Welcome');
$this->assertPageTitleContains('Example Domain');
}
}
Advanced Testing with Form Interactions
<?php
namespace App\Tests;
use Symfony\Component\Panther\PantherTestCase;
class FormInteractionTest extends PantherTestCase
{
public function testFormSubmission(): void
{
$client = static::createPantherClient();
$crawler = $client->request('GET', 'https://httpbin.org/forms/post');
// Fill out the form
$form = $crawler->selectButton('Submit')->form();
$form['custname'] = 'John Doe';
$form['custtel'] = '123-456-7890';
$form['custemail'] = 'john@example.com';
// Submit and verify response
$client->submit($form);
$this->assertSelectorTextContains('pre', 'John Doe');
}
}
Configuration and Customization
PHPUnit Configuration
Add Panther configuration to your phpunit.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
<php>
<env name="PANTHER_APP_ENV" value="test"/>
<env name="PANTHER_ERROR_SCREENSHOT_DIR" value="./var/error-screenshots"/>
<env name="PANTHER_CHROME_ARGUMENTS" value="--disable-dev-shm-usage --no-sandbox"/>
<env name="PANTHER_NO_HEADLESS" value="0"/>
</php>
<testsuites>
<testsuite name="panther">
<directory>tests/Panther</directory>
</testsuite>
</testsuites>
</phpunit>
Browser Options and Capabilities
Customize browser behavior with specific options:
<?php
use Symfony\Component\Panther\PantherTestCase;
use Symfony\Component\Panther\Client;
class CustomBrowserTest extends PantherTestCase
{
public function testWithCustomOptions(): void
{
$options = [
'--window-size=1920,1080',
'--disable-gpu',
'--no-sandbox',
'--disable-dev-shm-usage'
];
$client = static::createPantherClient([
'browser' => PantherTestCase::CHROME,
'chromeArguments' => $options
]);
$crawler = $client->request('GET', 'https://example.com');
$this->assertResponseIsSuccessful();
}
}
Handling Dynamic Content and AJAX
Waiting for Elements
When testing applications with dynamic content, use Panther's waiting capabilities:
<?php
use Symfony\Component\Panther\PantherTestCase;
class DynamicContentTest extends PantherTestCase
{
public function testAjaxContent(): void
{
$client = static::createPantherClient();
$crawler = $client->request('GET', 'https://example.com/ajax-page');
// Click button that triggers AJAX
$client->clickLink('Load Data');
// Wait for content to appear
$client->waitFor('.ajax-content');
// Verify the content
$this->assertSelectorTextContains('.ajax-content', 'Dynamic data loaded');
}
public function testWaitForVisibility(): void
{
$client = static::createPantherClient();
$crawler = $client->request('GET', 'https://example.com');
// Wait for element to become visible
$client->waitForVisibility('#hidden-element');
$this->assertSelectorIsVisible('#hidden-element');
}
}
JavaScript Execution and Browser Events
Executing Custom JavaScript
<?php
use Symfony\Component\Panther\PantherTestCase;
class JavaScriptTest extends PantherTestCase
{
public function testJavaScriptExecution(): void
{
$client = static::createPantherClient();
$crawler = $client->request('GET', 'https://example.com');
// Execute JavaScript
$result = $client->executeScript('return document.title;');
$this->assertStringContains('Example', $result);
// Modify page with JavaScript
$client->executeScript('document.querySelector("h1").textContent = "Modified Title";');
$this->assertSelectorTextContains('h1', 'Modified Title');
}
}
Data-Driven Testing
Testing with Multiple Datasets
<?php
use Symfony\Component\Panther\PantherTestCase;
class DataDrivenTest extends PantherTestCase
{
/**
* @dataProvider searchTermProvider
*/
public function testSearchFunctionality(string $searchTerm, string $expectedResult): void
{
$client = static::createPantherClient();
$crawler = $client->request('GET', 'https://example.com/search');
$form = $crawler->selectButton('Search')->form();
$form['q'] = $searchTerm;
$client->submit($form);
$client->waitFor('.search-results');
$this->assertSelectorTextContains('.search-results', $expectedResult);
}
public function searchTermProvider(): array
{
return [
['web scraping', 'Web Scraping Results'],
['API testing', 'API Testing Results'],
['automation', 'Automation Results']
];
}
}
Error Handling and Debugging
Screenshot on Failure
Configure automatic screenshots when tests fail:
<?php
use Symfony\Component\Panther\PantherTestCase;
class DebugTest extends PantherTestCase
{
protected function onNotSuccessfulTest(\Throwable $t): void
{
if ($this->client) {
$this->client->takeScreenshot('failure-screenshot.png');
}
parent::onNotSuccessfulTest($t);
}
public function testWithErrorHandling(): void
{
$client = static::createPantherClient();
try {
$crawler = $client->request('GET', 'https://example.com');
$this->assertSelectorExists('.non-existent-element');
} catch (\Exception $e) {
$client->takeScreenshot('debug-screenshot.png');
throw $e;
}
}
}
Performance Optimization
Parallel Test Execution
For faster test execution, configure parallel testing:
<!-- phpunit.xml -->
<phpunit processIsolation="true"
beStrictAboutResourceUsageDuringSmallTests="true">
<extensions>
<extension class="ParaTest\Extension" />
</extensions>
</phpunit>
Run tests in parallel:
vendor/bin/paratest --processes=4 tests/Panther/
Browser Instance Management
<?php
use Symfony\Component\Panther\PantherTestCase;
class OptimizedTest extends PantherTestCase
{
private static $client;
public static function setUpBeforeClass(): void
{
self::$client = static::createPantherClient();
}
public static function tearDownAfterClass(): void
{
self::$client->quit();
}
public function testOptimizedExecution(): void
{
$crawler = self::$client->request('GET', 'https://example.com');
$this->assertResponseIsSuccessful();
}
}
Integration with CI/CD
GitHub Actions Configuration
name: Panther Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite
- name: Install dependencies
run: composer install --prefer-dist --no-progress
- name: Install ChromeDriver
run: vendor/bin/bdi detect drivers
- name: Run Panther tests
run: vendor/bin/phpunit --testsuite=panther
env:
PANTHER_NO_HEADLESS: 0
PANTHER_CHROME_ARGUMENTS: "--disable-dev-shm-usage --no-sandbox"
Working with Iframes and Complex Elements
Handling Iframe Content
When testing applications with iframes, switch context appropriately:
<?php
use Symfony\Component\Panther\PantherTestCase;
class IframeTest extends PantherTestCase
{
public function testIframeContent(): void
{
$client = static::createPantherClient();
$crawler = $client->request('GET', 'https://example.com/iframe-page');
// Switch to iframe
$client->switchTo()->frame('content-frame');
// Interact with iframe content
$this->assertSelectorTextContains('h1', 'Iframe Content');
// Switch back to main frame
$client->switchTo()->defaultContent();
}
}
Testing Single Page Applications
For SPAs and dynamic content, similar to handling AJAX requests using browser automation:
<?php
use Symfony\Component\Panther\PantherTestCase;
class SPATest extends PantherTestCase
{
public function testSinglePageAppNavigation(): void
{
$client = static::createPantherClient();
$crawler = $client->request('GET', 'https://spa-example.com');
// Wait for SPA to load
$client->waitFor('[data-testid="app-loaded"]');
// Navigate within SPA
$client->clickLink('Products');
$client->waitFor('[data-testid="products-page"]');
$this->assertSelectorTextContains('h1', 'Products');
}
}
Mobile Testing and Responsive Design
Emulating Mobile Devices
<?php
use Symfony\Component\Panther\PantherTestCase;
class MobileTest extends PantherTestCase
{
public function testMobileView(): void
{
$client = static::createPantherClient([
'chromeArguments' => [
'--window-size=375,667', // iPhone SE dimensions
'--user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15'
]
]);
$crawler = $client->request('GET', 'https://example.com');
// Test mobile-specific elements
$this->assertSelectorExists('.mobile-menu');
$this->assertSelectorNotExists('.desktop-nav');
}
}
Best Practices
Test Organization
- Separate test suites: Keep Panther tests separate from unit tests for better organization
- Use page objects: Create page object classes for complex applications
- Implement waiting strategies: Always wait for elements before interactions, similar to handling timeouts in browser automation
- Clean up resources: Properly close browser instances after tests
Performance Considerations
- Use headless mode in CI environments
- Implement browser instance pooling for faster execution
- Optimize selectors for better performance
- Consider using data providers for parameterized tests
Security and Authentication Testing
<?php
use Symfony\Component\Panther\PantherTestCase;
class AuthenticationTest extends PantherTestCase
{
public function testLoginFlow(): void
{
$client = static::createPantherClient();
$crawler = $client->request('GET', 'https://example.com/login');
// Fill login form
$form = $crawler->selectButton('Login')->form();
$form['username'] = 'testuser';
$form['password'] = 'testpass';
$client->submit($form);
$client->waitFor('.dashboard');
// Verify successful login
$this->assertSelectorTextContains('.user-menu', 'Welcome, testuser');
}
}
Conclusion
Symfony Panther with PHPUnit provides a robust solution for end-to-end browser testing in PHP applications. Its seamless integration with PHPUnit, support for real browser environments, and comprehensive API make it an excellent choice for testing modern web applications that rely heavily on JavaScript and dynamic content.
The combination allows you to write maintainable, reliable tests that accurately simulate user interactions while leveraging PHPUnit's familiar testing framework. Whether you're testing form submissions, AJAX interactions, or complex user workflows, Panther and PHPUnit together provide the tools needed for comprehensive automated testing.
With proper configuration and best practices, you can create a robust testing suite that ensures your web applications work correctly across different browsers and scenarios, providing confidence in your deployment process and user experience.