| name | wordpress-testing-qa |
| description | WordPress plugin and theme testing with PHPUnit integration tests, WP_Mock unit tests, PHPCS coding standards, and CI/CD workflows |
| user-invocable | false |
| disable-model-invocation | true |
| progressive_disclosure | {"entry_point":{"summary":"WordPress plugin and theme testing with PHPUnit integration tests, WP_Mock unit tests, PHPCS coding standards, and CI/CD workflows","when_to_use":"When writing tests, implementing wordpress-testing-qa, or ensuring code quality.","quick_start":"1. Review the core concepts below. 2. Apply patterns to your use case. 3. Follow best practices for implementation."}} |
WordPress Testing & Quality Assurance
progressive_disclosure:
entry_point:
summary: "WordPress plugin and theme testing with PHPUnit, WP_Mock, PHPCS, and CI/CD for quality assurance"
when_to_use:
- "Testing WordPress plugins with PHPUnit integration tests"
- "Unit testing without loading WordPress core (WP_Mock)"
- "Enforcing coding standards with PHPCS"
quick_start:
- "Set up PHPUnit with WordPress test suite"
- "Write unit tests with WP_Mock"
- "Configure PHPCS with WPCS ruleset"
Testing Strategy
Testing Pyramid for WordPress
The WordPress Testing Hierarchy:
/\
/ \ E2E Tests (Playwright)
/ \ - Full user workflows
/------\ - Browser automation
/ \
/ INTEG \ Integration Tests (PHPUnit + WordPress)
/ TESTS \ - Database operations
/ \ - Hook interactions
--------------
UNIT TESTS Unit Tests (WP_Mock)
- Pure logic
- No WordPress dependency
Test Distribution Guidelines:
- Unit Tests (60%): Fast, isolated, no WordPress
- Pure PHP functions
- Class methods with clear inputs/outputs
- Business logic without side effects
- Integration Tests (30%): WordPress-loaded tests
- Database operations
- Hook/filter interactions
- Custom post type registration
- Settings API functionality
- E2E Tests (10%): Browser automation
- Critical user workflows
- Admin panel interactions
- Frontend form submissions
When to Use PHPUnit vs WP_Mock
Use PHPUnit (Integration Tests) when:
- ✅ Testing database operations (
$wpdb, post creation, meta data)
- ✅ Testing WordPress hooks (actions/filters actually firing)
- ✅ Testing template rendering and output
- ✅ Testing plugin activation/deactivation logic
- ✅ Testing with actual WordPress functions
Use WP_Mock (Unit Tests) when:
- ✅ Testing pure business logic
- ✅ Testing functions that call WordPress functions but logic is independent
- ✅ Need fast test execution (no database setup)
- ✅ Testing in isolation without side effects
- ✅ Mocking external API calls
Test Coverage Goals
Minimum Coverage Requirements:
- New Code: 80% minimum coverage
- Critical Paths: 95% coverage (payment processing, authentication, data validation)
- Legacy Code: Gradual improvement, prioritize high-risk areas
- Public APIs: 100% coverage for all public methods
What to Test (Priority Order):
- Security Functions: Nonce verification, sanitization, capability checks
- Data Operations: Database CRUD, data validation, transformation
- Business Logic: Calculations, workflows, state transitions
- Hook Callbacks: Action/filter handlers
- Public APIs: REST endpoints, WP-CLI commands
What NOT to Test:
- ❌ WordPress core functions (assume they work)
- ❌ Third-party library internals
- ❌ Simple getters/setters with no logic
- ❌ Configuration files (theme.json, block.json)
PHPUnit Integration Testing
WordPress Test Suite Setup
Step 1: Install Dependencies
composer require --dev phpunit/phpunit "^9.6"
composer require --dev yoast/phpunit-polyfills "^2.0"
wp scaffold plugin-tests my-plugin
Step 2: Install WordPress Test Library
bash bin/install-wp-tests.sh wordpress_test root '' localhost latest
bash bin/install-wp-tests.sh wordpress_test root '' localhost 6.7
Step 3: Configure phpunit.xml.dist
<?xml version="1.0"?>
<phpunit
bootstrap="tests/bootstrap.php"
backupGlobals="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
stopOnFailure="false"
>
<testsuites>
<testsuite name="plugin">
<directory prefix="test-" suffix=".php">./tests/</directory>
<exclude>./tests/bootstrap.php</exclude>
</testsuite>
</testsuites>
<coverage includeUncoveredFiles="true">
<include>
<directory suffix=".php">./includes/</directory>
</include>
<exclude>
<directory>./vendor/
./tests/
WP_UnitTestCase Base Class
tests/bootstrap.php:
<?php
require_once dirname(__DIR__) . '/vendor/autoload.php';
$_tests_dir = getenv('WP_TESTS_DIR');
if (!$_tests_dir) {
$_tests_dir = rtrim(sys_get_temp_dir(), '/\\') . '/wordpress-tests-lib';
}
if (!file_exists("{$_tests_dir}/includes/functions.php")) {
throw new Exception("Could not find {$_tests_dir}/includes/functions.php");
}
require_once "{$_tests_dir}/includes/functions.php";
function _manually_load_plugin() {
require dirname(__DIR__) . '/my-plugin.php';
}
tests_add_filter('muplugins_loaded', '_manually_load_plugin');
require "{$_tests_dir}/includes/bootstrap.php";
Factory Objects for Test Data
Using Built-in Factories:
<?php
class Test_Plugin_Integration extends WP_UnitTestCase {
public function test_create_post_with_meta() {
$post_id = $this->factory->post->create([
'post_title' => 'Test Post',
'post_content' => 'Test content for integration test',
'post_status' => 'publish',
'post_type' => 'post',
]);
$this->assertIsInt($post_id);
$this->assertGreaterThan(0, $post_id);
add_post_meta($post_id, '_custom_field', 'custom_value');
$meta_value = get_post_meta($post_id, '_custom_field', true);
$this->assertEquals('custom_value', $meta_value);
}
{
= ->factory->user->([
=> ,
=> ,
=> ,
]);
();
= ->factory->post->([
=> ,
]);
->((, ));
->(());
->(());
}
{
= ->factory->category->([
=> ,
=> ,
]);
= ->factory->post->();
(, []);
= ();
->(, );
}
{
= ->factory->post->();
= ->factory->comment->(, [
=> ,
=> ,
]);
->(, );
= ([ => ]);
->(, );
}
}
Available Factory Objects:
$this->factory->post - Posts, pages, custom post types
$this->factory->user - Users with roles
$this->factory->term - Terms (categories, tags, custom taxonomies)
$this->factory->category - Categories specifically
$this->factory->tag - Tags specifically
$this->factory->comment - Comments
$this->factory->blog - Multisite blogs
Database Fixtures and Teardown
setUp() and tearDown() Methods:
<?php
class Test_Custom_Post_Type extends WP_UnitTestCase {
protected $post_ids = [];
public function setUp(): void {
parent::setUp();
register_post_type('book', [
'public' => true,
'supports' => ['title', 'editor'],
]);
$this->post_ids = $this->factory->post->create_many(5, [
'post_type' => 'book',
]);
}
public function tearDown(): void {
foreach ($this->post_ids as $post_id) {
wp_delete_post($post_id, true);
}
();
::();
}
{
->(, ->post_ids);
= ([
=> ,
=> -,
]);
->(, ->found_posts);
}
}
setUpBeforeClass() and tearDownAfterClass():
<?php
class Test_Plugin_Database extends WP_UnitTestCase {
protected static $table_name;
public static function setUpBeforeClass(): void {
parent::setUpBeforeClass();
global $wpdb;
self::$table_name = $wpdb->prefix . 'plugin_data';
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE " . self::$table_name . " (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
user_id bigint(20) unsigned NOT NULL,
data_value varchar(255) NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY user_id (user_id)
) $charset_collate;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($sql);
}
public static function tearDownAfterClass(): {
;
->( . ::);
::();
}
{
;
= ->(
. :: .
);
->(::, );
}
{
;
= ->(
::,
[
=> ,
=> ,
],
[, ]
);
->(, );
->(, ->insert_id);
}
}
Complete Plugin Test Example
tests/test-plugin-functionality.php:
<?php
class Test_Plugin_Functionality extends WP_UnitTestCase {
public function test_custom_post_type_registered() {
$this->assertTrue(post_type_exists('book'));
$post_type = get_post_type_object('book');
$this->assertTrue($post_type->public);
$this->assertTrue($post_type->show_in_rest);
}
public function test_custom_taxonomy_registered() {
$this->assertTrue(taxonomy_exists('genre'));
$taxonomy = get_taxonomy('genre');
$this->assertTrue($taxonomy->hierarchical);
$this->assertContains('book', $taxonomy->object_type);
}
{
= ->factory->post->([
=> ,
=> ,
]);
(, , );
(, , );
(, , );
->(, (, , ));
->(, (, , ));
->(, (, , ));
}
{
= ->factory->post->([
=> ,
=> ,
]);
(, , );
= ( . . );
->(, );
->(, );
}
{
= ;
(, function() (&$) {
$ = ;
});
= ->factory->post->([
=> ,
=> ,
]);
(, );
->(, );
}
{
= (, );
= (, );
->(, ());
->(, );
}
}
WP_Mock Unit Testing
What is WP_Mock and When to Use It
WP_Mock Purpose:
- Test PHP code without loading WordPress
- Mock WordPress functions to return expected values
- Verify WordPress functions are called with correct arguments
- Much faster than integration tests (no database setup)
When to Use WP_Mock:
✅ Perfect for:
- Pure business logic that calls WordPress functions
- Data transformation/validation functions
- Service classes with WordPress dependencies
- Testing in continuous integration (faster CI builds)
❌ NOT Suitable for:
- Testing actual database operations
- Testing hook interactions between plugins
- Testing template rendering
- Testing functions that rely on WordPress state
Installation and Setup
composer require --dev mockery/mockery "^1.6"
composer require --dev 10up/wp_mock "^1.0"
composer require --dev phpunit/phpunit "^9.6"
tests/bootstrap-wp-mock.php:
<?php
require_once __DIR__ . '/../vendor/autoload.php';
WP_Mock::bootstrap();
if (!defined('ABSPATH')) {
define('ABSPATH', '/path/to/wordpress/');
}
phpunit-wp-mock.xml.dist:
<?xml version="1.0"?>
<phpunit
bootstrap="tests/bootstrap-wp-mock.php"
backupGlobals="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
>
<testsuites>
<testsuite name="unit">
<directory prefix="test-" suffix=".php">./tests/unit/</directory>
</testsuite>
</testsuites>
</phpunit>
Mocking WordPress Functions
tests/unit/test-data-processor.php:
<?php
use WP_Mock\Tools\TestCase;
class Test_Data_Processor extends TestCase {
public function setUp(): void {
WP_Mock::setUp();
}
public function tearDown(): void {
WP_Mock::tearDown();
}
public function test_sanitize_input() {
WP_Mock::userFunction('sanitize_text_field', [
'times' => 1,
'args' => ['<script>alert("xss")</script>'],
'return' => 'alert("xss")', // WordPress strips tags
]);
$processor = new MyPlugin\DataProcessor();
$result = $processor->sanitize_input('<script>alert("xss")</script>');
$this->assertEquals(, );
}
{
::(, [
=> ,
=> [, ],
=> ,
]);
= ();
= ->();
->(, );
}
{
= ;
::(, [
=> ,
=> [, , ],
=> ,
]);
::(, [
=> ,
=> [, , ],
=> ,
]);
= ();
= ->();
->(, );
}
{
::(, [
=> ,
=> [
,
::(),
],
=> ,
]);
= ();
= ->([ => ]);
->();
}
}
Mocking Filters and Actions
Testing add_filter() Calls:
<?php
class Test_Hook_Registration extends WP_Mock\Tools\TestCase {
public function setUp(): void {
WP_Mock::setUp();
}
public function tearDown(): void {
WP_Mock::tearDown();
}
public function test_content_filter_registered() {
WP_Mock::expectFilterAdded(
'the_content',
'MyPlugin\ContentFilter::add_reading_time',
10,
1
);
MyPlugin\Hooks::register_filters();
$this->assertConditionsMet();
}
public function test_init_action_registered() {
::(
,
,
,
);
::();
->();
}
{
= ;
= ;
::()
->()
->();
= ();
= ->();
->(, );
}
{
= ;
::(, );
= ();
->();
->();
}
}
Testing in Isolation (No WordPress Dependency)
Example: Email Service Class:
<?php
namespace MyPlugin;
class EmailService {
public function send_notification(string $to, string $message): bool {
$subject = $this->get_email_subject();
$headers = $this->get_email_headers();
return wp_mail($to, $subject, $message, $headers);
}
protected function get_email_subject(): string {
$site_name = get_bloginfo('name');
return sprintf('[%s] Notification', $site_name);
}
protected function get_email_headers(): array {
$admin_email = get_option('admin_email');
return [
'From: ' . $admin_email,
,
];
}
}
Unit Test Without WordPress:
<?php
use WP_Mock\Tools\TestCase;
class Test_Email_Service extends TestCase {
public function setUp(): void {
WP_Mock::setUp();
}
public function tearDown(): void {
WP_Mock::tearDown();
}
public function test_send_notification_email() {
WP_Mock::userFunction('get_bloginfo', [
'args' => 'name',
'return' => 'My WordPress Site',
]);
WP_Mock::userFunction('get_option', [
'args' => 'admin_email',
'return' => 'admin@example.com',
]);
WP_Mock::userFunction(, [
=> ,
=> [
,
,
,
::(),
],
=> ,
]);
= ();
= ->(
,
);
->();
}
{
::(, [
=> ,
]);
::(, [
=> ,
]);
::(, [
=> ,
]);
= ();
= ->(, );
->();
}
}
PHPCS & Coding Standards
Installing PHPCS and WPCS
via Composer (Recommended):
composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
composer require --dev wp-coding-standards/wpcs:"^3.0"
composer require --dev phpcompatibility/phpcompatibility-wp:"*"
composer require --dev squizlabs/php_codesniffer:"^3.7"
vendor/bin/phpcs -i
.phpcs.xml.dist Configuration
Complete Configuration File:
<?xml version="1.0"?>
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
name="WordPress Plugin Coding Standards"
xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/squizlabs/PHP_CodeSniffer/master/phpcs.xsd">
<description>Custom coding standards for WordPress plugin</description>
<file>./includes</file>
<file>./my-plugin.php</file>
<exclude-pattern>*/vendor/*</exclude-pattern>
<exclude-pattern>*/node_modules/*</exclude-pattern>
<exclude-pattern>*/tests/*</exclude-pattern>
<exclude-pattern>*/build/*</exclude-pattern>
<exclude-pattern>*/.git/*</exclude-pattern>
<arg value="ps"/>
<arg name="colors"/>
<arg = =/>
*/tests/*
error
Running PHPCS and PHPCBF
Command Line Usage:
vendor/bin/phpcs
vendor/bin/phpcs includes/Core.php
vendor/bin/phpcs -s
vendor/bin/phpcs -n
vendor/bin/phpcs --report=summary
vendor/bin/phpcs -v includes/Admin/Settings.php
vendor/bin/phpcbf
vendor/bin/phpcbf includes/Core.php
vendor/bin/phpcbf --dry-run
vendor/bin/phpcs --standard=WordPress-Core includes/
vendor/bin/phpcs --report=json > phpcs-report.json
vendor/bin/phpcs --report=xml > phpcs-report.xml
vendor/bin/phpcs --report=csv > phpcs-report.csv
composer.json Scripts:
{
"scripts": {
"phpcs": "phpcs",
"phpcbf": "phpcbf",
"phpcs:check": "phpcs --report=summary",
"phpcs:fix": "phpcbf",
"test": [
"@phpcs",
"phpunit"
]
}
}
Pre-commit Hooks
Install pre-commit hook (.git/hooks/pre-commit):
#!/bin/bash
FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '.php$')
if [ -z "$FILES" ]; then
echo "No PHP files to check"
exit 0
fi
echo "Running PHPCS on changed files..."
vendor/bin/phpcs $FILES
PHPCS_EXIT=$?
if [ $PHPCS_EXIT -ne 0 ]; then
echo ""
echo "PHPCS found coding standard violations."
echo "Run 'composer phpcbf' to auto-fix issues."
echo ""
exit 1
fi
echo "PHPCS passed!"
exit 0
Make hook executable:
chmod +x .git/hooks/pre-commit
IDE Integration
Visual Studio Code (.vscode/settings.json):
{
"phpcs.enable": true,
"phpcs.standard": "WordPress",
"phpcs.executablePath": "${workspaceFolder}/vendor/bin/phpcs",
"phpcbf.enable": true,
"phpcbf.executablePath": "${workspaceFolder}/vendor/bin/phpcbf",
"phpcbf.onsave": false,
"editor.formatOnSave": false,
"[php]": {
"editor.defaultFormatter": "bmewburn.vscode-intelephense-client",
"editor.formatOnSave": true
}
}
PHPStorm Configuration:
- Go to Settings → PHP → Quality Tools → PHP_CodeSniffer
- Set Configuration path:
{PROJECT_ROOT}/vendor/bin/phpcs
- Go to Settings → Editor → Inspections → PHP → Quality Tools
- Enable "PHP_CodeSniffer validation"
- Set Coding standard: "Custom"
- Set Path:
{PROJECT_ROOT}/.phpcs.xml.dist
GitHub Actions CI/CD
Workflow File Structure
.github/workflows/tests.yml:
name: Test Suite
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
phpcs:
name: PHPCS
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer
coverage: none
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-suggest
- name: Run PHPCS
run: vendor/bin/phpcs --report=summary
[, , ]
[, , , , ]
Matrix Testing (Multiple PHP/WP Versions)
Strategy Explanation:
strategy:
fail-fast: false
matrix:
php: ['8.1', '8.2', '8.3']
wordpress: ['6.4', '6.5', '6.6', '6.7', 'latest']
include:
- php: '8.3'
wordpress: 'trunk'
exclude:
- php: '8.1'
wordpress: 'trunk'
Matrix Results:
- Creates 18 test jobs (3 PHP × 6 WordPress versions)
- Ensures compatibility across supported versions
- Identifies version-specific issues early
PHPCS Checks in CI
Dedicated PHPCS Job:
phpcs-detailed:
name: Detailed PHPCS Report
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer, cs2pr
- name: Install dependencies
run: composer install --prefer-dist --no-progress
- name: Run PHPCS with annotations
run: vendor/bin/phpcs -q --report=checkstyle | cs2pr
- name: Generate PHPCS report
if: failure()
run: vendor/bin/phpcs --report=summary --report-file=phpcs-report.txt
-
PHPUnit Test Execution
With Code Coverage:
phpunit-coverage:
name: PHPUnit with Coverage
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: wordpress_test
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s
steps:
- uses: actions/checkout@v4
- name: Setup PHP with Xdebug
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: mysqli, zip, gd
tools: composer
coverage: xdebug
ini-values: xdebug.mode=coverage
- name: Install dependencies
run: composer
Coverage Reporting
Codecov Integration:
- name: Upload to Codecov
uses: codecov/codecov-action@v4
with:
files: ./coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: true
verbose: true
Coveralls Integration:
- name: Upload to Coveralls
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
path-to-lcov: ./coverage.xml
Complete Workflow Example
.github/workflows/ci.yml (Production-Ready):
name: CI Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
schedule:
- cron: '0 0 * * 0'
jobs:
coding-standards:
name: Coding Standards
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer, cs2pr
- run: composer install --prefer-dist --no-progress
- run: vendor/bin/phpcs -q --report=checkstyle | cs2pr
unit-tests:
name: Unit Tests (WP_Mock)
runs-on: ubuntu-latest
[, ]
[, ]
[, , ]
Testing Best Practices
Test Naming Conventions
Method Naming Pattern:
test_[method_name]_[scenario]_[expected_result]
Examples:
public function test_sanitize_email_with_valid_email_returns_email() {}
public function test_sanitize_email_with_invalid_email_returns_empty_string() {}
public function test_save_post_meta_with_valid_data_returns_true() {}
public function test_user_login_with_wrong_password_returns_wp_error() {}
public function test_email() {}
public function test_function() {}
public function test_it_works() {}
Class Naming:
class Test_Email_Service extends WP_UnitTestCase {}
class Test_Data_Validator extends WP_Mock\Tools\TestCase {}
class Test_Post_Meta_Handler extends WP_UnitTestCase {}
Arrange-Act-Assert Pattern
Structure Every Test:
public function test_calculate_discount() {
$original_price = 100;
$discount_percent = 20;
$calculator = new MyPlugin\PriceCalculator();
$discounted_price = $calculator->apply_discount($original_price, $discount_percent);
$this->assertEquals(80, $discounted_price);
}
Complete Example:
public function test_save_user_preferences_updates_database() {
$user_id = $this->factory->user->create();
$preferences = [
'theme' => 'dark',
'notifications' => true,
];
$service = new MyPlugin\UserPreferences();
$result = $service->save_preferences($user_id, $preferences);
$this->assertTrue($result);
$saved_prefs = get_user_meta($user_id, 'preferences', true);
$this->assertEquals('dark', $saved_prefs['theme']);
$this->assertTrue($saved_prefs['notifications']);
}
Data Providers
Purpose: Test same logic with multiple inputs
public function test_email_validation($email, $expected) {
$validator = new MyPlugin\Validator();
$result = $validator->is_valid_email($email);
$this->assertEquals($expected, $result);
}
public function email_validation_provider(): array {
return [
'valid email' => ['user@example.com', true],
'invalid no at' => ['userexample.com', false],
'invalid no domain' => ['user@', false],
'invalid spaces' => ['user @example.com', false],
'valid subdomain' => ['user@mail.example.com', true],
'invalid special chars' => ['user#@example.com', false],
];
}
Complex Data Provider:
public function test_discount_calculation($price, $discount, $expected) {
$calculator = new MyPlugin\PriceCalculator();
$result = $calculator->apply_discount($price, $discount);
$this->assertEquals($expected, $result);
}
public function discount_calculation_provider(): array {
return [
'20% off 100' => [100, 20, 80],
'50% off 100' => [100, 50, 50],
'0% off 100' => [100, 0, 100],
'100% off 100' => [100, 100, 0],
'20% off 0' => [0, 20, 0],
];
}
Testing Hooks and Filters
Testing add_action/add_filter:
public function test_init_hooks_registered() {
remove_all_actions('init');
MyPlugin\Hooks::register();
$this->assertTrue(has_action('init', 'MyPlugin\PostTypes::register'));
$this->assertEquals(10, has_action('init', 'MyPlugin\PostTypes::register'));
}
public function test_content_filter_registered() {
remove_all_filters('the_content');
MyPlugin\Hooks::register();
$this->assertTrue(has_filter('the_content', 'MyPlugin\Content::add_reading_time'));
}
Testing Hook Callbacks:
public function test_save_post_hook_saves_meta() {
$post_id = $this->factory->post->create([
'post_type' => 'book',
]);
$_POST['book_isbn'] = '978-3-16-148410-0';
$_POST['book_nonce'] = wp_create_nonce('save_book_meta');
do_action('save_post', $post_id);
$isbn = get_post_meta($post_id, '_isbn', true);
$this->assertEquals('978-3-16-148410-0', $isbn);
}
Testing AJAX Handlers
AJAX Test Setup:
public function test_ajax_load_more_posts() {
$post_ids = $this->factory->post->create_many(5);
$_POST['action'] = 'load_more_posts';
$_POST['page'] = 1;
$_POST['nonce'] = wp_create_nonce('load_more_nonce');
wp_set_current_user($this->factory->user->create(['role' => 'subscriber']));
try {
$this->_handleAjax('load_more_posts');
} catch (WPAjaxDieContinueException $e) {
}
$response = json_decode($this->_last_response, true);
$this->assertTrue($response['success']);
$this->assertCount(5, $response[][]);
}
Common Testing Patterns
Testing Custom Post Types
class Test_Book_Post_Type extends WP_UnitTestCase {
public function setUp(): void {
parent::setUp();
MyPlugin\PostTypes::register_book();
}
public function test_book_post_type_exists() {
$this->assertTrue(post_type_exists('book'));
}
public function test_book_supports_features() {
$post_type = get_post_type_object('book');
$this->assertTrue(post_type_supports('book', 'title'));
$this->assertTrue(post_type_supports('book', 'editor'));
$this->assertTrue(post_type_supports('book', 'thumbnail'));
$this->assertFalse((, ));
}
{
= ();
->(->show_in_rest);
}
{
= ->factory->post->([
=> ,
=> ,
]);
= ();
->(, ->post_type);
->(, ->post_title);
}
}
Testing Settings/Options
class Test_Plugin_Settings extends WP_UnitTestCase {
public function tearDown(): void {
delete_option('my_plugin_settings');
parent::tearDown();
}
public function test_default_settings_created() {
$settings = MyPlugin\Settings::get_defaults();
$this->assertIsArray($settings);
$this->assertArrayHasKey('api_key', $settings);
$this->assertEquals('', $settings['api_key']);
}
public function test_save_settings() {
$new_settings = [
'api_key' => 'test_key_123',
'enabled' => true,
];
$result = MyPlugin\Settings::save($new_settings);
$this->();
= ();
->(, []);
->([]);
}
{
= [
=> ,
=> ,
];
= ::();
->(, []);
->([]);
}
}
Testing Database Operations
class Test_Database_Operations extends WP_UnitTestCase {
protected static $table_name;
public static function setUpBeforeClass(): void {
parent::setUpBeforeClass();
global $wpdb;
self::$table_name = $wpdb->prefix . 'plugin_logs';
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE " . self::$table_name . " (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
user_id bigint(20) unsigned NOT NULL,
action varchar(50) NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
) $charset_collate;";
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
dbDelta($sql);
}
public static function tearDownAfterClass(): void {
global $wpdb;
$wpdb->query("DROP TABLE IF EXISTS " . ::);
::();
}
{
;
= ;
= ;
= ->(
::,
[
=> ,
=> ,
],
[, ]
);
->(, );
->(, ->insert_id);
= ->(
->(
. :: . ,
->insert_id
)
);
->(, ->user_id);
->(, ->action);
}
{
;
= ;
->(::, [ => , => ], [, ]);
->(::, [ => , => ], [, ]);
= ->(
->(
. :: . ,
)
);
->(, );
}
}
Testing REST API Endpoints
class Test_REST_API extends WP_UnitTestCase {
protected $server;
public function setUp(): void {
parent::setUp();
global $wp_rest_server;
$this->server = $wp_rest_server = new WP_REST_Server();
do_action('rest_api_init');
}
public function test_endpoint_registered() {
$routes = $this->server->get_routes();
$this->assertArrayHasKey('/myplugin/v1/items', $routes);
}
public function test_get_items_endpoint() {
$post_ids = $this->factory->post->create_many(3, ['post_type' => 'book']);
$request = new WP_REST_Request('GET', );
= ->server->();
->(, ->());
= ->();
->(, );
}
{
= (, );
->([
=> ,
]);
= ->server->();
->(, ->());
}
{
= ->factory->user->([ => ]);
();
= (, );
->([
=> ,
=> ,
]);
= ->server->();
->(, ->());
= ->();
->(, []);
}
}
Related Skills:
When testing WordPress applications, consider these complementary skills (available in the skill library):
- WordPress Plugin Fundamentals: Core plugin architecture and hooks - essential foundation for understanding what to test
- WordPress Security & Validation: Security patterns and data validation - critical for security testing strategies
- Python pytest Testing: Modern testing patterns - concepts applicable to WordPress testing approaches
- GitHub Actions CI/CD: CI/CD automation - integrate WordPress tests into automated pipelines
Further Reading: