From wordpress-expert
Guides WordPress testing strategy using PHPUnit and WP_UnitTestCase, with test categories, coverage targets, and AAA patterns. Use for writing tests, infrastructure setup, or coverage review.
npx claudepluginhub dr-robert-li/cowork-wordpress-expertThis skill uses the workspace's default tool permissions.
For all custom code (themes and plugins), design and maintain tests using PHPUnit with the WordPress test framework (`WP_UnitTestCase`).
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
For all custom code (themes and plugins), design and maintain tests using PHPUnit with the WordPress test framework (WP_UnitTestCase).
tests/
├── unit/ # Pure unit tests (no WordPress dependencies)
│ ├── Validators/ # Input validation functions
│ ├── Formatters/ # Data formatting/transformation
│ └── Calculators/ # Business logic calculations
├── integration/ # Tests requiring WordPress (WP_UnitTestCase)
│ ├── PostTypes/ # Custom post type registration & behavior
│ ├── Taxonomies/ # Custom taxonomy behavior
│ ├── AJAX/ # AJAX handler tests
│ ├── REST/ # REST API endpoint tests
│ ├── Database/ # Custom table operations
│ ├── Hooks/ # Filter and action behavior
│ └── Admin/ # Admin page functionality
├── security/ # Security-specific tests
│ ├── Nonce/ # CSRF protection verification
│ ├── Capability/ # Authorization checks
│ ├── Sanitization/ # Input sanitization coverage
│ ├── Escaping/ # Output escaping coverage
│ └── SQLInjection/ # SQL injection resistance
├── performance/ # Performance regression tests
│ ├── QueryCount/ # Database query count assertions
│ ├── MemoryUsage/ # Memory consumption limits
│ └── ExecutionTime/ # Execution time thresholds
└── e2e/ # End-to-end tests (Cypress/Playwright)
├── Frontend/ # User-facing functionality
├── Admin/ # Dashboard functionality
└── Forms/ # Form submission flows
Every test should follow the AAA pattern:
public function test_user_can_save_settings() {
// Arrange: Set up test conditions
$user_id = $this->factory->user->create( [ 'role' => 'administrator' ] );
wp_set_current_user( $user_id );
$_POST['settings_nonce'] = wp_create_nonce( 'save_settings' );
$_POST['settings'] = [ 'option_a' => 'value_a' ];
// Act: Perform the action
$result = save_settings_handler();
// Assert: Verify the outcome
$this->assertTrue( $result );
$this->assertEquals( 'value_a', get_option( 'option_a' ) );
}
Each test method tests ONE behavior. If you need "and" in your test name, you probably need two tests.
Good:
public function test_unauthenticated_user_cannot_access_admin_ajax() { }
public function test_authenticated_user_can_access_admin_ajax() { }
Bad:
public function test_ajax_authentication_and_response() { }
Test names should describe the expected behavior in plain English:
public function test_post_with_missing_title_returns_validation_error() { }
public function test_expired_transient_returns_false() { }
public function test_admin_notice_appears_after_successful_save() { }
For testing multiple inputs against the same logic:
/**
* @dataProvider invalid_email_provider
*/
public function test_invalid_email_returns_error( $email ) {
$result = validate_email( $email );
$this->assertWPError( $result );
}
public function invalid_email_provider() {
return [
'missing_at_sign' => [ 'notanemail.com' ],
'missing_domain' => [ 'test@' ],
'spaces' => [ 'test @example.com' ],
'empty_string' => [ '' ],
];
}
Don't make real HTTP requests or file system operations in tests:
public function test_api_call_handles_timeout() {
// Mock wp_remote_get to simulate timeout
add_filter( 'pre_http_request', function( $preempt, $args, $url ) {
return new WP_Error( 'http_request_failed', 'Operation timed out' );
}, 10, 3 );
$result = fetch_api_data();
$this->assertFalse( $result );
}
Leverage WordPress test framework factories for creating test data:
// Create posts
$post_id = $this->factory->post->create( [
'post_title' => 'Test Post',
'post_type' => 'custom_type',
] );
// Create users
$user_id = $this->factory->user->create( [ 'role' => 'editor' ] );
// Create terms
$term_id = $this->factory->term->create( [
'taxonomy' => 'category',
'name' => 'Test Category',
] );
Use setUp() and tearDown() to ensure test isolation:
public function setUp(): void {
parent::setUp();
// Set up test conditions before each test
$this->admin_user = $this->factory->user->create( [ 'role' => 'administrator' ] );
}
public function tearDown(): void {
// Clean up after each test
wp_set_current_user( 0 );
parent::tearDown();
}
Write tests for the bug BEFORE writing the fix:
After each change:
<phpunit
bootstrap="tests/bootstrap.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true">
<testsuites>
<testsuite name="unit">
<directory suffix="Test.php">./tests/unit/</directory>
</testsuite>
<testsuite name="integration">
<directory suffix="Test.php">./tests/integration/</directory>
</testsuite>
<testsuite name="security">
<directory suffix="Test.php">./tests/security/</directory>
</testsuite>
<testsuite name="performance">
<directory suffix="Test.php">./tests/performance/</directory>
</testsuite>
</testsuites>
<coverage>
<include>
<directory suffix=".php">./src/</directory>
<directory suffix=".php">./includes/</directory>
</include>
<exclude>
<directory suffix=".php">./vendor/</directory>
<directory suffix=".php">./tests/</directory>
</exclude>
<report>
<html outputDirectory="tests/coverage/html"/>
<clover outputFile="tests/coverage/clover.xml"/>
</report>
</coverage>
</phpunit>
public function test_filter_modifies_post_title() {
$original_title = 'Original Title';
$filtered_title = apply_filters( 'my_plugin_post_title', $original_title );
$this->assertEquals( 'Modified: Original Title', $filtered_title );
}
public function test_action_sends_email() {
// Use a test mailer
$mailer = tests_retrieve_phpmailer_instance();
do_action( 'my_plugin_send_notification', 'user@example.com' );
$this->assertEquals( 'user@example.com', $mailer->get_recipient( 'to' )->address );
}
public function test_ajax_handler_requires_nonce() {
// Simulate AJAX request without nonce
try {
$this->_handleAjax( 'my_ajax_action' );
} catch ( WPAjaxDieContinueException $e ) {
// Expected to die with -1 (nonce failure)
}
$response = json_decode( $this->_last_response );
$this->assertEquals( -1, $response );
}
public function test_rest_endpoint_requires_authentication() {
$request = new WP_REST_Request( 'POST', '/my-plugin/v1/data' );
$response = rest_do_request( $request );
$this->assertEquals( 401, $response->get_status() );
}
public function test_rest_endpoint_returns_valid_data() {
$user_id = $this->factory->user->create( [ 'role' => 'administrator' ] );
wp_set_current_user( $user_id );
$request = new WP_REST_Request( 'GET', '/my-plugin/v1/data' );
$response = rest_do_request( $request );
$this->assertEquals( 200, $response->get_status() );
$this->assertArrayHasKey( 'data', $response->get_data() );
}
public function test_custom_table_created_on_activation() {
global $wpdb;
$table_name = $wpdb->prefix . 'my_custom_table';
my_plugin_activation_hook();
$this->assertEquals( $table_name, $wpdb->get_var( "SHOW TABLES LIKE '{$table_name}'" ) );
}
# Run all tests
phpunit
# Run specific test suite
phpunit --testsuite=unit
phpunit --testsuite=integration
phpunit --testsuite=security
# Run specific test file
phpunit tests/unit/Validators/EmailValidatorTest.php
# Run with coverage report
phpunit --coverage-html tests/coverage/html
# Run with code coverage filter
phpunit --filter test_specific_method
Integrate with CI/CD pipelines (GitHub Actions, GitLab CI, etc.):
# .github/workflows/tests.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- name: Install dependencies
run: composer install
- name: Run tests
run: phpunit --coverage-clover coverage.xml
# Generate HTML coverage report
phpunit --coverage-html tests/coverage/html
# View in browser
open tests/coverage/html/index.html
# Generate coverage summary
phpunit --coverage-text
# Check coverage threshold (fail if below 80%)
phpunit --coverage-text --coverage-clover=coverage.xml --coverage-threshold=80