wordpress-testing-qa

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

WordPress Testing & Quality Assurance

WordPress测试与质量保障


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"


progressive_disclosure: entry_point: summary: "使用PHPUnit、WP_Mock、PHPCS及CI/CD进行WordPress插件与主题测试,保障质量" when_to_use: - "使用PHPUnit集成测试WordPress插件" - "无需加载WordPress核心的单元测试(WP_Mock)" - "使用PHPCS强制执行编码标准" quick_start: - "搭配WordPress测试套件搭建PHPUnit环境" - "使用WP_Mock编写单元测试" - "配置PHPCS并应用WPCS规则集"

Testing Strategy

测试策略

Testing Pyramid for WordPress

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
WordPress测试层级:
       /\
      /  \     E2E Tests (Playwright)
     /    \    - 完整用户工作流
    /------\   - 浏览器自动化
   /        \
  /  集成测试  \  Integration Tests (PHPUnit + WordPress)
 /    TESTS    \ - 数据库操作
/              \ - 钩子交互
--------------
 单元测试      Unit Tests (WP_Mock)
               - 纯逻辑
               - 无WordPress依赖
测试分布指南:
  • 单元测试(60%): 快速、隔离、无需WordPress
    • 纯PHP函数
    • 输入输出明确的类方法
    • 无副作用的业务逻辑
  • 集成测试(30%): 加载WordPress的测试
    • 数据库操作
    • 钩子/过滤器交互
    • 自定义文章类型注册
    • 设置API功能
  • 端到端测试(10%): 浏览器自动化
    • 关键用户工作流
    • 后台面板交互
    • 前端表单提交

When to Use PHPUnit vs WP_Mock

何时使用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
使用PHPUnit(集成测试)的场景:
  • ✅ 测试数据库操作(
    $wpdb
    、文章创建、元数据)
  • ✅ 测试WordPress钩子(动作/过滤器实际触发)
  • ✅ 测试模板渲染与输出
  • ✅ 测试插件激活/停用逻辑
  • ✅ 测试实际WordPress函数
使用WP_Mock(单元测试)的场景:
  • ✅ 测试纯业务逻辑
  • ✅ 测试调用WordPress函数但逻辑独立的代码
  • ✅ 需要快速执行测试(无需数据库搭建)
  • ✅ 隔离测试无副作用
  • ✅ 模拟外部API调用

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)

最低覆盖率要求:
  • 新代码: 最低80%覆盖率
  • 关键路径: 95%覆盖率(支付处理、身份验证、数据验证)
  • 遗留代码: 逐步优化,优先处理高风险区域
  • 公开API: 所有公开方法100%覆盖
测试优先级:
  1. 安全函数: Nonce验证、数据清理、权限检查
  2. 数据操作: 数据库增删改查、数据验证、转换
  3. 业务逻辑: 计算、工作流、状态转换
  4. 钩子回调: 动作/过滤器处理器
  5. 公开API: REST端点、WP-CLI命令
无需测试的内容:
  • ❌ WordPress核心函数(假设其功能正常)
  • ❌ 第三方库内部实现
  • ❌ 无逻辑的简单获取/设置方法
  • ❌ 配置文件(theme.json、block.json)

PHPUnit Integration Testing

PHPUnit集成测试

WordPress Test Suite Setup

WordPress测试套件搭建

Step 1: Install Dependencies
bash
undefined
步骤1:安装依赖
bash
undefined

Install PHPUnit and WordPress polyfills

Install PHPUnit and WordPress polyfills

composer require --dev phpunit/phpunit "^9.6" composer require --dev yoast/phpunit-polyfills "^2.0"
composer require --dev phpunit/phpunit "^9.6" composer require --dev yoast/phpunit-polyfills "^2.0"

Generate test scaffold with WP-CLI

Generate test scaffold with WP-CLI

wp scaffold plugin-tests my-plugin
wp scaffold plugin-tests my-plugin

This creates:

This creates:

- tests/bootstrap.php

- tests/bootstrap.php

- tests/test-sample.php

- tests/test-sample.php

- phpunit.xml.dist

- phpunit.xml.dist

- bin/install-wp-tests.sh

- bin/install-wp-tests.sh


**Step 2: Install WordPress Test Library**

```bash

**步骤2:安装WordPress测试库**

```bash

Install WordPress test suite and test database

Install WordPress test suite and test database

Syntax: bash bin/install-wp-tests.sh <db-name> <db-user> <db-pass> <db-host> <wp-version>

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
bash bin/install-wp-tests.sh wordpress_test root '' localhost latest

For specific WordPress version:

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>
bash bin/install-wp-tests.sh wordpress_test root '' localhost 6.7

**步骤3:配置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

WP_UnitTestCase基类

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";
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' => 'editor@example.com',
        ]);

        // 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
使用内置工厂:
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' => 'editor@example.com',
        ]);

        // 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);
    }
}
可用工厂对象:
  • $this->factory->post
    - 文章、页面、自定义文章类型
  • $this->factory->user
    - 带角色的用户
  • $this->factory->term
    - 分类项(分类、标签、自定义分类法)
  • $this->factory->category
    - 专门用于分类
  • $this->factory->tag
    - 专门用于标签
  • $this->factory->comment
    - 评论
  • $this->factory->blog
    - 多站点博客

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);
    }
}
setUp()和tearDown()方法:
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()和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);
    }
}

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

WP_Mock单元测试

What is WP_Mock and When to Use It

WP_Mock是什么及何时使用

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
WP_Mock用途:
  • 无需加载WordPress即可测试PHP代码
  • 模拟WordPress函数返回预期值
  • 验证WordPress函数是否以正确参数被调用
  • 比集成测试快得多(无需数据库搭建)
何时使用WP_Mock:
适用场景:
  • 调用WordPress函数的纯业务逻辑
  • 数据转换/验证函数
  • 依赖WordPress的服务类
  • 持续集成中的测试(更快的CI构建)
不适用场景:
  • 测试实际数据库操作
  • 测试插件间的钩子交互
  • 测试模板渲染
  • 测试依赖WordPress状态的函数

Installation and Setup

安装与配置

bash
undefined
bash
undefined

Install WP_Mock and Mockery

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

模拟WordPress函数

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);
    }
}
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();
    }
}
测试add_filter()调用:
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)

隔离测试(无WordPress依赖)

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' => 'admin@example.com',
        ]);

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

        $service = new MyPlugin\EmailService();
        $result = $service->send_notification(
            'user@example.com',
            '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' => 'admin@test.com',
        ]);

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

        $service = new MyPlugin\EmailService();
        $result = $service->send_notification('user@test.com', 'Message');

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

示例:邮件服务类:
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',
        ];
    }
}
无需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' => 'admin@example.com',
        ]);

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

        $service = new MyPlugin\EmailService();
        $result = $service->send_notification(
            'user@example.com',
            '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' => 'admin@test.com',
        ]);

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

        $service = new MyPlugin\EmailService();
        $result = $service->send_notification('user@test.com', 'Message');

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

PHPCS & Coding Standards

PHPCS与编码标准

Installing PHPCS and WPCS

安装PHPCS与WPCS

via Composer (Recommended):
bash
undefined
通过Composer(推荐):
bash
undefined

Allow PHPCS composer installer plugin

Allow PHPCS composer installer plugin

composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true
composer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true

Install WordPress Coding Standards

Install WordPress Coding Standards

composer require --dev wp-coding-standards/wpcs:"^3.0"
composer require --dev wp-coding-standards/wpcs:"^3.0"

Install PHP Compatibility checker

Install PHP Compatibility checker

composer require --dev phpcompatibility/phpcompatibility-wp:"*"
composer require --dev phpcompatibility/phpcompatibility-wp:"*"

Install PHPCS itself (if not already installed)

Install PHPCS itself (if not already installed)

composer require --dev squizlabs/php_codesniffer:"^3.7"
composer require --dev squizlabs/php_codesniffer:"^3.7"

Verify installation

Verify installation

vendor/bin/phpcs -i
vendor/bin/phpcs -i

Should show: WordPress, WordPress-Core, WordPress-Docs, WordPress-Extra

Should show: WordPress, WordPress-Core, WordPress-Docs, WordPress-Extra

undefined
undefined

.phpcs.xml.dist Configuration

.phpcs.xml.dist配置

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>
完整配置文件:
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

运行PHPCS与PHPCBF

Command Line Usage:
bash
undefined
命令行用法:
bash
undefined

Check all files

Check all files

vendor/bin/phpcs
vendor/bin/phpcs

Check specific file

Check specific file

vendor/bin/phpcs includes/Core.php
vendor/bin/phpcs includes/Core.php

Show error codes

Show error codes

vendor/bin/phpcs -s
vendor/bin/phpcs -s

Show only errors (hide warnings)

Show only errors (hide warnings)

vendor/bin/phpcs -n
vendor/bin/phpcs -n

Generate report summary

Generate report summary

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

Check single file with detailed output

Check single file with detailed output

vendor/bin/phpcs -v includes/Admin/Settings.php
vendor/bin/phpcs -v includes/Admin/Settings.php

Auto-fix fixable issues

Auto-fix fixable issues

vendor/bin/phpcbf
vendor/bin/phpcbf

Auto-fix specific file

Auto-fix specific file

vendor/bin/phpcbf includes/Core.php
vendor/bin/phpcbf includes/Core.php

Dry run (show what would be fixed)

Dry run (show what would be fixed)

vendor/bin/phpcbf --dry-run
vendor/bin/phpcbf --dry-run

Use specific standard

Use specific standard

vendor/bin/phpcs --standard=WordPress-Core includes/
vendor/bin/phpcs --standard=WordPress-Core includes/

Generate different report formats

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"
        ]
    }
}
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脚本:**

```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
安装提交前钩子(.git/hooks/pre-commit):
bash
#!/bin/bash

Run PHPCS on changed PHP files

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
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:**

```bash
chmod +x .git/hooks/pre-commit
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

**设置钩子可执行:**

```bash
chmod +x .git/hooks/pre-commit

IDE Integration

IDE集成

Visual Studio Code (.vscode/settings.json):
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:
  1. Go to Settings → PHP → Quality Tools → PHP_CodeSniffer
  2. Set Configuration path:
    {PROJECT_ROOT}/vendor/bin/phpcs
  3. Go to Settings → Editor → Inspections → PHP → Quality Tools
  4. Enable "PHP_CodeSniffer validation"
  5. Set Coding standard: "Custom"
  6. Set Path:
    {PROJECT_ROOT}/.phpcs.xml.dist

Visual Studio Code(.vscode/settings.json):
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配置:
  1. 前往 设置 → PHP → 质量工具 → PHP_CodeSniffer
  2. 设置配置路径:
    {PROJECT_ROOT}/vendor/bin/phpcs
  3. 前往 设置 → 编辑器 → 检查 → PHP → 质量工具
  4. 启用“PHP_CodeSniffer验证”
  5. 设置编码标准:“自定义”
  6. 设置路径:
    {PROJECT_ROOT}/.phpcs.xml.dist

GitHub Actions CI/CD

GitHub Actions CI/CD

Workflow File Structure

工作流文件结构

.github/workflows/tests.yml:
yaml
name: Test Suite

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  # Job 1: Coding Standards Check
  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

  # Job 2: PHPUnit Tests with Matrix
  phpunit:
    name: PHPUnit (PHP ${{ matrix.php }}, WP ${{ matrix.wordpress }})
    runs-on: ubuntu-latest

    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'

    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 --health-timeout=5s --health-retries=3

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          extensions: mysqli, zip
          tools: composer
          coverage: xdebug

      - name: Install Composer dependencies
        run: composer install --prefer-dist --no-progress

      - name: Install WordPress test suite
        run: |
          bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1:3306 ${{ matrix.wordpress }}

      - name: Run PHPUnit tests
        run: vendor/bin/phpunit --coverage-clover=coverage.xml

      - name: Upload coverage to Codecov
        if: matrix.php == '8.3' && matrix.wordpress == 'latest'
        uses: codecov/codecov-action@v4
        with:
          files: ./coverage.xml
          flags: unittests
          name: codecov-umbrella

  # Job 3: WP_Mock Unit Tests
  wp-mock:
    name: WP_Mock Unit Tests
    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

      - name: Run WP_Mock tests
        run: vendor/bin/phpunit -c phpunit-wp-mock.xml.dist
.github/workflows/tests.yml:
yaml
name: Test Suite

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  # Job 1: Coding Standards Check
  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

  # Job 2: PHPUnit Tests with Matrix
  phpunit:
    name: PHPUnit (PHP ${{ matrix.php }}, WP ${{ matrix.wordpress }})
    runs-on: ubuntu-latest

    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'

    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 --health-timeout=5s --health-retries=3

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          extensions: mysqli, zip
          tools: composer
          coverage: xdebug

      - name: Install Composer dependencies
        run: composer install --prefer-dist --no-progress

      - name: Install WordPress test suite
        run: |
          bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1:3306 ${{ matrix.wordpress }}

      - name: Run PHPUnit tests
        run: vendor/bin/phpunit --coverage-clover=coverage.xml

      - name: Upload coverage to Codecov
        if: matrix.php == '8.3' && matrix.wordpress == 'latest'
        uses: codecov/codecov-action@v4
        with:
          files: ./coverage.xml
          flags: unittests
          name: codecov-umbrella

  # Job 3: WP_Mock Unit Tests
  wp-mock:
    name: WP_Mock Unit Tests
    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

      - name: Run WP_Mock tests
        run: vendor/bin/phpunit -c phpunit-wp-mock.xml.dist

Matrix Testing (Multiple PHP/WP Versions)

矩阵测试(多PHP/WP版本)

Strategy Explanation:
yaml
strategy:
  fail-fast: false  # Continue testing other versions even if one fails
  matrix:
    php: ['8.1', '8.2', '8.3']  # Test PHP versions
    wordpress: ['6.4', '6.5', '6.6', '6.7', 'latest']  # Test WP versions
    include:
      # Add specific combination not in default matrix
      - php: '8.3'
        wordpress: 'trunk'  # WordPress development version
    exclude:
      # Exclude incompatible combinations
      - 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
策略说明:
yaml
strategy:
  fail-fast: false  # Continue testing other versions even if one fails
  matrix:
    php: ['8.1', '8.2', '8.3']  # Test PHP versions
    wordpress: ['6.4', '6.5', '6.6', '6.7', 'latest']  # Test WP versions
    include:
      # Add specific combination not in default matrix
      - php: '8.3'
        wordpress: 'trunk'  # WordPress development version
    exclude:
      # Exclude incompatible combinations
      - php: '8.1'
        wordpress: 'trunk'
矩阵结果:
  • 创建 18个测试任务(3个PHP版本 × 6个WordPress版本)
  • 确保在支持的版本间兼容
  • 尽早发现版本特定问题

PHPCS Checks in CI

CI中的PHPCS检查

Dedicated PHPCS Job:
yaml
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

    - name: Upload PHPCS report
      if: failure()
      uses: actions/upload-artifact@v3
      with:
        name: phpcs-report
        path: phpcs-report.txt
专用PHPCS任务:
yaml
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

    - name: Upload PHPCS report
      if: failure()
      uses: actions/upload-artifact@v3
      with:
        name: phpcs-report
        path: phpcs-report.txt

PHPUnit Test Execution

PHPUnit测试执行

With Code Coverage:
yaml
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 install --prefer-dist --no-progress

    - name: Install WordPress test suite
      run: bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1:3306 latest

    - name: Run tests with coverage
      run: vendor/bin/phpunit --coverage-html coverage-html --coverage-clover coverage.xml

    - name: Upload coverage HTML report
      uses: actions/upload-artifact@v3
      with:
        name: coverage-report
        path: coverage-html

    - name: Check coverage threshold
      run: |
        COVERAGE=$(vendor/bin/phpunit --coverage-text | grep "Lines:" | awk '{print $2}' | sed 's/%//')
        if (( $(echo "$COVERAGE < 80" | bc -l) )); then
          echo "Coverage $COVERAGE% is below 80% threshold"
          exit 1
        fi
带代码覆盖率:
yaml
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 install --prefer-dist --no-progress

    - name: Install WordPress test suite
      run: bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1:3306 latest

    - name: Run tests with coverage
      run: vendor/bin/phpunit --coverage-html coverage-html --coverage-clover coverage.xml

    - name: Upload coverage HTML report
      uses: actions/upload-artifact@v3
      with:
        name: coverage-report
        path: coverage-html

    - name: Check coverage threshold
      run: |
        COVERAGE=$(vendor/bin/phpunit --coverage-text | grep "Lines:" | awk '{print $2}' | sed 's/%//')
        if (( $(echo "$COVERAGE < 80" | bc -l) )); then
          echo "Coverage $COVERAGE% is below 80% threshold"
          exit 1
        fi

Coverage Reporting

覆盖率报告

Codecov Integration:
yaml
- 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:
yaml
- name: Upload to Coveralls
  uses: coverallsapp/github-action@v2
  with:
    github-token: ${{ secrets.GITHUB_TOKEN }}
    path-to-lcov: ./coverage.xml
Codecov集成:
yaml
- 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集成:
yaml
- 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):
yaml
name: CI Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
  schedule:
    - cron: '0 0 * * 0'  # Weekly on Sunday

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
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          tools: composer
      - run: composer install --prefer-dist --no-progress
      - run: vendor/bin/phpunit -c phpunit-wp-mock.xml.dist --testdox

  integration-tests:
    name: Integration Tests
    runs-on: ubuntu-latest
    strategy:
      matrix:
        php: ['8.1', '8.3']
        wordpress: ['6.5', '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
      - uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          extensions: mysqli
          tools: composer
          coverage: xdebug
      - run: composer install --prefer-dist --no-progress
      - run: bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1:3306 ${{ matrix.wordpress }}
      - run: vendor/bin/phpunit --coverage-clover=coverage.xml
      - uses: codecov/codecov-action@v4
        if: matrix.php == '8.3' && matrix.wordpress == 'latest'
        with:
          files: ./coverage.xml

  deploy-ready:
    name: Deployment Check
    needs: [coding-standards, unit-tests, integration-tests]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - run: echo "All checks passed - ready for deployment"

.github/workflows/ci.yml(生产可用):
yaml
name: CI Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
  schedule:
    - cron: '0 0 * * 0'  # Weekly on Sunday

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
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          tools: composer
      - run: composer install --prefer-dist --no-progress
      - run: vendor/bin/phpunit -c phpunit-wp-mock.xml.dist --testdox

  integration-tests:
    name: Integration Tests
    runs-on: ubuntu-latest
    strategy:
      matrix:
        php: ['8.1', '8.3']
        wordpress: ['6.5', '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
      - uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          extensions: mysqli
          tools: composer
          coverage: xdebug
      - run: composer install --prefer-dist --no-progress
      - run: bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1:3306 ${{ matrix.wordpress }}
      - run: vendor/bin/phpunit --coverage-clover=coverage.xml
      - uses: codecov/codecov-action@v4
        if: matrix.php == '8.3' && matrix.wordpress == 'latest'
        with:
          files: ./coverage.xml

  deploy-ready:
    name: Deployment Check
    needs: [coding-standards, unit-tests, integration-tests]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - run: echo "All checks passed - ready for deployment"

Testing Best Practices

测试最佳实践

Test Naming Conventions

测试命名规范

Method Naming Pattern:
test_[method_name]_[scenario]_[expected_result]
Examples:
php
// ✅ GOOD: Descriptive test names
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() {}

// ❌ BAD: Vague test names
public function test_email() {}
public function test_function() {}
public function test_it_works() {}
Class Naming:
php
// Pattern: Test_[ClassName]
class Test_Email_Service extends WP_UnitTestCase {}
class Test_Data_Validator extends WP_Mock\Tools\TestCase {}
class Test_Post_Meta_Handler extends WP_UnitTestCase {}
方法命名模式:
test_[方法名]_[场景]_[预期结果]
示例:
php
// ✅ 好:描述性测试名称
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() {}
类命名:
php
// 模式:Test_[类名]
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:
php
public function test_calculate_discount() {
    // ARRANGE: Set up test data and conditions
    $original_price = 100;
    $discount_percent = 20;
    $calculator = new MyPlugin\PriceCalculator();

    // ACT: Execute the code being tested
    $discounted_price = $calculator->apply_discount($original_price, $discount_percent);

    // ASSERT: Verify expected outcome
    $this->assertEquals(80, $discounted_price);
}
Complete Example:
php
public function test_save_user_preferences_updates_database() {
    // ARRANGE
    $user_id = $this->factory->user->create();
    $preferences = [
        'theme' => 'dark',
        'notifications' => true,
    ];
    $service = new MyPlugin\UserPreferences();

    // ACT
    $result = $service->save_preferences($user_id, $preferences);

    // ASSERT
    $this->assertTrue($result);
    $saved_prefs = get_user_meta($user_id, 'preferences', true);
    $this->assertEquals('dark', $saved_prefs['theme']);
    $this->assertTrue($saved_prefs['notifications']);
}
每个测试的结构:
php
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);
}
完整示例:
php
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
php
/**
 * @dataProvider email_validation_provider
 */
public function test_email_validation($email, $expected) {
    $validator = new MyPlugin\Validator();
    $result = $validator->is_valid_email($email);
    $this->assertEquals($expected, $result);
}

/**
 * Data provider for email validation tests
 */
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:
php
/**
 * @dataProvider discount_calculation_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],
    ];
}
用途: 使用多组输入测试同一逻辑
php
/**
 * @dataProvider email_validation_provider
 */
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 [
        '有效邮箱' => ['user@example.com', true],
        '无@符号的无效邮箱' => ['userexample.com', false],
        '无域名的无效邮箱' => ['user@', false],
        '含空格的无效邮箱' => ['user @example.com', false],
        '含子域名的有效邮箱' => ['user@mail.example.com', true],
        '含特殊字符的无效邮箱' => ['user#@example.com', false],
    ];
}
复杂数据提供者:
php
/**
 * @dataProvider discount_calculation_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 [
        '100元打8折' => [100, 20, 80],
        '100元打5折' => [100, 50, 50],
        '100元不打折' => [100, 0, 100],
        '100元免费' => [100, 100, 0],
        '0元打8折' => [0, 20, 0],
    ];
}

Testing Hooks and Filters

测试钩子与过滤器

Testing add_action/add_filter:
php
public function test_init_hooks_registered() {
    // Remove all hooks first
    remove_all_actions('init');

    // Register plugin hooks
    MyPlugin\Hooks::register();

    // Verify action was added
    $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:
php
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');

    // Manually trigger the hook callback
    do_action('save_post', $post_id);

    // Verify meta was saved
    $isbn = get_post_meta($post_id, '_isbn', true);
    $this->assertEquals('978-3-16-148410-0', $isbn);
}
测试add_action/add_filter:
php
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'));
}
测试钩子回调:
php
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处理器

AJAX Test Setup:
php
public function test_ajax_load_more_posts() {
    // Create test posts
    $post_ids = $this->factory->post->create_many(5);

    // Set up AJAX request
    $_POST['action'] = 'load_more_posts';
    $_POST['page'] = 1;
    $_POST['nonce'] = wp_create_nonce('load_more_nonce');

    // Set current user (if authentication required)
    wp_set_current_user($this->factory->user->create(['role' => 'subscriber']));

    // Capture output
    try {
        $this->_handleAjax('load_more_posts');
    } catch (WPAjaxDieContinueException $e) {
        // Expected exception
    }

    // Get response
    $response = json_decode($this->_last_response, true);

    $this->assertTrue($response['success']);
    $this->assertCount(5, $response['data']['posts']);
}

AJAX测试配置:
php
public function test_ajax_load_more_posts() {
    // 创建测试文章
    $post_ids = $this->factory->post->create_many(5);

    // 设置AJAX请求
    $_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['data']['posts']);
}

Common Testing Patterns

常见测试模式

Testing Custom Post Types

测试自定义文章类型

php
class Test_Book_Post_Type extends WP_UnitTestCase {

    public function setUp(): void {
        parent::setUp();
        // Ensure CPT is registered
        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(post_type_supports('book', 'comments'));
    }

    public function test_book_has_rest_support() {
        $post_type = get_post_type_object('book');
        $this->assertTrue($post_type->show_in_rest);
    }

    public function test_create_book_post() {
        $book_id = $this->factory->post->create([
            'post_type' => 'book',
            'post_title' => 'The Great Gatsby',
        ]);

        $book = get_post($book_id);
        $this->assertEquals('book', $book->post_type);
        $this->assertEquals('The Great Gatsby', $book->post_title);
    }
}
php
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(post_type_supports('book', 'comments'));
    }

    public function test_book_has_rest_support() {
        $post_type = get_post_type_object('book');
        $this->assertTrue($post_type->show_in_rest);
    }

    public function test_create_book_post() {
        $book_id = $this->factory->post->create([
            'post_type' => 'book',
            'post_title' => 'The Great Gatsby',
        ]);

        $book = get_post($book_id);
        $this->assertEquals('book', $book->post_type);
        $this->assertEquals('The Great Gatsby', $book->post_title);
    }
}

Testing Settings/Options

测试设置/选项

php
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->assertTrue($result);

        $saved = get_option('my_plugin_settings');
        $this->assertEquals('test_key_123', $saved['api_key']);
        $this->assertTrue($saved['enabled']);
    }

    public function test_sanitize_settings() {
        $dirty_input = [
            'api_key' => '<script>alert("xss")</script>',
            'enabled' => 'yes',
        ];

        $clean = MyPlugin\Settings::sanitize($dirty_input);

        $this->assertEquals('alert("xss")', $clean['api_key']);
        $this->assertTrue($clean['enabled']);
    }
}
php
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->assertTrue($result);

        $saved = get_option('my_plugin_settings');
        $this->assertEquals('test_key_123', $saved['api_key']);
        $this->assertTrue($saved['enabled']);
    }

    public function test_sanitize_settings() {
        $dirty_input = [
            'api_key' => '<script>alert("xss")</script>',
            'enabled' => 'yes',
        ];

        $clean = MyPlugin\Settings::sanitize($dirty_input);

        $this->assertEquals('alert("xss")', $clean['api_key']);
        $this->assertTrue($clean['enabled']);
    }
}

Testing Database Operations

测试数据库操作

php
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 " . self::$table_name);
        parent::tearDownAfterClass();
    }

    public function test_insert_log_entry() {
        global $wpdb;

        $user_id = 1;
        $action = 'user_login';

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

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

        // Verify data
        $log = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT * FROM " . self::$table_name . " WHERE id = %d",
                $wpdb->insert_id
            )
        );

        $this->assertEquals($user_id, $log->user_id);
        $this->assertEquals($action, $log->action);
    }

    public function test_query_logs_by_user() {
        global $wpdb;

        $user_id = 42;

        // Insert test data
        $wpdb->insert(self::$table_name, ['user_id' => $user_id, 'action' => 'login'], ['%d', '%s']);
        $wpdb->insert(self::$table_name, ['user_id' => $user_id, 'action' => 'logout'], ['%d', '%s']);

        // Query logs
        $logs = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT * FROM " . self::$table_name . " WHERE user_id = %d",
                $user_id
            )
        );

        $this->assertCount(2, $logs);
    }
}
php
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 " . self::$table_name);
        parent::tearDownAfterClass();
    }

    public function test_insert_log_entry() {
        global $wpdb;

        $user_id = 1;
        $action = 'user_login';

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

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

        // 验证数据
        $log = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT * FROM " . self::$table_name . " WHERE id = %d",
                $wpdb->insert_id
            )
        );

        $this->assertEquals($user_id, $log->user_id);
        $this->assertEquals($action, $log->action);
    }

    public function test_query_logs_by_user() {
        global $wpdb;

        $user_id = 42;

        // 插入测试数据
        $wpdb->insert(self::$table_name, ['user_id' => $user_id, 'action' => 'login'], ['%d', '%s']);
        $wpdb->insert(self::$table_name, ['user_id' => $user_id, 'action' => 'logout'], ['%d', '%s']);

        // 查询日志
        $logs = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT * FROM " . self::$table_name . " WHERE user_id = %d",
                $user_id
            )
        );

        $this->assertCount(2, $logs);
    }
}

Testing REST API Endpoints

测试REST API端点

php
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() {
        // Create test posts
        $post_ids = $this->factory->post->create_many(3, ['post_type' => 'book']);

        $request = new WP_REST_Request('GET', '/myplugin/v1/items');
        $response = $this->server->dispatch($request);

        $this->assertEquals(200, $response->get_status());

        $data = $response->get_data();
        $this->assertCount(3, $data);
    }

    public function test_create_item_requires_authentication() {
        $request = new WP_REST_Request('POST', '/myplugin/v1/items');
        $request->set_body_params([
            'title' => 'New Item',
        ]);

        $response = $this->server->dispatch($request);

        $this->assertEquals(401, $response->get_status());
    }

    public function test_create_item_with_authentication() {
        $user_id = $this->factory->user->create(['role' => 'editor']);
        wp_set_current_user($user_id);

        $request = new WP_REST_Request('POST', '/myplugin/v1/items');
        $request->set_body_params([
            'title' => 'New Item',
            'content' => 'Item content',
        ]);

        $response = $this->server->dispatch($request);

        $this->assertEquals(201, $response->get_status());

        $data = $response->get_data();
        $this->assertEquals('New Item', $data['title']);
    }
}

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:
php
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', '/myplugin/v1/items');
        $response = $this->server->dispatch($request);

        $this->assertEquals(200, $response->get_status());

        $data = $response->get_data();
        $this->assertCount(3, $data);
    }

    public function test_create_item_requires_authentication() {
        $request = new WP_REST_Request('POST', '/myplugin/v1/items');
        $request->set_body_params([
            'title' => 'New Item',
        ]);

        $response = $this->server->dispatch($request);

        $this->assertEquals(401, $response->get_status());
    }

    public function test_create_item_with_authentication() {
        $user_id = $this->factory->user->create(['role' => 'editor']);
        wp_set_current_user($user_id);

        $request = new WP_REST_Request('POST', '/myplugin/v1/items');
        $request->set_body_params([
            'title' => 'New Item',
            'content' => 'Item content',
        ]);

        $response = $this->server->dispatch($request);

        $this->assertEquals(201, $response->get_status());

        $data = $response->get_data();
        $this->assertEquals('New Item', $data['title']);
    }
}

相关技能: 测试WordPress应用时,可参考这些互补技能(技能库中已提供):
  • WordPress插件基础:核心插件架构与钩子 - 理解测试内容的必备基础
  • WordPress安全与验证:安全模式与数据验证 - 安全测试策略的关键
  • Python pytest测试:现代测试模式 - 概念可应用于WordPress测试方法
  • GitHub Actions CI/CD:CI/CD自动化 - 将WordPress测试集成到自动化流水线
扩展阅读: