Automated testing ensures your WordPress plugin works reliably across updates, prevents regressions, and gives you confidence to refactor code. PHPUnit combined with the WordPress test framework provides a robust solution for testing plugin functionality.
Why Test WordPress Plugins
Manual testing becomes impractical as plugins grow. Automated tests catch bugs before users do, document expected behavior, and enable safe refactoring. Tests save time in the long run by preventing regressions and reducing debugging sessions.
Well-tested plugins are more maintainable, easier to collaborate on, and inspire user confidence. Professional development includes comprehensive testing as a standard practice.
Understanding Test Types
Unit Tests: Test individual functions or methods in isolation. Fast to run and pinpoint specific issues.
Integration Tests: Test how components work together, including WordPress functions, database operations, and plugin interactions.
Acceptance Tests: Test complete user workflows from end to end. Slower but verify real-world usage.
For WordPress plugins, you’ll primarily write integration tests since most functionality interacts with WordPress core.
Setting Up PHPUnit for WordPress
Install PHPUnit and set up the WordPress test suite using WP-CLI:
# Navigate to your plugin directory
cd wp-content/plugins/my-plugin
# Generate test scaffolding
wp scaffold plugin-tests my-plugin
# Install test suite
bash bin/install-wp-tests.sh wordpress_test root '' localhost latestThis creates the necessary directory structure:
my-plugin/
├── bin/
│ └── install-wp-tests.sh
├── tests/
│ ├── bootstrap.php
│ └── test-sample.php
├── phpunit.xml.dist
└── .phpcs.xml.dist
Writing Your First Test
Create a test file in the tests/ directory:
<?php
class Test_MyPlugin_Functions extends WP_UnitTestCase {
public function test_plugin_activated() {
$this->assertTrue(is_plugin_active('my-plugin/my-plugin.php'));
}
public function test_custom_function_returns_expected_value() {
$result = myplugin_get_greeting('John');
$this->assertEquals('Hello, John!', $result);
}
public function test_custom_post_type_registered() {
$this->assertTrue(post_type_exists('myplugin_item'));
}
}Run tests from your plugin directory:
phpunitPHPUnit Assertions
Common assertions for testing WordPress plugins:
// Equality
$this->assertEquals($expected, $actual);
$this->assertSame($expected, $actual); // Strict comparison
// Boolean
$this->assertTrue($condition);
$this->assertFalse($condition);
// Null and Empty
$this->assertNull($value);
$this->assertEmpty($array);
$this->assertNotEmpty($array);
// Arrays and Strings
$this->assertContains('needle', $haystack);
$this->assertCount(5, $array);
$this->assertStringContainsString('word', $string);
// Instances and Types
$this->assertInstanceOf(WP_Post::class, $post);
$this->assertIsArray($value);
$this->assertIsString($value);
// WordPress-specific
$this->assertWPError($result);
$this->assertNotWPError($result);Testing WordPress Functions
Test interactions with WordPress core:
class Test_Post_Functions extends WP_UnitTestCase {
public function test_create_custom_post() {
$post_id = wp_insert_post(array(
'post_title' => 'Test Post',
'post_type' => 'myplugin_item',
'post_status' => 'publish'
));
$this->assertGreaterThan(0, $post_id);
$this->assertEquals('myplugin_item', get_post_type($post_id));
}
public function test_post_meta_saved_correctly() {
$post_id = $this->factory()->post->create();
add_post_meta($post_id, '_myplugin_rating', 4.5);
$rating = get_post_meta($post_id, '_myplugin_rating', true);
$this->assertEquals(4.5, $rating);
}
}Using Factories for Test Data
WordPress test framework includes factories for generating test data:
class Test_With_Factories extends WP_UnitTestCase {
public function test_user_can_edit_own_post() {
// Create test user
$user_id = $this->factory()->user->create(array(
'role' => 'author'
));
// Create test post owned by user
$post_id = $this->factory()->post->create(array(
'post_author' => $user_id
));
wp_set_current_user($user_id);
$this->assertTrue(current_user_can('edit_post', $post_id));
}
public function test_category_assignment() {
$category_id = $this->factory()->category->create(array(
'name' => 'Test Category'
));
$post_id = $this->factory()->post->create();
wp_set_post_categories($post_id, array($category_id));
$categories = wp_get_post_categories($post_id);
$this->assertContains($category_id, $categories);
}
}Available factories:
$this->factory()->post– Posts$this->factory()->user– Users$this->factory()->comment– Comments$this->factory()->term– Terms$this->factory()->category– Categories$this->factory()->tag– Tags
setUp() and tearDown() Methods
Prepare test environment before each test and clean up after:
class Test_With_Setup extends WP_UnitTestCase {
protected $plugin;
protected $test_post_id;
public function setUp(): void {
parent::setUp();
// Runs before each test
$this->plugin = new MyPlugin();
$this->test_post_id = $this->factory()->post->create();
}
public function tearDown(): void {
// Runs after each test
wp_delete_post($this->test_post_id, true);
unset($this->plugin);
parent::tearDown();
}
public function test_plugin_initialized() {
$this->assertInstanceOf(MyPlugin::class, $this->plugin);
}
public function test_post_exists() {
$this->assertNotNull(get_post($this->test_post_id));
}
}Testing Hooks and Filters
Verify actions and filters execute correctly:
class Test_Hooks extends WP_UnitTestCase {
public function test_action_adds_meta_box() {
global $wp_meta_boxes;
do_action('add_meta_boxes');
$this->assertArrayHasKey('myplugin_meta_box',
$wp_meta_boxes['post']['normal']['high']);
}
public function test_filter_modifies_title() {
add_filter('the_title', 'myplugin_modify_title');
$post_id = $this->factory()->post->create(array(
'post_title' => 'Original Title'
));
$title = apply_filters('the_title', get_the_title($post_id));
$this->assertStringContainsString('Modified', $title);
}
public function test_custom_filter_value() {
$value = apply_filters('myplugin_custom_filter', 10);
// Default value
$this->assertEquals(10, $value);
// Add filter
add_filter('myplugin_custom_filter', function($val) {
return $val * 2;
});
$filtered_value = apply_filters('myplugin_custom_filter', 10);
$this->assertEquals(20, $filtered_value);
}
}Testing AJAX Functionality
Test AJAX handlers using WordPress test helpers:
class Test_AJAX extends WP_Ajax_UnitTestCase {
public function test_ajax_like_post() {
$post_id = $this->factory()->post->create();
$_POST['action'] = 'myplugin_like_post';
$_POST['post_id'] = $post_id;
$_POST['nonce'] = wp_create_nonce('myplugin_like_nonce');
try {
$this->_handleAjax('myplugin_like_post');
} catch (WPAjaxDieContinueException $e) {
// Expected exception
}
$response = json_decode($this->_last_response);
$this->assertTrue($response->success);
$this->assertEquals(1, $response->data->likes);
}
}Testing Database Operations
Test custom database tables and queries:
class Test_Database extends WP_UnitTestCase {
public function test_custom_table_created() {
global $wpdb;
myplugin_create_custom_table();
$table_name = $wpdb->prefix . 'myplugin_data';
$this->assertEquals($table_name,
$wpdb->get_var("SHOW TABLES LIKE '$table_name'"));
}
public function test_insert_custom_data() {
global $wpdb;
$table_name = $wpdb->prefix . 'myplugin_data';
$result = $wpdb->insert($table_name, array(
'name' => 'Test Item',
'value' => 100
));
$this->assertNotFalse($result);
$this->assertEquals(1, $wpdb->insert_id);
}
}Mocking External API Calls
Use pre_http_request filter to mock external APIs:
class Test_External_API extends WP_UnitTestCase {
public function test_api_call_returns_data() {
// Mock the HTTP request
add_filter('pre_http_request', function($response, $args, $url) {
if (strpos($url, 'api.example.com') !== false) {
return array(
'response' => array('code' => 200),
'body' => json_encode(array(
'status' => 'success',
'data' => array('value' => 42)
))
);
}
return $response;
}, 10, 3);
$result = myplugin_fetch_external_data();
$this->assertEquals(42, $result['value']);
}
}Code Coverage Analysis
Generate code coverage reports to identify untested code:
phpunit --coverage-html coverage/View the HTML report in coverage/index.html. Aim for at least 70% coverage for critical code paths.
Configure coverage in phpunit.xml:
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./src</directory>
</include>
<exclude>
<directory>./vendor</directory>
<directory>./tests</directory>
</exclude>
</coverage>Continuous Integration with GitHub Actions
Automate testing on every commit:
# .github/workflows/test.yml
name: PHPUnit Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
php: ["7.4", "8.0", "8.1"]
wordpress: ["latest", "6.0"]
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
- name: Install dependencies
run: composer install
- name: Install WordPress Test Suite
run: bash bin/install-wp-tests.sh wordpress_test root root localhost ${{ matrix.wordpress }}
- name: Run PHPUnit
run: phpunitTest-Driven Development (TDD)
Follow the Red-Green-Refactor cycle:
- Red: Write a failing test for new functionality
- Green: Write minimal code to make the test pass
- Refactor: Improve code while keeping tests green
Example TDD workflow:
// Step 1: Write failing test
public function test_calculate_discount() {
$result = myplugin_calculate_discount(100, 10);
$this->assertEquals(90, $result);
}
// Step 2: Implement function
function myplugin_calculate_discount($price, $discount) {
return $price - $discount;
}
// Step 3: Refactor
function myplugin_calculate_discount($price, $discount_percent) {
return $price * (1 - $discount_percent / 100);
}Best Practices
Test Organization: Group related tests in classes. Use descriptive test method names that explain what’s being tested.
Independence: Each test should run independently. Don’t rely on test execution order or shared state.
Speed: Keep tests fast. Use transactions to roll back database changes automatically. Mock external dependencies.
Clarity: Write clear assertions with helpful failure messages. One logical assertion per test method.
Maintenance: Update tests alongside code changes. Treat test code with the same care as production code.
Automated testing transforms plugin development from reactive debugging to proactive quality assurance. Invest time in testing now to save exponentially more time later.
- Why testing WordPress plugins matters
- Types of tests: unit, integration, acceptance
- Understanding PHPUnit testing framework
- Installing PHPUnit for WordPress
- WordPress core test suite setup
- Creating test scaffolding with WP-CLI
- wp scaffold plugin-tests command
- Test directory structure
- bootstrap.php configuration
- Writing your first PHPUnit test
- Test case classes extending WP_UnitTestCase
- Test method naming conventions
- Assertions in PHPUnit
- assertEquals(), assertTrue(), assertFalse()
- assertCount(), assertEmpty(), assertContains()
- Testing WordPress functions and hooks
- Testing actions and filters
- Mock objects and method stubs
- WordPress factory for test data
- Creating test posts, users, and terms
- setUp() and tearDown() methods
- Database transactions in tests
- Test fixtures and sample data
- Testing AJAX functionality
- Testing custom post types
- Testing meta boxes and custom fields
- Testing shortcodes
- Testing widgets
- Testing REST API endpoints
- Testing database queries
- Testing caching logic
- Code coverage analysis
- Generating coverage reports
- Improving test coverage
- Test-driven development (TDD) workflow
- Writing tests before code
- Red-Green-Refactor cycle
- Continuous integration setup
- GitHub Actions for WordPress testing
- Travis CI configuration
- GitLab CI/CD pipelines
- Automated testing on pull requests
- Testing multiple PHP versions
- Testing multiple WordPress versions
- Integration testing best practices
- Mocking external API calls
- Performance testing
- Debugging failing tests
- Common testing pitfalls
- Real-world testing examples
Includes complete test examples, CI/CD configurations, and best practices for building reliable, well-tested WordPress plugins with comprehensive test coverage.
External Links
- PHPUnit Documentation
- WordPress PHPUnit Test Suite
- WP-CLI Testing Commands
- GitHub Actions for WordPress
- WordPress Test Library
Call to Action
Supercharge your development! ACF Copilot Pro generates ACF field groups with AI, exports to PHP, and accelerates custom field workflows—try it free!

