К основному содержанию
скилл CX Codex

wordpress-testing-qa — скилл для Claude Code: тесты плагинов WordPress

Скилл для тестирования плагинов и тем WordPress: PHPUnit-интеграция, WP_Mock, PHPCS и CI/CD. Пригодится, когда нужно навести порядок в тестах.

Установить в Claude Code · Через Skills CLI (Vercel) Все способы и платформы ↓
npx skills add bobmatnyc/claude-mpm-skills --skill wordpress-testing-qa -a claude-code
Открыть на GitHub

Что делает wordpress-testing-qa

Скилл описывает, как устроено тестирование в WordPress: пирамиду из юнит-тестов на WP_Mock, интеграционных на PHPUnit с загруженным ядром и E2E на Playwright. Внутри — готовые шаги: установка зависимостей через composer, генерация каркаса командой wp scaffold plugin-tests my-plugin, установка тестовой библиотеки WordPress скриптом bin/install-wp-tests.sh, настройка phpunit.xml.dist и tests/bootstrap.php.

Отдельно разобрано, когда брать PHPUnit, а когда WP_Mock. Для операций с базой, хуков и шаблонов нужен PHPUnit с настоящим WordPress. Для чистой логики хватит WP_Mock, потому что база не поднимается и тесты идут быстрее. Есть ориентиры по покрытию: 80% для нового кода, 95% для критичных путей вроде платежей и аутентификации, 100% для публичных методов.

Скилл не запускается моделью сам: в нём стоит disable-model-invocation: true и user-invocable: false. Это значит, что вызывать его нужно явно, а не ждать, пока агент сам решит им воспользоваться. Для работы понадобятся PHP, Composer и WP-CLI — без них шаги из инструкции не пройдут.

Какие задачи решает

  • поднять тестовую библиотеку WordPress и отдельную базу для интеграционных тестов
  • разделить тесты на юнит и интеграционные, чтобы не гонять базу на каждой проверке
  • настроить phpunit.xml.dist с покрытием и полифиллами Yoast
  • подключить WPCS через PHPCS и держать код в рамках стандартов
  • создать тестовые данные через фабрики вместо ручных фикстур

Примеры запросов агенту

  • помоги настроить PHPUnit для моего плагина WordPress
  • напиши интеграционный тест на создание записи с метаданными
  • какие тесты писать через WP_Mock, а какие через PHPUnit?

Как установить wordpress-testing-qa

1Через Skills CLI (Vercel)

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

2Вручную: положить файлы

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

Папка скилла целиком; для одного проекта — .claude/skills/wordpress-testing-qa/

По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов

Вопросы о wordpress-testing-qa

Как установить wordpress-testing-qa в Claude?

Выполните npx skills add bobmatnyc/claude-mpm-skills --skill wordpress-testing-qa -a claude-code. После этого файл скилла окажется в ~/.claude/skills/wordpress-testing-qa/SKILL.md.

Что делает wordpress-testing-qa?

Описывает тестирование плагинов и тем WordPress: интеграционные тесты на PHPUnit, юнит-тесты на WP_Mock, проверку стандартов кодирования через PHPCS и рабочие процессы CI/CD.

Нужно ли что-то ставить заранее?

Да. В инструкции используются Composer, WP-CLI и тестовая библиотека WordPress, которую ставит bin/install-wp-tests.sh. Без них шаги не выполнятся.

Исходный текст

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

Похожие скиллы

Всё по теме «Тесты и QA» →
wordpress-pro https://github.com/Jeffallan Скилл помогает разрабатывать темы и плагины WordPress, блоки Gutenberg и магазины WooCommerce. Нужен для задач с REST API, хуками, безопасностью и производительностью. CX Codex +1 9 700установок wordpress-master VoltAgent Community Субагент настраивает, оптимизирует и защищает проекты на WordPress: от кастомных тем и плагинов до multisite-сетей и headless-архитектур. 763установок wordpress-penetration-testing zebbern Скилл проверяет WordPress-сайты на типичные уязвимости и проблемы прав доступа REST API плагинов. Используется для авторизованных аудитов безопасности. CX Codex +1 1 100установок wordpress-router WordPress Скилл определяет тип кодовой базы WordPress и направляет агента по нужным веткам разработки. Пригодится на старте задач с темами, плагинами или ядром. CX Codex +1 5 600установок wordpress Mindrally Скилл wordpress задаёт агенту правила разработки под WordPress и WooCommerce на PHP. Он держит код в рамках стандартов WordPress, напоминает про хуки, nonce и подготовленные запросы. CX Codex +1 894установок wordpress-performance-best-practices bartekmis Скилл для Claude и других ИИ-редакторов: 34 правила производительности WordPress, разбитые по приоритету. Пригодится при написании и ревью PHP-кода плагинов и тем. CX Codex +1 708установок

Из того же набора65

golang-cli-cobra-viper Скилл для сборки консольных утилит на Go: структура команд через Cobra и конфигурация из флагов, переменных окружения и файлов через Viper. CX Codex +1 425установок dependency-audit Скилл для ревизии зависимостей: находит устаревшие пакеты, уязвимости и мусор, который давно никто не импортирует. CX Codex +1 422установок wordpress-plugin-fundamentals Скилл для разработки плагинов WordPress на PHP 8.3+ с ООП, хуками, работой с базой и Settings API. Пригодится, когда агенту нужно собрать структуру плагина, а не писать её с нуля. CX Codex +1 421установок digitalocean-compute Подсказывает агенту, что выбрать в DigitalOcean под задачу: Droplets, App Platform, Functions, DOKS или GPU. Пригодится, когда решаешь, куда деплоить приложение или контейнер. CX Codex +1 416установок golang-observability-opentelemetry Скилл для инструментирования Go-сервисов: OpenTelemetry для распределённых трейсов, Prometheus для метрик, slog для структурных логов. CX Codex +1 409установок digitalocean-overview Скилл помогает освоить DigitalOcean, выбрать сервисы и настроить работу через Control Panel, doctl, REST API или Terraform. CX Codex +1 408установок hono-validation Скилл для валидации запросов в Hono: JSON, формы, query-параметры, заголовки и cookie. Пригодится, когда нужны типы из схемы прямо в обработчике. CX Codex +1 408установок golang-concurrency-patterns Скилл для написания конкурентного Go-кода: отмена через context, errgroup, worker pools и разбор типичных гонок и дедлоков. CX Codex +1 408установок

Спонсоры проекта

Компании, которые помогают держать аналитику открытой