wordpress-testing-qa
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWordPress 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 dependencyTest 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 (, post creation, meta data)
$wpdb - ✅ 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):
- Security Functions: Nonce verification, sanitization, capability checks
- Data Operations: Database CRUD, data validation, transformation
- Business Logic: Calculations, workflows, state transitions
- Hook Callbacks: Action/filter handlers
- Public APIs: REST endpoints, WP-CLI commands
What NOT to Test:
- ❌ WordPress core functions (assume they work)
- ❌ Third-party library internals
- ❌ Simple getters/setters with no logic
- ❌ Configuration files (theme.json, block.json)
最低覆盖率要求:
- 新代码: 最低80%覆盖率
- 关键路径: 95%覆盖率(支付处理、身份验证、数据验证)
- 遗留代码: 逐步优化,优先处理高风险区域
- 公开API: 所有公开方法100%覆盖
测试优先级:
- 安全函数: Nonce验证、数据清理、权限检查
- 数据操作: 数据库增删改查、数据验证、转换
- 业务逻辑: 计算、工作流、状态转换
- 钩子回调: 动作/过滤器处理器
- 公开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
undefinedInstall 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测试库**
```bashInstall 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:
- - Posts, pages, custom post types
$this->factory->post - - Users with roles
$this->factory->user - - Terms (categories, tags, custom taxonomies)
$this->factory->term - - Categories specifically
$this->factory->category - - Tags specifically
$this->factory->tag - - Comments
$this->factory->comment - - Multisite blogs
$this->factory->blog
使用内置工厂:
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
undefinedbash
undefinedInstall 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
undefinedAllow 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
undefinedundefined.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
undefinedCheck 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/bashRun 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-commitFILES=$(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-commitIDE 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:
- Go to Settings → PHP → Quality Tools → PHP_CodeSniffer
- Set Configuration path:
{PROJECT_ROOT}/vendor/bin/phpcs - Go to Settings → Editor → Inspections → PHP → Quality Tools
- Enable "PHP_CodeSniffer validation"
- Set Coding standard: "Custom"
- Set Path:
{PROJECT_ROOT}/.phpcs.xml.dist
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配置:
- 前往 设置 → PHP → 质量工具 → PHP_CodeSniffer
- 设置配置路径:
{PROJECT_ROOT}/vendor/bin/phpcs - 前往 设置 → 编辑器 → 检查 → PHP → 质量工具
- 启用“PHP_CodeSniffer验证”
- 设置编码标准:“自定义”
- 设置路径:
{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.distMatrix 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.txtPHPUnit 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
fiCoverage 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: trueCoveralls Integration:
yaml
- name: Upload to Coveralls
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
path-to-lcov: ./coverage.xmlCodecov集成:
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: trueCoveralls集成:
yaml
- name: Upload to Coveralls
uses: coverallsapp/github-action@v2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
path-to-lcov: ./coverage.xmlComplete 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测试集成到自动化流水线
扩展阅读: