Skip to main content
skill CX Codex

wordpress-testing-qa — Claude Code skill for WordPress tests

A skill for testing WordPress plugins and themes: PHPUnit integration, WP_Mock, PHPCS and CI/CD. Useful when your test setup needs structure.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add bobmatnyc/claude-mpm-skills --skill wordpress-testing-qa -a claude-code
Open on GitHub

What wordpress-testing-qa does

The skill lays out the WordPress testing pyramid: unit tests on WP_Mock, integration tests on PHPUnit with core loaded, and E2E on Playwright. It walks through the setup: installing dependencies with composer, generating a scaffold via wp scaffold plugin-tests my-plugin, installing the WordPress test library with bin/install-wp-tests.sh, then configuring phpunit.xml.dist and tests/bootstrap.php.

There is a clear split between the two runners. Database work, hooks and template output need PHPUnit with a real WordPress instance. Pure logic goes to WP_Mock, where no database is started and tests finish faster. Coverage targets are spelled out too: 80% for new code, 95% for critical paths such as payment processing and authentication, 100% for public methods.

The skill does not fire on its own. It ships with disable-model-invocation: true and user-invocable: false, so you have to reach for it deliberately instead of hoping the agent picks it up. PHP, Composer and WP-CLI are prerequisites — the documented steps assume all three are present.

What it helps with

  • set up the WordPress test library and a separate test database
  • split unit and integration tests so the database is not started on every run
  • configure phpunit.xml.dist with coverage and Yoast polyfills
  • wire up WPCS through PHPCS and keep code inside the standard
  • create test data with factories instead of hand-written fixtures

Example prompts for the agent

  • help me set up PHPUnit for my WordPress plugin
  • write an integration test for creating a post with meta
  • which tests should use WP_Mock and which should use PHPUnit?

How to install wordpress-testing-qa

1Skills CLI (Vercel)

npx skills add bobmatnyc/claude-mpm-skills --skill wordpress-testing-qa -a claude-code

2Manually: put the files in place

~/.claude/skills/wordpress-testing-qa/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/wordpress-testing-qa/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about wordpress-testing-qa

How do I install wordpress-testing-qa in Claude?

Run npx skills add bobmatnyc/claude-mpm-skills --skill wordpress-testing-qa -a claude-code. The skill file then lands at ~/.claude/skills/wordpress-testing-qa/SKILL.md.

What does wordpress-testing-qa do?

It covers testing WordPress plugins and themes: PHPUnit integration tests, WP_Mock unit tests, coding standards checks with PHPCS, and CI/CD workflows.

Does it work outside Claude?

Yes. The same skill installs into Codex, Cursor, GitHub Copilot, Gemini CLI and OpenCode, each with its own install command and skill path.

Source text

SKILL.md · MIT
---
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):**
1. **Security Functions:** Nonce verification, sanitization, capability checks
2. **Data Operations:** Database CRUD, data validation, transformation
3. **Business Logic:** Calculations, workflows, state transitions
4. **Hook Callbacks:** Action/filter handlers
5. **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**

```bash
# Install PHPUnit and WordPress polyfills
composer require --dev phpunit/phpunit "^9.6"
composer require --dev yoast/phpunit-polyfills "^2.0"

# Generate test scaffold with WP-CLI
wp scaffold plugin-tests my-plugin

# This creates:
# - tests/bootstrap.php
# - tests/test-sample.php
# - phpunit.xml.dist
# - bin/install-wp-tests.sh
```

**Step 2: Install WordPress Test Library**

```bash
# Install WordPress test suite and test database
# Syntax: bash bin/install-wp-tests.sh <db-name> <db-user> <db-pass> <db-host> <wp-version>
bash bin/install-wp-tests.sh wordpress_test root '' localhost latest

# For specific WordPress version:
bash bin/install-wp-tests.sh wordpress_test root '' localhost 6.7
```

**Step 3: Configure phpunit.xml.dist**

```xml
<?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/</directory>
            <directory>./tests/</directory>
        </exclude>
        <report>
            <html outputDirectory="coverage-html"/>
            <text outputFile="php://stdout" showOnlySummary="true"/>
        </report>
    </coverage>

    <php>
        <const name="WP_TESTS_PHPUNIT_POLYFILLS_PATH" value="vendor/yoast/phpunit-polyfills"/>
    </php>
</phpunit>
```

### WP_UnitTestCase Base Class

**tests/bootstrap.php:**

```php
<?php
/**
 * PHPUnit bootstrap file
 */

// Composer autoloader
require_once dirname(__DIR__) . '/vendor/autoload.php';

// WordPress tests directory
$_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");
}

// Give access to tests_add_filter() function
require_once "{$_tests_dir}/includes/functions.php";

/**
 * Manually load the plugin being tested
 */
function _manually_load_plugin() {
    require dirname(__DIR__) . '/my-plugin.php';
}
tests_add_filter('muplugins_loaded', '_manually_load_plugin');

// Start up the WordPress testing environment
require "{$_tests_dir}/includes/bootstrap.php";
```

### Factory Objects for Test Data

**Using Built-in Factories:**

```php
<?php
class Test_Plugin_Integration extends WP_UnitTestCase {

    /**
     * Test creating posts with factory
     */
    public function test_create_post_with_meta() {
        // Create a post using factory
        $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
        add_post_meta($post_id, '_custom_field', 'custom_value');

        // Verify meta was saved
        $meta_value = get_post_meta($post_id, '_custom_field', true);
        $this->assertEquals('custom_value', $meta_value);
    }

    /**
     * Test creating users
     */
    public function test_user_can_edit_post() {
        // Create editor user
        $editor_id = $this->factory->user->create([
            'role' => 'editor',
            'user_login' => 'test_editor',
            'user_email' => '[email protected]',
        ]);

        // Set as current user
        wp_set_current_user($editor_id);

        // Create post
        $post_id = $this->factory->post->create([
            'post_author' => $editor_id,
        ]);

        // Test capabilities
        $this->assertTrue(current_user_can('edit_post', $post_id));
        $this->assertTrue(current_user_can('edit_posts'));
        $this->assertFalse(current_user_can('manage_options'));
    }

    /**
     * Test creating terms and taxonomy
     */
    public function test_assign_categories() {
        // Create category
        $category_id = $this->factory->category->create([
            'name' => 'Test Category',
            'slug' => 'test-category',
        ]);

        // Create post
        $post_id = $this->factory->post->create();

        // Assign category
        wp_set_post_categories($post_id, [$category_id]);

        // Verify assignment
        $categories = wp_get_post_categories($post_id);
        $this->assertContains($category_id, $categories);
    }

    /**
     * Test creating comments
     */
    public function test_post_has_comments() {
        $post_id = $this->factory->post->create();

        // Create multiple comments
        $comment_ids = $this->factory->comment->create_many(3, [
            'comment_post_ID' => $post_id,
            'comment_approved' => 1,
        ]);

        $this->assertCount(3, $comment_ids);

        // Get comments for post
        $comments = get_comments(['post_id' => $post_id]);
        $this->assertCount(3, $comments);
    }
}
```

**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
<?php
class Test_Custom_Post_Type extends WP_UnitTestCase {

    protected $post_ids = [];

    /**
     * Setup runs before EACH test method
     */
    public function setUp(): void {
        parent::setUp();

        // Register custom post type
        register_post_type('book', [
            'public' => true,
            'supports' => ['title', 'editor'],
        ]);

        // Create test data
        $this->post_ids = $this->factory->post->create_many(5, [
            'post_type' => 'book',
        ]);
    }

    /**
     * Teardown runs after EACH test method
     */
    public function tearDown(): void {
        // Clean up test data
        foreach ($this->post_ids as $post_id) {
            wp_delete_post($post_id, true); // Force delete
        }

        // Unregister post type
        unregister_post_type('book');

        parent::tearDown();
    }

    /**
     * Test that books are created
     */
    public function test_books_created() {
        $this->assertCount(5, $this->post_ids);

        $query = new WP_Query([
            'post_type' => 'book',
            'posts_per_page' => -1,
        ]);

        $this->assertEquals(5, $query->found_posts);
    }
}
```

**setUpBeforeClass() and tearDownAfterClass():**

```php
<?php
class Test_Plugin_Database extends WP_UnitTestCase {

    protected static $table_name;

    /**
     * Runs ONCE before all tests in class
     */
    public static function setUpBeforeClass(): void {
        parent::setUpBeforeClass();

        global $wpdb;
        self::$table_name = $wpdb->prefix . 'plugin_data';

        // Create custom table
        $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);
    }

    /**
     * Runs ONCE after all tests in class
     */
    public static function tearDownAfterClass(): void {
        global $wpdb;
        $wpdb->query("DROP TABLE IF EXISTS " . self::$table_name);

        parent::tearDownAfterClass();
    }

    /**
     * Test table exists
     */
    public function test_custom_table_exists() {
        global $wpdb;
        $table_exists = $wpdb->get_var(
            "SHOW TABLES LIKE '" . self::$table_name . "'"
        );
        $this->assertEquals(self::$table_name, $table_exists);
    }

    /**
     * Test insert data
     */
    public function test_insert_data() {
        global $wpdb;

        $result = $wpdb->insert(
            self::$table_name,
            [
                'user_id' => 1,
                'data_value' => 'test_value',
            ],
            ['%d', '%s']
        );

        $this->assertEquals(1, $result);
        $this->assertGreaterThan(0, $wpdb->insert_id);
    }
}
```

### Complete Plugin Test Example

**tests/test-plugin-functionality.php:**

```php
<?php
/**
 * Test plugin core functionality
 */
class Test_Plugin_Functionality extends WP_UnitTestCase {

    /**
     * Test plugin registers custom post type
     */
    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);
    }

    /**
     * Test custom taxonomy registration
     */
    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);
    }

    /**
     * Test saving custom meta data
     */
    public function test_save_book_metadata() {
        $book_id = $this->factory->post->create([
            'post_type' => 'book',
            'post_title' => 'Test Book',
        ]);

        // Simulate saving meta (as would happen in save_post hook)
        update_post_meta($book_id, '_isbn', '978-3-16-148410-0');
        update_post_meta($book_id, '_author', 'John Doe');
        update_post_meta($book_id, '_publication_year', 2024);

        // Verify meta saved correctly
        $this->assertEquals('978-3-16-148410-0', get_post_meta($book_id, '_isbn', true));
        $this->assertEquals('John Doe', get_post_meta($book_id, '_author', true));
        $this->assertEquals(2024, get_post_meta($book_id, '_publication_year', true));
    }

    /**
     * Test shortcode output
     */
    public function test_book_shortcode_output() {
        $book_id = $this->factory->post->create([
            'post_type' => 'book',
            'post_title' => 'The Great Gatsby',
        ]);

        update_post_meta($book_id, '_author', 'F. Scott Fitzgerald');

        // Test shortcode
        $output = do_shortcode('[book id="' . $book_id . '"]');

        $this->assertStringContainsString('The Great Gatsby', $output);
        $this->assertStringContainsString('F. Scott Fitzgerald', $output);
    }

    /**
     * Test action hook fires correctly
     */
    public function test_book_published_action_fires() {
        $action_fired = false;

        // Add temporary hook to verify action fires
        add_action('my_plugin_book_published', function($post_id) use (&$action_fired) {
            $action_fired = true;
        });

        // Create published book (should trigger action)
        $book_id = $this->factory->post->create([
            'post_type' => 'book',
            'post_status' => 'publish',
        ]);

        // Manually trigger the action (simulating what plugin does)
        do_action('my_plugin_book_published', $book_id);

        $this->assertTrue($action_fired, 'Book published action did not fire');
    }

    /**
     * Test filter modifies content
     */
    public function test_reading_time_filter() {
        $content = str_repeat('word ', 200); // 200 words

        // Apply filter
        $filtered = apply_filters('my_plugin_content_filter', $content);

        $this->assertStringContainsString('reading time', strtolower($filtered));
        $this->assertStringContainsString('1 min', $filtered);
    }
}
```

---

## 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

```bash
# Install WP_Mock and Mockery
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
<?php
/**
 * Bootstrap file for WP_Mock tests
 */

require_once __DIR__ . '/../vendor/autoload.php';

// WP_Mock setup
WP_Mock::bootstrap();

// Define WordPress constants if needed
if (!defined('ABSPATH')) {
    define('ABSPATH', '/path/to/wordpress/');
}
```

**phpunit-wp-mock.xml.dist:**

```xml
<?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
<?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();
    }

    /**
     * Test sanitization function
     */
    public function test_sanitize_input() {
        // Mock sanitize_text_field
        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('alert("xss")', $result);
    }

    /**
     * Test get_option is called
     */
    public function test_get_setting() {
        // Mock get_option call
        WP_Mock::userFunction('get_option', [
            'times' => 1,
            'args' => ['my_plugin_api_key', ''],
            'return' => 'test_api_key_12345',
        ]);

        $processor = new MyPlugin\DataProcessor();
        $api_key = $processor->get_api_key();

        $this->assertEquals('test_api_key_12345', $api_key);
    }

    /**
     * Test multiple function calls with different returns
     */
    public function test_user_data_retrieval() {
        $user_id = 42;

        // Mock get_user_meta
        WP_Mock::userFunction('get_user_meta', [
            'times' => 1,
            'args' => [$user_id, 'first_name', true],
            'return' => 'John',
        ]);

        WP_Mock::userFunction('get_user_meta', [
            'times' => 1,
            'args' => [$user_id, 'last_name', true],
            'return' => 'Doe',
        ]);

        $processor = new MyPlugin\DataProcessor();
        $full_name = $processor->get_user_full_name($user_id);

        $this->assertEquals('John Doe', $full_name);
    }

    /**
     * Test function with type matcher
     */
    public function test_save_data_with_array() {
        // Accept any array as second argument
        WP_Mock::userFunction('update_option', [
            'times' => 1,
            'args' => [
                'my_plugin_settings',
                WP_Mock\Functions::type('array'),
            ],
            'return' => true,
        ]);

        $processor = new MyPlugin\DataProcessor();
        $result = $processor->save_settings(['api_key' => 'test123']);

        $this->assertTrue($result);
    }
}
```

### Mocking Filters and Actions

**Testing add_filter() Calls:**

```php
<?php
class Test_Hook_Registration extends WP_Mock\Tools\TestCase {

    public function setUp(): void {
        WP_Mock::setUp();
    }

    public function tearDown(): void {
        WP_Mock::tearDown();
    }

    /**
     * Test that filter is registered
     */
    public function test_content_filter_registered() {
        // Expect filter to be added
        WP_Mock::expectFilterAdded(
            'the_content',
            'MyPlugin\ContentFilter::add_reading_time',
            10,
            1
        );

        // Execute function that adds the filter
        MyPlugin\Hooks::register_filters();

        // Verify expectations met
        $this->assertConditionsMet();
    }

    /**
     * Test that action is registered
     */
    public function test_init_action_registered() {
        WP_Mock::expectActionAdded(
            'init',
            'MyPlugin\PostTypes::register_custom_post_types',
            10,
            0
        );

        MyPlugin\Hooks::register_actions();

        $this->assertConditionsMet();
    }

    /**
     * Test apply_filters modifies value
     */
    public function test_apply_custom_filter() {
        $original_value = 100;
        $filtered_value = 150;

        // Mock apply_filters
        WP_Mock::onFilter('my_plugin_price')
            ->with($original_value)
            ->reply($filtered_value);

        $processor = new MyPlugin\PriceCalculator();
        $result = $processor->get_final_price($original_value);

        $this->assertEquals($filtered_value, $result);
    }

    /**
     * Test do_action is called
     */
    public function test_custom_action_fired() {
        $order_id = 12345;

        // Expect action to be fired with specific arguments
        WP_Mock::expectAction('my_plugin_order_processed', $order_id);

        $processor = new MyPlugin\OrderProcessor();
        $processor->process_order($order_id);

        $this->assertConditionsMet();
    }
}
```

### Testing in Isolation (No WordPress Dependency)

**Example: Email Service Class:**

```php
<?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,
            'Content-Type: text/html; charset=UTF-8',
        ];
    }
}
```

**Unit Test Without WordPress:**

```php
<?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();
    }

    /**
     * Test email sending logic
     */
    public function test_send_notification_email() {
        // Mock get_bloginfo
        WP_Mock::userFunction('get_bloginfo', [
            'args' => 'name',
            'return' => 'My WordPress Site',
        ]);

        // Mock get_option
        WP_Mock::userFunction('get_option', [
            'args' => 'admin_email',
            'return' => '[email protected]',
        ]);

        // Mock wp_mail and verify arguments
        WP_Mock::userFunction('wp_mail', [
            'times' => 1,
            'args' => [
                '[email protected]',
                '[My WordPress Site] Notification',
                'Test message content',
                WP_Mock\Functions::type('array'),
            ],
            'return' => true,
        ]);

        $service = new MyPlugin\EmailService();
        $result = $service->send_notification(
            '[email protected]',
            'Test message content'
        );

        $this->assertTrue($result);
    }

    /**
     * Test email failure handling
     */
    public function test_email_send_failure() {
        WP_Mock::userFunction('get_bloginfo', [
            'return' => 'Test Site',
        ]);

        WP_Mock::userFunction('get_option', [
            'return' => '[email protected]',
        ]);

        // Simulate wp_mail failure
        WP_Mock::userFunction('wp_mail', [
            'return' => false,
        ]);

        $service = new MyPlugin\EmailService();
        $result = $service->send_notification('[email protected]', 'Message');

        $this->assertFalse($result);
    }
}
```

---

## PHPCS & Coding Standards

### Installing PHPCS and WPCS

**via Composer (Recommended):**

```bash
# Allow PHPCS composer installer plugin
composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true

# Install WordPress Coding Standards
composer require --dev wp-coding-standards/wpcs:"^3.0"

# Install PHP Compatibility checker
composer require --dev phpcompatibility/phpcompatibility-wp:"*"

# Install PHPCS itself (if not already installed)
composer require --dev squizlabs/php_codesniffer:"^3.7"

# Verify installation
vendor/bin/phpcs -i
# Should show: WordPress, WordPress-Core, WordPress-Docs, WordPress-Extra
```

### .phpcs.xml.dist Configuration

**Complete Configuration File:**

```xml
<?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>

    <!-- What to scan -->
    <file>./includes</file>
    <file>./my-plugin.php</file>

    <!-- Exclude patterns -->
    <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>

    <!-- Show progress -->
    <arg value="ps"/>
    <arg name="colors"/>
    <arg name="extensions" value="php"/>
    <arg name="parallel" value="8"/>

    <!-- Rules: Use WordPress-Extra ruleset -->
    <rule ref="WordPress-Extra">
        <!-- Allow short array syntax [] instead of array() -->
        <exclude name="Generic.Arrays.DisallowShortArraySyntax"/>

        <!-- Allow multiple assignments in single line -->
        <exclude name="Squiz.PHP.DisallowMultipleAssignments"/>

        <!-- Relax file comment requirements -->
        <exclude name="Squiz.Commenting.FileComment"/>
    </rule>

    <!-- WordPress.WP.I18n: Check text domain -->
    <rule ref="WordPress.WP.I18n">
        <properties>
            <property name="text_domain" type="array">
                <element value="my-plugin"/>
            </property>
        </properties>
    </rule>

    <!-- WordPress.NamingConventions.PrefixAllGlobals: Check function/class prefixes -->
    <rule ref="WordPress.NamingConventions.PrefixAllGlobals">
        <properties>
            <property name="prefixes" type="array">
                <element value="my_plugin"/>
                <element value="MyPlugin"/>
            </property>
        </properties>
    </rule>

    <!-- PHP version compatibility -->
    <config name="testVersion" value="8.1-"/>
    <rule ref="PHPCompatibilityWP"/>

    <!-- Minimum supported WordPress version -->
    <config name="minimum_wp_version" value="6.4"/>

    <!-- Exclude specific rules for test files -->
    <rule ref="WordPress.Files.FileName">
        <exclude-pattern>*/tests/*</exclude-pattern>
    </rule>

    <!-- Enforce line length limit (warning at 80, error at 120) -->
    <rule ref="Generic.Files.LineLength">
        <properties>
            <property name="lineLimit" value="120"/>
            <property name="absoluteLineLimit" value="150"/>
        </properties>
    </rule>

    <!-- Allow WordPress globals to be modified -->
    <rule ref="WordPress.WP.GlobalVariablesOverride">
        <type>error</type>
    </rule>
</ruleset>
```

### Running PHPCS and PHPCBF

**Command Line Usage:**

```bash
# Check all files
vendor/bin/phpcs

# Check specific file
vendor/bin/phpcs includes/Core.php

# Show error codes
vendor/bin/phpcs -s

# Show only errors (hide warnings)
vendor/bin/phpcs -n

# Generate report summary
vendor/bin/phpcs --report=summary

# Check single file with detailed output
vendor/bin/phpcs -v includes/Admin/Settings.php

# Auto-fix fixable issues
vendor/bin/phpcbf

# Auto-fix specific file
vendor/bin/phpcbf includes/Core.php

# Dry run (show what would be fixed)
vendor/bin/phpcbf --dry-run

# Use specific standard
vendor/bin/phpcs --standard=WordPress-Core includes/

# Generate different report formats
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:**

```json
{
    "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):**

```bash
#!/bin/bash

# Run PHPCS on changed PHP files
FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '.php$')

if [ -z "$FILES" ]; then
    echo "No PHP files to check"
    exit 0
fi...

© bobmatnyc · MIT · bobmatnyc/claude-mpm-skills

Similar skills

Everything for Testing and QA →

From the same set65

golang-cli-cobra-viper A skill for building Go command-line tools: command structure with Cobra and configuration from flags, environment variables and files with Viper. CX Codex +1 425installs dependency-audit A skill that walks through dependency cleanup: outdated packages, security vulnerabilities and libraries nothing imports anymore. CX Codex +1 422installs wordpress-plugin-fundamentals A skill for building WordPress plugins on PHP 8.3+ with OOP, hooks, database access and the Settings API. Useful when the agent has to scaffold a plugin instead of guessing at structure. CX Codex +1 421installs digitalocean-compute Tells your agent which DigitalOcean compute to use for a job: Droplets, App Platform, Functions, DOKS or GPUs. Handy when you are deciding where an app or container should run. CX Codex +1 416installs golang-observability-opentelemetry A skill for instrumenting Go services: OpenTelemetry for distributed tracing, Prometheus for metrics, slog for structured logging. CX Codex +1 409installs digitalocean-overview This skill maps DigitalOcean accounts, projects, services, and tooling so you can plan a setup through the Control Panel, doctl, REST API, or Terraform. CX Codex +1 408installs hono-validation A skill for validating Hono requests: JSON bodies, forms, query params, headers and cookies. Useful when you want schema types inside the handler. CX Codex +1 408installs golang-concurrency-patterns A skill that feeds your agent production Go concurrency patterns: context cancellation, errgroup, worker pools, and the usual race and deadlock traps. CX Codex +1 408installs

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor