WordPress Testing & Quality Assurance --- progressive disclosure: entry point: summary: "WordPress plugin and theme testing with PHPUnit, WP Mock, PHPCS, and CI/CD for quality assurance" when to use: - "Testing WordPress plugins with PHPUnit integration tests" - "Unit testing without loading WordPress core (WP Mock)" - "Enforcing coding standards with PHPCS" quick start: - "Set up PHPUnit with WordPress test suite" - "Write unit tests with WP Mock" - "Configure PHPCS with WPCS ruleset" --- Testing Strategy Testing Pyramid for WordPress The WordPress Testing Hierarchy: Test Distribution Guidel…

)\n\nif [ -z \"$FILES\" ]; then\n echo \"No PHP files to check\"\n exit 0\nfi\n\necho \"Running PHPCS on changed files...\"\n\nvendor/bin/phpcs $FILES\n\nPHPCS_EXIT=$?\n\nif [ $PHPCS_EXIT -ne 0 ]; then\n echo \"\"\n echo \"PHPCS found coding standard violations.\"\n echo \"Run 'composer phpcbf' to auto-fix issues.\"\n echo \"\"\n exit 1\nfi\n\necho \"PHPCS passed!\"\nexit 0\n```\n\n**Make hook executable:**\n\n```bash\nchmod +x .git/hooks/pre-commit\n```\n\n### IDE Integration\n\n**Visual Studio Code (.vscode/settings.json):**\n\n```json\n{\n \"phpcs.enable\": true,\n \"phpcs.standard\": \"WordPress\",\n \"phpcs.executablePath\": \"${workspaceFolder}/vendor/bin/phpcs\",\n \"phpcbf.enable\": true,\n \"phpcbf.executablePath\": \"${workspaceFolder}/vendor/bin/phpcbf\",\n \"phpcbf.onsave\": false,\n \"editor.formatOnSave\": false,\n \"[php]\": {\n \"editor.defaultFormatter\": \"bmewburn.vscode-intelephense-client\",\n \"editor.formatOnSave\": true\n }\n}\n```\n\n**PHPStorm Configuration:**\n\n1. Go to **Settings → PHP → Quality Tools → PHP_CodeSniffer**\n2. Set Configuration path: `{PROJECT_ROOT}/vendor/bin/phpcs`\n3. Go to **Settings → Editor → Inspections → PHP → Quality Tools**\n4. Enable \"PHP_CodeSniffer validation\"\n5. Set Coding standard: \"Custom\"\n6. Set Path: `{PROJECT_ROOT}/.phpcs.xml.dist`\n\n---\n\n## GitHub Actions CI/CD\n\n### Workflow File Structure\n\n**.github/workflows/tests.yml:**\n\n```yaml\nname: Test Suite\n\non:\n push:\n branches: [ main, develop ]\n pull_request:\n branches: [ main ]\n\njobs:\n # Job 1: Coding Standards Check\n phpcs:\n name: PHPCS\n runs-on: ubuntu-latest\n\n steps:\n - name: Checkout code\n uses: actions/checkout@v4\n\n - name: Setup PHP\n uses: shivammathur/setup-php@v2\n with:\n php-version: '8.3'\n tools: composer\n coverage: none\n\n - name: Install dependencies\n run: composer install --prefer-dist --no-progress --no-suggest\n\n - name: Run PHPCS\n run: vendor/bin/phpcs --report=summary\n\n # Job 2: PHPUnit Tests with Matrix\n phpunit:\n name: PHPUnit (PHP ${{ matrix.php }}, WP ${{ matrix.wordpress }})\n runs-on: ubuntu-latest\n\n strategy:\n fail-fast: false\n matrix:\n php: ['8.1', '8.2', '8.3']\n wordpress: ['6.4', '6.5', '6.6', '6.7', 'latest']\n include:\n - php: '8.3'\n wordpress: 'trunk'\n\n services:\n mysql:\n image: mysql:8.0\n env:\n MYSQL_ROOT_PASSWORD: root\n MYSQL_DATABASE: wordpress_test\n ports:\n - 3306:3306\n options: --health-cmd=\"mysqladmin ping\" --health-interval=10s --health-timeout=5s --health-retries=3\n\n steps:\n - name: Checkout code\n uses: actions/checkout@v4\n\n - name: Setup PHP\n uses: shivammathur/setup-php@v2\n with:\n php-version: ${{ matrix.php }}\n extensions: mysqli, zip\n tools: composer\n coverage: xdebug\n\n - name: Install Composer dependencies\n run: composer install --prefer-dist --no-progress\n\n - name: Install WordPress test suite\n run: |\n bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1:3306 ${{ matrix.wordpress }}\n\n - name: Run PHPUnit tests\n run: vendor/bin/phpunit --coverage-clover=coverage.xml\n\n - name: Upload coverage to Codecov\n if: matrix.php == '8.3' && matrix.wordpress == 'latest'\n uses: codecov/codecov-action@v4\n with:\n files: ./coverage.xml\n flags: unittests\n name: codecov-umbrella\n\n # Job 3: WP_Mock Unit Tests\n wp-mock:\n name: WP_Mock Unit Tests\n runs-on: ubuntu-latest\n\n steps:\n - name: Checkout code\n uses: actions/checkout@v4\n\n - name: Setup PHP\n uses: shivammathur/setup-php@v2\n with:\n php-version: '8.3'\n tools: composer\n coverage: none\n\n - name: Install dependencies\n run: composer install --prefer-dist --no-progress\n\n - name: Run WP_Mock tests\n run: vendor/bin/phpunit -c phpunit-wp-mock.xml.dist\n```\n\n### Matrix Testing (Multiple PHP/WP Versions)\n\n**Strategy Explanation:**\n\n```yaml\nstrategy:\n fail-fast: false # Continue testing other versions even if one fails\n matrix:\n php: ['8.1', '8.2', '8.3'] # Test PHP versions\n wordpress: ['6.4', '6.5', '6.6', '6.7', 'latest'] # Test WP versions\n include:\n # Add specific combination not in default matrix\n - php: '8.3'\n wordpress: 'trunk' # WordPress development version\n exclude:\n # Exclude incompatible combinations\n - php: '8.1'\n wordpress: 'trunk'\n```\n\n**Matrix Results:**\n- Creates **18 test jobs** (3 PHP × 6 WordPress versions)\n- Ensures compatibility across supported versions\n- Identifies version-specific issues early\n\n### PHPCS Checks in CI\n\n**Dedicated PHPCS Job:**\n\n```yaml\nphpcs-detailed:\n name: Detailed PHPCS Report\n runs-on: ubuntu-latest\n\n steps:\n - uses: actions/checkout@v4\n\n - name: Setup PHP\n uses: shivammathur/setup-php@v2\n with:\n php-version: '8.3'\n tools: composer, cs2pr\n\n - name: Install dependencies\n run: composer install --prefer-dist --no-progress\n\n - name: Run PHPCS with annotations\n run: vendor/bin/phpcs -q --report=checkstyle | cs2pr\n\n - name: Generate PHPCS report\n if: failure()\n run: vendor/bin/phpcs --report=summary --report-file=phpcs-report.txt\n\n - name: Upload PHPCS report\n if: failure()\n uses: actions/upload-artifact@v3\n with:\n name: phpcs-report\n path: phpcs-report.txt\n```\n\n### PHPUnit Test Execution\n\n**With Code Coverage:**\n\n```yaml\nphpunit-coverage:\n name: PHPUnit with Coverage\n runs-on: ubuntu-latest\n\n services:\n mysql:\n image: mysql:8.0\n env:\n MYSQL_ROOT_PASSWORD: root\n MYSQL_DATABASE: wordpress_test\n ports:\n - 3306:3306\n options: --health-cmd=\"mysqladmin ping\" --health-interval=10s\n\n steps:\n - uses: actions/checkout@v4\n\n - name: Setup PHP with Xdebug\n uses: shivammathur/setup-php@v2\n with:\n php-version: '8.3'\n extensions: mysqli, zip, gd\n tools: composer\n coverage: xdebug\n ini-values: xdebug.mode=coverage\n\n - name: Install dependencies\n run: composer install --prefer-dist --no-progress\n\n - name: Install WordPress test suite\n run: bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1:3306 latest\n\n - name: Run tests with coverage\n run: vendor/bin/phpunit --coverage-html coverage-html --coverage-clover coverage.xml\n\n - name: Upload coverage HTML report\n uses: actions/upload-artifact@v3\n with:\n name: coverage-report\n path: coverage-html\n\n - name: Check coverage threshold\n run: |\n COVERAGE=$(vendor/bin/phpunit --coverage-text | grep \"Lines:\" | awk '{print $2}' | sed 's/%//')\n if (( $(echo \"$COVERAGE \u003c 80\" | bc -l) )); then\n echo \"Coverage $COVERAGE% is below 80% threshold\"\n exit 1\n fi\n```\n\n### Coverage Reporting\n\n**Codecov Integration:**\n\n```yaml\n- name: Upload to Codecov\n uses: codecov/codecov-action@v4\n with:\n files: ./coverage.xml\n flags: unittests\n name: codecov-umbrella\n fail_ci_if_error: true\n verbose: true\n```\n\n**Coveralls Integration:**\n\n```yaml\n- name: Upload to Coveralls\n uses: coverallsapp/github-action@v2\n with:\n github-token: ${{ secrets.GITHUB_TOKEN }}\n path-to-lcov: ./coverage.xml\n```\n\n### Complete Workflow Example\n\n**.github/workflows/ci.yml (Production-Ready):**\n\n```yaml\nname: CI Pipeline\n\non:\n push:\n branches: [ main, develop ]\n pull_request:\n branches: [ main ]\n schedule:\n - cron: '0 0 * * 0' # Weekly on Sunday\n\njobs:\n coding-standards:\n name: Coding Standards\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: shivammathur/setup-php@v2\n with:\n php-version: '8.3'\n tools: composer, cs2pr\n - run: composer install --prefer-dist --no-progress\n - run: vendor/bin/phpcs -q --report=checkstyle | cs2pr\n\n unit-tests:\n name: Unit Tests (WP_Mock)\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: shivammathur/setup-php@v2\n with:\n php-version: '8.3'\n tools: composer\n - run: composer install --prefer-dist --no-progress\n - run: vendor/bin/phpunit -c phpunit-wp-mock.xml.dist --testdox\n\n integration-tests:\n name: Integration Tests\n runs-on: ubuntu-latest\n strategy:\n matrix:\n php: ['8.1', '8.3']\n wordpress: ['6.5', 'latest']\n services:\n mysql:\n image: mysql:8.0\n env:\n MYSQL_ROOT_PASSWORD: root\n MYSQL_DATABASE: wordpress_test\n ports:\n - 3306:3306\n options: --health-cmd=\"mysqladmin ping\" --health-interval=10s\n steps:\n - uses: actions/checkout@v4\n - uses: shivammathur/setup-php@v2\n with:\n php-version: ${{ matrix.php }}\n extensions: mysqli\n tools: composer\n coverage: xdebug\n - run: composer install --prefer-dist --no-progress\n - run: bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1:3306 ${{ matrix.wordpress }}\n - run: vendor/bin/phpunit --coverage-clover=coverage.xml\n - uses: codecov/codecov-action@v4\n if: matrix.php == '8.3' && matrix.wordpress == 'latest'\n with:\n files: ./coverage.xml\n\n deploy-ready:\n name: Deployment Check\n needs: [coding-standards, unit-tests, integration-tests]\n runs-on: ubuntu-latest\n if: github.ref == 'refs/heads/main'\n steps:\n - run: echo \"All checks passed - ready for deployment\"\n```\n\n---\n\n## Testing Best Practices\n\n### Test Naming Conventions\n\n**Method Naming Pattern:**\n```\ntest_[method_name]_[scenario]_[expected_result]\n```\n\n**Examples:**\n\n```php\n// ✅ GOOD: Descriptive test names\npublic function test_sanitize_email_with_valid_email_returns_email() {}\npublic function test_sanitize_email_with_invalid_email_returns_empty_string() {}\npublic function test_save_post_meta_with_valid_data_returns_true() {}\npublic function test_user_login_with_wrong_password_returns_wp_error() {}\n\n// ❌ BAD: Vague test names\npublic function test_email() {}\npublic function test_function() {}\npublic function test_it_works() {}\n```\n\n**Class Naming:**\n```php\n// Pattern: Test_[ClassName]\nclass Test_Email_Service extends WP_UnitTestCase {}\nclass Test_Data_Validator extends WP_Mock\\Tools\\TestCase {}\nclass Test_Post_Meta_Handler extends WP_UnitTestCase {}\n```\n\n### Arrange-Act-Assert Pattern\n\n**Structure Every Test:**\n\n```php\npublic function test_calculate_discount() {\n // ARRANGE: Set up test data and conditions\n $original_price = 100;\n $discount_percent = 20;\n $calculator = new MyPlugin\\PriceCalculator();\n\n // ACT: Execute the code being tested\n $discounted_price = $calculator->apply_discount($original_price, $discount_percent);\n\n // ASSERT: Verify expected outcome\n $this->assertEquals(80, $discounted_price);\n}\n```\n\n**Complete Example:**\n\n```php\npublic function test_save_user_preferences_updates_database() {\n // ARRANGE\n $user_id = $this->factory->user->create();\n $preferences = [\n 'theme' => 'dark',\n 'notifications' => true,\n ];\n $service = new MyPlugin\\UserPreferences();\n\n // ACT\n $result = $service->save_preferences($user_id, $preferences);\n\n // ASSERT\n $this->assertTrue($result);\n $saved_prefs = get_user_meta($user_id, 'preferences', true);\n $this->assertEquals('dark', $saved_prefs['theme']);\n $this->assertTrue($saved_prefs['notifications']);\n}\n```\n\n### Data Providers\n\n**Purpose:** Test same logic with multiple inputs\n\n```php\n/**\n * @dataProvider email_validation_provider\n */\npublic function test_email_validation($email, $expected) {\n $validator = new MyPlugin\\Validator();\n $result = $validator->is_valid_email($email);\n $this->assertEquals($expected, $result);\n}\n\n/**\n * Data provider for email validation tests\n */\npublic function email_validation_provider(): array {\n return [\n 'valid email' => ['[email protected]', true],\n 'invalid no at' => ['userexample.com', false],\n 'invalid no domain' => ['user@', false],\n 'invalid spaces' => ['user @example.com', false],\n 'valid subdomain' => ['[email protected]', true],\n 'invalid special chars' => ['user#@example.com', false],\n ];\n}\n```\n\n**Complex Data Provider:**\n\n```php\n/**\n * @dataProvider discount_calculation_provider\n */\npublic function test_discount_calculation($price, $discount, $expected) {\n $calculator = new MyPlugin\\PriceCalculator();\n $result = $calculator->apply_discount($price, $discount);\n $this->assertEquals($expected, $result);\n}\n\npublic function discount_calculation_provider(): array {\n return [\n '20% off 100' => [100, 20, 80],\n '50% off 100' => [100, 50, 50],\n '0% off 100' => [100, 0, 100],\n '100% off 100' => [100, 100, 0],\n '20% off 0' => [0, 20, 0],\n ];\n}\n```\n\n### Testing Hooks and Filters\n\n**Testing add_action/add_filter:**\n\n```php\npublic function test_init_hooks_registered() {\n // Remove all hooks first\n remove_all_actions('init');\n\n // Register plugin hooks\n MyPlugin\\Hooks::register();\n\n // Verify action was added\n $this->assertTrue(has_action('init', 'MyPlugin\\PostTypes::register'));\n $this->assertEquals(10, has_action('init', 'MyPlugin\\PostTypes::register'));\n}\n\npublic function test_content_filter_registered() {\n remove_all_filters('the_content');\n\n MyPlugin\\Hooks::register();\n\n $this->assertTrue(has_filter('the_content', 'MyPlugin\\Content::add_reading_time'));\n}\n```\n\n**Testing Hook Callbacks:**\n\n```php\npublic function test_save_post_hook_saves_meta() {\n $post_id = $this->factory->post->create([\n 'post_type' => 'book',\n ]);\n\n $_POST['book_isbn'] = '978-3-16-148410-0';\n $_POST['book_nonce'] = wp_create_nonce('save_book_meta');\n\n // Manually trigger the hook callback\n do_action('save_post', $post_id);\n\n // Verify meta was saved\n $isbn = get_post_meta($post_id, '_isbn', true);\n $this->assertEquals('978-3-16-148410-0', $isbn);\n}\n```\n\n### Testing AJAX Handlers\n\n**AJAX Test Setup:**\n\n```php\npublic function test_ajax_load_more_posts() {\n // Create test posts\n $post_ids = $this->factory->post->create_many(5);\n\n // Set up AJAX request\n $_POST['action'] = 'load_more_posts';\n $_POST['page'] = 1;\n $_POST['nonce'] = wp_create_nonce('load_more_nonce');\n\n // Set current user (if authentication required)\n wp_set_current_user($this->factory->user->create(['role' => 'subscriber']));\n\n // Capture output\n try {\n $this->_handleAjax('load_more_posts');\n } catch (WPAjaxDieContinueException $e) {\n // Expected exception\n }\n\n // Get response\n $response = json_decode($this->_last_response, true);\n\n $this->assertTrue($response['success']);\n $this->assertCount(5, $response['data']['posts']);\n}\n```\n\n---\n\n## Common Testing Patterns\n\n### Testing Custom Post Types\n\n```php\nclass Test_Book_Post_Type extends WP_UnitTestCase {\n\n public function setUp(): void {\n parent::setUp();\n // Ensure CPT is registered\n MyPlugin\\PostTypes::register_book();\n }\n\n public function test_book_post_type_exists() {\n $this->assertTrue(post_type_exists('book'));\n }\n\n public function test_book_supports_features() {\n $post_type = get_post_type_object('book');\n\n $this->assertTrue(post_type_supports('book', 'title'));\n $this->assertTrue(post_type_supports('book', 'editor'));\n $this->assertTrue(post_type_supports('book', 'thumbnail'));\n $this->assertFalse(post_type_supports('book', 'comments'));\n }\n\n public function test_book_has_rest_support() {\n $post_type = get_post_type_object('book');\n $this->assertTrue($post_type->show_in_rest);\n }\n\n public function test_create_book_post() {\n $book_id = $this->factory->post->create([\n 'post_type' => 'book',\n 'post_title' => 'The Great Gatsby',\n ]);\n\n $book = get_post($book_id);\n $this->assertEquals('book', $book->post_type);\n $this->assertEquals('The Great Gatsby', $book->post_title);\n }\n}\n```\n\n### Testing Settings/Options\n\n```php\nclass Test_Plugin_Settings extends WP_UnitTestCase {\n\n public function tearDown(): void {\n delete_option('my_plugin_settings');\n parent::tearDown();\n }\n\n public function test_default_settings_created() {\n $settings = MyPlugin\\Settings::get_defaults();\n\n $this->assertIsArray($settings);\n $this->assertArrayHasKey('api_key', $settings);\n $this->assertEquals('', $settings['api_key']);\n }\n\n public function test_save_settings() {\n $new_settings = [\n 'api_key' => 'test_key_123',\n 'enabled' => true,\n ];\n\n $result = MyPlugin\\Settings::save($new_settings);\n $this->assertTrue($result);\n\n $saved = get_option('my_plugin_settings');\n $this->assertEquals('test_key_123', $saved['api_key']);\n $this->assertTrue($saved['enabled']);\n }\n\n public function test_sanitize_settings() {\n $dirty_input = [\n 'api_key' => '\u003cscript>alert(\"xss\")\u003c/script>',\n 'enabled' => 'yes',\n ];\n\n $clean = MyPlugin\\Settings::sanitize($dirty_input);\n\n $this->assertEquals('alert(\"xss\")', $clean['api_key']);\n $this->assertTrue($clean['enabled']);\n }\n}\n```\n\n### Testing Database Operations\n\n```php\nclass Test_Database_Operations extends WP_UnitTestCase {\n\n protected static $table_name;\n\n public static function setUpBeforeClass(): void {\n parent::setUpBeforeClass();\n\n global $wpdb;\n self::$table_name = $wpdb->prefix . 'plugin_logs';\n\n $charset_collate = $wpdb->get_charset_collate();\n $sql = \"CREATE TABLE \" . self::$table_name . \" (\n id bigint(20) unsigned NOT NULL AUTO_INCREMENT,\n user_id bigint(20) unsigned NOT NULL,\n action varchar(50) NOT NULL,\n created_at datetime DEFAULT CURRENT_TIMESTAMP,\n PRIMARY KEY (id)\n ) $charset_collate;\";\n\n require_once ABSPATH . 'wp-admin/includes/upgrade.php';\n dbDelta($sql);\n }\n\n public static function tearDownAfterClass(): void {\n global $wpdb;\n $wpdb->query(\"DROP TABLE IF EXISTS \" . self::$table_name);\n parent::tearDownAfterClass();\n }\n\n public function test_insert_log_entry() {\n global $wpdb;\n\n $user_id = 1;\n $action = 'user_login';\n\n $result = $wpdb->insert(\n self::$table_name,\n [\n 'user_id' => $user_id,\n 'action' => $action,\n ],\n ['%d', '%s']\n );\n\n $this->assertEquals(1, $result);\n $this->assertGreaterThan(0, $wpdb->insert_id);\n\n // Verify data\n $log = $wpdb->get_row(\n $wpdb->prepare(\n \"SELECT * FROM \" . self::$table_name . \" WHERE id = %d\",\n $wpdb->insert_id\n )\n );\n\n $this->assertEquals($user_id, $log->user_id);\n $this->assertEquals($action, $log->action);\n }\n\n public function test_query_logs_by_user() {\n global $wpdb;\n\n $user_id = 42;\n\n // Insert test data\n $wpdb->insert(self::$table_name, ['user_id' => $user_id, 'action' => 'login'], ['%d', '%s']);\n $wpdb->insert(self::$table_name, ['user_id' => $user_id, 'action' => 'logout'], ['%d', '%s']);\n\n // Query logs\n $logs = $wpdb->get_results(\n $wpdb->prepare(\n \"SELECT * FROM \" . self::$table_name . \" WHERE user_id = %d\",\n $user_id\n )\n );\n\n $this->assertCount(2, $logs);\n }\n}\n```\n\n### Testing REST API Endpoints\n\n```php\nclass Test_REST_API extends WP_UnitTestCase {\n\n protected $server;\n\n public function setUp(): void {\n parent::setUp();\n\n global $wp_rest_server;\n $this->server = $wp_rest_server = new WP_REST_Server();\n do_action('rest_api_init');\n }\n\n public function test_endpoint_registered() {\n $routes = $this->server->get_routes();\n $this->assertArrayHasKey('/myplugin/v1/items', $routes);\n }\n\n public function test_get_items_endpoint() {\n // Create test posts\n $post_ids = $this->factory->post->create_many(3, ['post_type' => 'book']);\n\n $request = new WP_REST_Request('GET', '/myplugin/v1/items');\n $response = $this->server->dispatch($request);\n\n $this->assertEquals(200, $response->get_status());\n\n $data = $response->get_data();\n $this->assertCount(3, $data);\n }\n\n public function test_create_item_requires_authentication() {\n $request = new WP_REST_Request('POST', '/myplugin/v1/items');\n $request->set_body_params([\n 'title' => 'New Item',\n ]);\n\n $response = $this->server->dispatch($request);\n\n $this->assertEquals(401, $response->get_status());\n }\n\n public function test_create_item_with_authentication() {\n $user_id = $this->factory->user->create(['role' => 'editor']);\n wp_set_current_user($user_id);\n\n $request = new WP_REST_Request('POST', '/myplugin/v1/items');\n $request->set_body_params([\n 'title' => 'New Item',\n 'content' => 'Item content',\n ]);\n\n $response = $this->server->dispatch($request);\n\n $this->assertEquals(201, $response->get_status());\n\n $data = $response->get_data();\n $this->assertEquals('New Item', $data['title']);\n }\n}\n```\n\n---\n\n**Related Skills:**\nWhen testing WordPress applications, consider these complementary skills (available in the skill library):\n\n- **WordPress Plugin Fundamentals**: Core plugin architecture and hooks - essential foundation for understanding what to test\n- **WordPress Security & Validation**: Security patterns and data validation - critical for security testing strategies\n- **Python pytest Testing**: Modern testing patterns - concepts applicable to WordPress testing approaches\n- **GitHub Actions CI/CD**: CI/CD automation - integrate WordPress tests into automated pipelines\n\n**Further Reading:**\n- [WordPress PHPUnit Documentation](https://make.wordpress.org/core/handbook/testing/automated-testing/writing-phpunit-tests/)\n- [WP_Mock GitHub Repository](https://github.com/10up/wp_mock)\n- [WordPress Coding Standards](https://developer.wordpress.org/coding-standards/)\n- [PHPUnit Documentation](https://phpunit.de/documentation.html)\n---","attachment_filenames":["metadata.json","README.md"],"attachments":[{"filename":"metadata.json","content":"{\n \"name\": \"wordpress-testing-qa\",\n \"version\": \"1.0.0\",\n \"category\": \"toolchain\",\n \"toolchain\": \"php\",\n \"tags\": [\n \"wordpress\",\n \"php\",\n \"testing\",\n \"phpunit\",\n \"wp-mock\",\n \"phpcs\",\n \"quality-assurance\",\n \"ci-cd\",\n \"github-actions\",\n \"code-coverage\",\n \"coding-standards\"\n ],\n \"entry_point_tokens\": 106,\n \"full_tokens\": 12579,\n \"related_skills\": [\n \"../plugin-fundamentals\",\n \"../security-validation\",\n \"../block-editor\",\n \"../../../../python/testing/pytest\",\n \"../../../../universal/infrastructure/github-actions\"\n ],\n \"description\": \"WordPress plugin and theme testing with PHPUnit integration tests, WP_Mock unit tests, PHPCS coding standards enforcement, and GitHub Actions CI/CD pipelines\",\n \"requirements\": {\n \"wordpress\": \">=6.4\",\n \"php\": \">=8.1\",\n \"tools\": [\n \"composer\",\n \"phpunit\",\n \"phpcs\",\n \"wp-cli\"\n ]\n },\n \"dependencies\": [\n \"../plugin-fundamentals\",\n \"../security-validation\",\n \"../../../../php/fundamentals\",\n \"../../../../php/composer\"\n ],\n \"learning_path\": [\n \"Understanding the WordPress testing pyramid\",\n \"Setting up PHPUnit with WordPress test suite\",\n \"Writing integration tests with WP_UnitTestCase\",\n \"Unit testing with WP_Mock (no WordPress dependency)\",\n \"Enforcing coding standards with PHPCS and WPCS\",\n \"Configuring GitHub Actions for CI/CD\",\n \"Implementing test coverage requirements\",\n \"Testing common WordPress patterns (CPTs, hooks, AJAX, REST API)\"\n ],\n \"key_concepts\": [\n \"PHPUnit integration testing with WordPress\",\n \"WP_Mock for isolated unit testing\",\n \"Test fixtures and factories\",\n \"WordPress Coding Standards (WPCS)\",\n \"Continuous Integration workflows\",\n \"Code coverage measurement\",\n \"Test-Driven Development (TDD)\",\n \"Arrange-Act-Assert pattern\"\n ],\n \"progressive_disclosure\": {\n \"entry_point_tokens\": 78,\n \"full_content_tokens\": 5200,\n \"expansion_ratio\": 66.7\n },\n \"examples\": {\n \"phpunit_integration\": {\n \"description\": \"WordPress test suite setup with factory objects\",\n \"complexity\": \"intermediate\",\n \"lines\": 50\n },\n \"wp_mock_unit\": {\n \"description\": \"Unit testing without WordPress using WP_Mock\",\n \"complexity\": \"intermediate\",\n \"lines\": 40\n },\n \"phpcs_config\": {\n \"description\": \"PHPCS configuration with WPCS ruleset\",\n \"complexity\": \"beginner\",\n \"lines\": 30\n },\n \"github_actions\": {\n \"description\": \"Complete CI/CD workflow with matrix testing\",\n \"complexity\": \"advanced\",\n \"lines\": 100\n },\n \"ajax_testing\": {\n \"description\": \"Testing WordPress AJAX handlers\",\n \"complexity\": \"advanced\",\n \"lines\": 35\n },\n \"rest_api_testing\": {\n \"description\": \"Testing custom REST API endpoints\",\n \"complexity\": \"advanced\",\n \"lines\": 45\n }\n },\n \"testing_coverage\": {\n \"unit_tests\": \"60%\",\n \"integration_tests\": \"30%\",\n \"e2e_tests\": \"10%\",\n \"minimum_coverage\": \"80%\",\n \"critical_paths\": \"95%\"\n },\n \"tools\": {\n \"phpunit\": {\n \"version\": \"^9.6\",\n \"purpose\": \"WordPress integration testing\"\n },\n \"wp_mock\": {\n \"version\": \"^1.0\",\n \"purpose\": \"Unit testing without WordPress\"\n },\n \"phpcs\": {\n \"version\": \"^3.7\",\n \"purpose\": \"Coding standards enforcement\"\n },\n \"wpcs\": {\n \"version\": \"^3.0\",\n \"purpose\": \"WordPress-specific coding rules\"\n },\n \"phpcompatibility\": {\n \"version\": \"*\",\n \"purpose\": \"PHP version compatibility checking\"\n }\n },\n \"ci_cd\": {\n \"platforms\": [\n \"GitHub Actions\",\n \"GitLab CI\",\n \"CircleCI\",\n \"Travis CI\"\n ],\n \"matrix_testing\": {\n \"php_versions\": [\n \"8.1\",\n \"8.2\",\n \"8.3\"\n ],\n \"wordpress_versions\": [\n \"6.4\",\n \"6.5\",\n \"6.6\",\n \"6.7\",\n \"latest\",\n \"trunk\"\n ]\n },\n \"coverage_reporting\": [\n \"Codecov\",\n \"Coveralls\"\n ]\n },\n \"best_practices\": [\n \"Follow test pyramid: 60% unit, 30% integration, 10% E2E\",\n \"Use WP_Mock for pure logic, PHPUnit for WordPress interactions\",\n \"Maintain 80% minimum code coverage for new code\",\n \"Test critical paths at 95% coverage\",\n \"Use data providers for testing multiple scenarios\",\n \"Follow Arrange-Act-Assert pattern consistently\",\n \"Run PHPCS in pre-commit hooks\",\n \"Test across multiple PHP and WordPress versions\",\n \"Use descriptive test names explaining scenario and outcome\",\n \"Mock external dependencies in unit tests\"\n ],\n \"common_pitfalls\": [\n \"Loading entire WordPress for unit tests (use WP_Mock instead)\",\n \"Not cleaning up test data in tearDown()\",\n \"Testing WordPress core functions instead of your code\",\n \"Skipping security-critical function tests\",\n \"Not testing error conditions and edge cases\",\n \"Hardcoding WordPress table prefixes instead of using $wpdb->prefix\",\n \"Forgetting to set up database fixtures before tests\",\n \"Not mocking external API calls in tests\"\n ],\n \"skill_level\": \"intermediate\",\n \"estimated_time\": \"8-12 hours\",\n \"last_updated\": \"2025-01-30\",\n \"maintained_by\": \"claude-mpm-skills\",\n \"documentation_urls\": [\n \"https://make.wordpress.org/core/handbook/testing/automated-testing/\",\n \"https://make.wordpress.org/core/handbook/testing/automated-testing/writing-phpunit-tests/\",\n \"https://github.com/10up/wp_mock\",\n \"https://github.com/WordPress/WordPress-Coding-Standards\",\n \"https://phpunit.de/documentation.html\"\n ]\n}\n","content_type":"application/json; charset=utf-8","language":"json","size":5545,"content_sha256":"10bfd01bf8655ca72828e775761f0a75e72ed9db2cff521a58372a03afa723fd"},{"filename":"README.md","content":"# WordPress Testing & Quality Assurance Skill\n\n**Version:** 1.0.0 \n**Last Updated:** January 30, 2025 \n**Skill Level:** Intermediate \n**Estimated Time:** 8-12 hours\n\n## Overview\n\nComprehensive guide to WordPress plugin and theme testing using PHPUnit integration tests, WP_Mock unit tests, PHPCS coding standards enforcement, and GitHub Actions CI/CD pipelines.\n\n## What You'll Learn\n\n- **Testing Strategy**: WordPress testing pyramid (60% unit, 30% integration, 10% E2E)\n- **PHPUnit Integration**: WordPress test suite with factory objects and fixtures\n- **WP_Mock Unit Testing**: Isolated testing without loading WordPress\n- **PHPCS Standards**: Enforcing WordPress Coding Standards\n- **CI/CD Pipelines**: Automated testing with GitHub Actions\n- **Coverage Requirements**: Achieving 80%+ code coverage\n- **Common Patterns**: Testing CPTs, hooks, AJAX, REST API endpoints\n\n## Quick Start\n\n### 1. PHPUnit Setup\n```bash\ncomposer require --dev phpunit/phpunit \"^9.6\"\nwp scaffold plugin-tests my-plugin\nbash bin/install-wp-tests.sh wordpress_test root '' localhost latest\nvendor/bin/phpunit\n```\n\n### 2. WP_Mock Unit Tests\n```bash\ncomposer require --dev 10up/wp_mock \"^1.0\"\nvendor/bin/phpunit -c phpunit-wp-mock.xml.dist\n```\n\n### 3. PHPCS Standards\n```bash\ncomposer require --dev wp-coding-standards/wpcs:\"^3.0\"\nvendor/bin/phpcs\nvendor/bin/phpcbf # Auto-fix issues\n```\n\n## Key Features\n\n### Testing Tools\n- **PHPUnit 9.6+**: WordPress integration testing\n- **WP_Mock 1.0+**: Fast unit tests without WordPress\n- **PHPCS 3.7+**: Coding standards enforcement\n- **WPCS 3.0+**: WordPress-specific rules\n- **GitHub Actions**: Automated CI/CD pipelines\n\n### Testing Pyramid\n```\n /\\\n /E2E\\ 10% - Browser automation\n /------\\\n /INTEGR \\ 30% - WordPress + database\n /----------\\\n /UNIT TESTS \\ 60% - Pure logic, WP_Mock\n```\n\n### Coverage Goals\n- **New Code**: 80% minimum\n- **Critical Paths**: 95% (auth, payments, validation)\n- **Legacy Code**: Gradual improvement\n- **Public APIs**: 100% coverage\n\n## Progressive Disclosure\n\n**Entry Point** (~78 tokens):\n- Quick summary and when to use\n- Three quick start commands\n\n**Full Content** (~5,200 tokens):\n1. Testing Strategy (600 tokens)\n2. PHPUnit Integration (1,200 tokens)\n3. WP_Mock Unit Testing (1,000 tokens)\n4. PHPCS & Standards (900 tokens)\n5. GitHub Actions CI/CD (800 tokens)\n6. Testing Best Practices (500 tokens)\n7. Common Testing Patterns (500 tokens)\n\n## File Structure\n\n```\ntesting-qa/\n├── SKILL.md # Main skill content\n├── metadata.json # Skill metadata\n└── README.md # This file\n```\n\n## Requirements\n\n- **WordPress**: 6.4+\n- **PHP**: 8.1+ (8.3 recommended)\n- **Composer**: Latest version\n- **Docker**: For wp-env (optional)\n\n## Related Skills\n\nRelated skills available in the skill library:\n- **WordPress Plugin Fundamentals**: Core plugin architecture and hooks\n- **WordPress Security & Validation**: Security patterns and data validation\n- **WordPress Block Editor**: Modern block development and testing\n- **Python pytest Testing**: Testing patterns applicable to WordPress\n- **GitHub Actions**: CI/CD automation for WordPress testing pipelines\n\n## Testing Workflow Example\n\n```bash\n# 1. Install dependencies\ncomposer install\n\n# 2. Run coding standards check\ncomposer phpcs\n\n# 3. Auto-fix PHPCS issues\ncomposer phpcbf\n\n# 4. Run unit tests (WP_Mock)\nvendor/bin/phpunit -c phpunit-wp-mock.xml.dist\n\n# 5. Run integration tests (PHPUnit + WordPress)\nvendor/bin/phpunit\n\n# 6. Generate coverage report\nvendor/bin/phpunit --coverage-html coverage/\n```\n\n## GitHub Actions Example\n\n```yaml\nname: CI Pipeline\n\non: [push, pull_request]\n\njobs:\n phpcs:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: shivammathur/setup-php@v2\n with:\n php-version: '8.3'\n - run: composer install\n - run: vendor/bin/phpcs\n\n phpunit:\n runs-on: ubuntu-latest\n strategy:\n matrix:\n php: ['8.1', '8.3']\n wordpress: ['6.5', 'latest']\n steps:\n - uses: actions/checkout@v4\n - run: composer install\n - run: bash bin/install-wp-tests.sh wordpress_test root root localhost ${{ matrix.wordpress }}\n - run: vendor/bin/phpunit --coverage-clover=coverage.xml\n```\n\n## Common Patterns Covered\n\n### Custom Post Types\n- Registration testing\n- Supports features verification\n- REST API enablement\n- Meta data handling\n\n### Hooks and Filters\n- Action registration testing\n- Filter callback verification\n- Priority testing\n- Custom hook creation\n\n### AJAX Handlers\n- Request simulation\n- Response validation\n- Nonce verification\n- Authentication testing\n\n### REST API Endpoints\n- Route registration\n- Permission callbacks\n- Data validation\n- Response format testing\n\n## Best Practices\n\n1. **Test Pyramid**: 60% unit, 30% integration, 10% E2E\n2. **Isolation**: Use WP_Mock for pure logic\n3. **Coverage**: 80% minimum for new code\n4. **Naming**: Descriptive test names (test_method_scenario_result)\n5. **AAA Pattern**: Arrange-Act-Assert structure\n6. **Data Providers**: Test multiple scenarios efficiently\n7. **CI/CD**: Automate testing on every push\n8. **Matrix Testing**: Test across PHP/WP versions\n\n## Token Budget Compliance\n\n- **Entry Point**: 78 tokens (Target: 70-85) ✅\n- **Full Content**: ~5,200 tokens (Target: 5,000-5,500) ✅\n- **Expansion Ratio**: 66.7x\n\n## Documentation\n\n- [WordPress PHPUnit Handbook](https://make.wordpress.org/core/handbook/testing/automated-testing/)\n- [WP_Mock GitHub](https://github.com/10up/wp_mock)\n- [WordPress Coding Standards](https://github.com/WordPress/WordPress-Coding-Standards)\n- [PHPUnit Documentation](https://phpunit.de/documentation.html)\n\n## License\n\nThis skill is part of the claude-mpm-skills library and follows the same licensing as the parent project.\n\n---\n\n**Maintained by**: claude-mpm-skills \n**Research Date**: January 30, 2025 \n**WordPress Version**: 6.7+ \n**PHP Version**: 8.3 (recommended)\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":5950,"content_sha256":"11d6d9ae80362aefccca110504fde3d0a312ee174f7493d3be8fce4fd273cb78"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"WordPress Testing & Quality Assurance","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"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\"","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Testing Strategy","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Testing Pyramid for WordPress","type":"text"}]},{"type":"paragraph","content":[{"text":"The WordPress Testing Hierarchy:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":" /\\\n / \\ E2E Tests (Playwright)\n / \\ - Full user workflows\n /------\\ - Browser automation\n / \\\n / INTEG \\ Integration Tests (PHPUnit + WordPress)\n / TESTS \\ - Database operations\n/ \\ - Hook interactions\n--------------\n UNIT TESTS Unit Tests (WP_Mock)\n - Pure logic\n - No WordPress dependency","type":"text"}]},{"type":"paragraph","content":[{"text":"Test Distribution Guidelines:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Unit Tests (60%):","type":"text","marks":[{"type":"strong"}]},{"text":" Fast, isolated, no WordPress","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pure PHP functions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Class methods with clear inputs/outputs","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Business logic without side effects","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Integration Tests (30%):","type":"text","marks":[{"type":"strong"}]},{"text":" WordPress-loaded tests","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Database operations","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Hook/filter interactions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Custom post type registration","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Settings API functionality","type":"text"}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"E2E Tests (10%):","type":"text","marks":[{"type":"strong"}]},{"text":" Browser automation","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Critical user workflows","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Admin panel interactions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Frontend form submissions","type":"text"}]}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"When to Use PHPUnit vs WP_Mock","type":"text"}]},{"type":"paragraph","content":[{"text":"Use PHPUnit (Integration Tests) when:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"✅ Testing database operations (","type":"text"},{"text":"$wpdb","type":"text","marks":[{"type":"code_inline"}]},{"text":", post creation, meta data)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"✅ Testing WordPress hooks (actions/filters actually firing)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"✅ Testing template rendering and output","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"✅ Testing plugin activation/deactivation logic","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"✅ Testing with actual WordPress functions","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Use WP_Mock (Unit Tests) when:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"✅ Testing pure business logic","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"✅ Testing functions that call WordPress functions but logic is independent","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"✅ Need fast test execution (no database setup)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"✅ Testing in isolation without side effects","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"✅ Mocking external API calls","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Test Coverage Goals","type":"text"}]},{"type":"paragraph","content":[{"text":"Minimum Coverage Requirements:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"New Code:","type":"text","marks":[{"type":"strong"}]},{"text":" 80% minimum coverage","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Critical Paths:","type":"text","marks":[{"type":"strong"}]},{"text":" 95% coverage (payment processing, authentication, data validation)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Legacy Code:","type":"text","marks":[{"type":"strong"}]},{"text":" Gradual improvement, prioritize high-risk areas","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Public APIs:","type":"text","marks":[{"type":"strong"}]},{"text":" 100% coverage for all public methods","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"What to Test (Priority Order):","type":"text","marks":[{"type":"strong"}]}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Security Functions:","type":"text","marks":[{"type":"strong"}]},{"text":" Nonce verification, sanitization, capability checks","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Data Operations:","type":"text","marks":[{"type":"strong"}]},{"text":" Database CRUD, data validation, transformation","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Business Logic:","type":"text","marks":[{"type":"strong"}]},{"text":" Calculations, workflows, state transitions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Hook Callbacks:","type":"text","marks":[{"type":"strong"}]},{"text":" Action/filter handlers","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Public APIs:","type":"text","marks":[{"type":"strong"}]},{"text":" REST endpoints, WP-CLI commands","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"What NOT to Test:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"❌ WordPress core functions (assume they work)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"❌ Third-party library internals","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"❌ Simple getters/setters with no logic","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"❌ Configuration files (theme.json, block.json)","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"PHPUnit Integration Testing","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"WordPress Test Suite Setup","type":"text"}]},{"type":"paragraph","content":[{"text":"Step 1: Install Dependencies","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Install PHPUnit and WordPress polyfills\ncomposer require --dev phpunit/phpunit \"^9.6\"\ncomposer require --dev yoast/phpunit-polyfills \"^2.0\"\n\n# Generate test scaffold with WP-CLI\nwp scaffold plugin-tests my-plugin\n\n# This creates:\n# - tests/bootstrap.php\n# - tests/test-sample.php\n# - phpunit.xml.dist\n# - bin/install-wp-tests.sh","type":"text"}]},{"type":"paragraph","content":[{"text":"Step 2: Install WordPress Test Library","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Install WordPress test suite and test database\n# Syntax: bash bin/install-wp-tests.sh \u003cdb-name> \u003cdb-user> \u003cdb-pass> \u003cdb-host> \u003cwp-version>\nbash bin/install-wp-tests.sh wordpress_test root '' localhost latest\n\n# For specific WordPress version:\nbash bin/install-wp-tests.sh wordpress_test root '' localhost 6.7","type":"text"}]},{"type":"paragraph","content":[{"text":"Step 3: Configure phpunit.xml.dist","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"xml"},"content":[{"text":"\u003c?xml version=\"1.0\"?>\n\u003cphpunit\n bootstrap=\"tests/bootstrap.php\"\n backupGlobals=\"false\"\n colors=\"true\"\n convertErrorsToExceptions=\"true\"\n convertNoticesToExceptions=\"true\"\n convertWarningsToExceptions=\"true\"\n stopOnFailure=\"false\"\n>\n \u003ctestsuites>\n \u003ctestsuite name=\"plugin\">\n \u003cdirectory prefix=\"test-\" suffix=\".php\">./tests/\u003c/directory>\n \u003cexclude>./tests/bootstrap.php\u003c/exclude>\n \u003c/testsuite>\n \u003c/testsuites>\n\n \u003ccoverage includeUncoveredFiles=\"true\">\n \u003cinclude>\n \u003cdirectory suffix=\".php\">./includes/\u003c/directory>\n \u003c/include>\n \u003cexclude>\n \u003cdirectory>./vendor/\u003c/directory>\n \u003cdirectory>./tests/\u003c/directory>\n \u003c/exclude>\n \u003creport>\n \u003chtml outputDirectory=\"coverage-html\"/>\n \u003ctext outputFile=\"php://stdout\" showOnlySummary=\"true\"/>\n \u003c/report>\n \u003c/coverage>\n\n \u003cphp>\n \u003cconst name=\"WP_TESTS_PHPUNIT_POLYFILLS_PATH\" value=\"vendor/yoast/phpunit-polyfills\"/>\n \u003c/php>\n\u003c/phpunit>","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"WP_UnitTestCase Base Class","type":"text"}]},{"type":"paragraph","content":[{"text":"tests/bootstrap.php:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"php"},"content":[{"text":"\u003c?php\n/**\n * PHPUnit bootstrap file\n */\n\n// Composer autoloader\nrequire_once dirname(__DIR__) . '/vendor/autoload.php';\n\n// WordPress tests directory\n$_tests_dir = getenv('WP_TESTS_DIR');\nif (!$_tests_dir) {\n $_tests_dir = rtrim(sys_get_temp_dir(), '/\\\\') . '/wordpress-tests-lib';\n}\n\nif (!file_exists(\"{$_tests_dir}/includes/functions.php\")) {\n throw new Exception(\"Could not find {$_tests_dir}/includes/functions.php\");\n}\n\n// Give access to tests_add_filter() function\nrequire_once \"{$_tests_dir}/includes/functions.php\";\n\n/**\n * Manually load the plugin being tested\n */\nfunction _manually_load_plugin() {\n require dirname(__DIR__) . '/my-plugin.php';\n}\ntests_add_filter('muplugins_loaded', '_manually_load_plugin');\n\n// Start up the WordPress testing environment\nrequire \"{$_tests_dir}/includes/bootstrap.php\";","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Factory Objects for Test Data","type":"text"}]},{"type":"paragraph","content":[{"text":"Using Built-in Factories:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"php"},"content":[{"text":"\u003c?php\nclass Test_Plugin_Integration extends WP_UnitTestCase {\n\n /**\n * Test creating posts with factory\n */\n public function test_create_post_with_meta() {\n // Create a post using factory\n $post_id = $this->factory->post->create([\n 'post_title' => 'Test Post',\n 'post_content' => 'Test content for integration test',\n 'post_status' => 'publish',\n 'post_type' => 'post',\n ]);\n\n $this->assertIsInt($post_id);\n $this->assertGreaterThan(0, $post_id);\n\n // Add post meta\n add_post_meta($post_id, '_custom_field', 'custom_value');\n\n // Verify meta was saved\n $meta_value = get_post_meta($post_id, '_custom_field', true);\n $this->assertEquals('custom_value', $meta_value);\n }\n\n /**\n * Test creating users\n */\n public function test_user_can_edit_post() {\n // Create editor user\n $editor_id = $this->factory->user->create([\n 'role' => 'editor',\n 'user_login' => 'test_editor',\n 'user_email' => '[email protected]',\n ]);\n\n // Set as current user\n wp_set_current_user($editor_id);\n\n // Create post\n $post_id = $this->factory->post->create([\n 'post_author' => $editor_id,\n ]);\n\n // Test capabilities\n $this->assertTrue(current_user_can('edit_post', $post_id));\n $this->assertTrue(current_user_can('edit_posts'));\n $this->assertFalse(current_user_can('manage_options'));\n }\n\n /**\n * Test creating terms and taxonomy\n */\n public function test_assign_categories() {\n // Create category\n $category_id = $this->factory->category->create([\n 'name' => 'Test Category',\n 'slug' => 'test-category',\n ]);\n\n // Create post\n $post_id = $this->factory->post->create();\n\n // Assign category\n wp_set_post_categories($post_id, [$category_id]);\n\n // Verify assignment\n $categories = wp_get_post_categories($post_id);\n $this->assertContains($category_id, $categories);\n }\n\n /**\n * Test creating comments\n */\n public function test_post_has_comments() {\n $post_id = $this->factory->post->create();\n\n // Create multiple comments\n $comment_ids = $this->factory->comment->create_many(3, [\n 'comment_post_ID' => $post_id,\n 'comment_approved' => 1,\n ]);\n\n $this->assertCount(3, $comment_ids);\n\n // Get comments for post\n $comments = get_comments(['post_id' => $post_id]);\n $this->assertCount(3, $comments);\n }\n}","type":"text"}]},{"type":"paragraph","content":[{"text":"Available Factory Objects:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"$this->factory->post","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Posts, pages, custom post types","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"$this->factory->user","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Users with roles","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"$this->factory->term","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Terms (categories, tags, custom taxonomies)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"$this->factory->category","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Categories specifically","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"$this->factory->tag","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Tags specifically","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"$this->factory->comment","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Comments","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"$this->factory->blog","type":"text","marks":[{"type":"code_inline"}]},{"text":" - Multisite blogs","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Database Fixtures and Teardown","type":"text"}]},{"type":"paragraph","content":[{"text":"setUp() and tearDown() Methods:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"php"},"content":[{"text":"\u003c?php\nclass Test_Custom_Post_Type extends WP_UnitTestCase {\n\n protected $post_ids = [];\n\n /**\n * Setup runs before EACH test method\n */\n public function setUp(): void {\n parent::setUp();\n\n // Register custom post type\n register_post_type('book', [\n 'public' => true,\n 'supports' => ['title', 'editor'],\n ]);\n\n // Create test data\n $this->post_ids = $this->factory->post->create_many(5, [\n 'post_type' => 'book',\n ]);\n }\n\n /**\n * Teardown runs after EACH test method\n */\n public function tearDown(): void {\n // Clean up test data\n foreach ($this->post_ids as $post_id) {\n wp_delete_post($post_id, true); // Force delete\n }\n\n // Unregister post type\n unregister_post_type('book');\n\n parent::tearDown();\n }\n\n /**\n * Test that books are created\n */\n public function test_books_created() {\n $this->assertCount(5, $this->post_ids);\n\n $query = new WP_Query([\n 'post_type' => 'book',\n 'posts_per_page' => -1,\n ]);\n\n $this->assertEquals(5, $query->found_posts);\n }\n}","type":"text"}]},{"type":"paragraph","content":[{"text":"setUpBeforeClass() and tearDownAfterClass():","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"php"},"content":[{"text":"\u003c?php\nclass Test_Plugin_Database extends WP_UnitTestCase {\n\n protected static $table_name;\n\n /**\n * Runs ONCE before all tests in class\n */\n public static function setUpBeforeClass(): void {\n parent::setUpBeforeClass();\n\n global $wpdb;\n self::$table_name = $wpdb->prefix . 'plugin_data';\n\n // Create custom table\n $charset_collate = $wpdb->get_charset_collate();\n $sql = \"CREATE TABLE \" . self::$table_name . \" (\n id bigint(20) unsigned NOT NULL AUTO_INCREMENT,\n user_id bigint(20) unsigned NOT NULL,\n data_value varchar(255) NOT NULL,\n created_at datetime DEFAULT CURRENT_TIMESTAMP,\n PRIMARY KEY (id),\n KEY user_id (user_id)\n ) $charset_collate;\";\n\n require_once ABSPATH . 'wp-admin/includes/upgrade.php';\n dbDelta($sql);\n }\n\n /**\n * Runs ONCE after all tests in class\n */\n public static function tearDownAfterClass(): void {\n global $wpdb;\n $wpdb->query(\"DROP TABLE IF EXISTS \" . self::$table_name);\n\n parent::tearDownAfterClass();\n }\n\n /**\n * Test table exists\n */\n public function test_custom_table_exists() {\n global $wpdb;\n $table_exists = $wpdb->get_var(\n \"SHOW TABLES LIKE '\" . self::$table_name . \"'\"\n );\n $this->assertEquals(self::$table_name, $table_exists);\n }\n\n /**\n * Test insert data\n */\n public function test_insert_data() {\n global $wpdb;\n\n $result = $wpdb->insert(\n self::$table_name,\n [\n 'user_id' => 1,\n 'data_value' => 'test_value',\n ],\n ['%d', '%s']\n );\n\n $this->assertEquals(1, $result);\n $this->assertGreaterThan(0, $wpdb->insert_id);\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Complete Plugin Test Example","type":"text"}]},{"type":"paragraph","content":[{"text":"tests/test-plugin-functionality.php:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"php"},"content":[{"text":"\u003c?php\n/**\n * Test plugin core functionality\n */\nclass Test_Plugin_Functionality extends WP_UnitTestCase {\n\n /**\n * Test plugin registers custom post type\n */\n public function test_custom_post_type_registered() {\n $this->assertTrue(post_type_exists('book'));\n\n $post_type = get_post_type_object('book');\n $this->assertTrue($post_type->public);\n $this->assertTrue($post_type->show_in_rest);\n }\n\n /**\n * Test custom taxonomy registration\n */\n public function test_custom_taxonomy_registered() {\n $this->assertTrue(taxonomy_exists('genre'));\n\n $taxonomy = get_taxonomy('genre');\n $this->assertTrue($taxonomy->hierarchical);\n $this->assertContains('book', $taxonomy->object_type);\n }\n\n /**\n * Test saving custom meta data\n */\n public function test_save_book_metadata() {\n $book_id = $this->factory->post->create([\n 'post_type' => 'book',\n 'post_title' => 'Test Book',\n ]);\n\n // Simulate saving meta (as would happen in save_post hook)\n update_post_meta($book_id, '_isbn', '978-3-16-148410-0');\n update_post_meta($book_id, '_author', 'John Doe');\n update_post_meta($book_id, '_publication_year', 2024);\n\n // Verify meta saved correctly\n $this->assertEquals('978-3-16-148410-0', get_post_meta($book_id, '_isbn', true));\n $this->assertEquals('John Doe', get_post_meta($book_id, '_author', true));\n $this->assertEquals(2024, get_post_meta($book_id, '_publication_year', true));\n }\n\n /**\n * Test shortcode output\n */\n public function test_book_shortcode_output() {\n $book_id = $this->factory->post->create([\n 'post_type' => 'book',\n 'post_title' => 'The Great Gatsby',\n ]);\n\n update_post_meta($book_id, '_author', 'F. Scott Fitzgerald');\n\n // Test shortcode\n $output = do_shortcode('[book id=\"' . $book_id . '\"]');\n\n $this->assertStringContainsString('The Great Gatsby', $output);\n $this->assertStringContainsString('F. Scott Fitzgerald', $output);\n }\n\n /**\n * Test action hook fires correctly\n */\n public function test_book_published_action_fires() {\n $action_fired = false;\n\n // Add temporary hook to verify action fires\n add_action('my_plugin_book_published', function($post_id) use (&$action_fired) {\n $action_fired = true;\n });\n\n // Create published book (should trigger action)\n $book_id = $this->factory->post->create([\n 'post_type' => 'book',\n 'post_status' => 'publish',\n ]);\n\n // Manually trigger the action (simulating what plugin does)\n do_action('my_plugin_book_published', $book_id);\n\n $this->assertTrue($action_fired, 'Book published action did not fire');\n }\n\n /**\n * Test filter modifies content\n */\n public function test_reading_time_filter() {\n $content = str_repeat('word ', 200); // 200 words\n\n // Apply filter\n $filtered = apply_filters('my_plugin_content_filter', $content);\n\n $this->assertStringContainsString('reading time', strtolower($filtered));\n $this->assertStringContainsString('1 min', $filtered);\n }\n}","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"WP_Mock Unit Testing","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"What is WP_Mock and When to Use It","type":"text"}]},{"type":"paragraph","content":[{"text":"WP_Mock Purpose:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Test PHP code ","type":"text"},{"text":"without loading WordPress","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Mock WordPress functions to return expected values","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Verify WordPress functions are called with correct arguments","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Much faster than integration tests (no database setup)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"When to Use WP_Mock:","type":"text","marks":[{"type":"strong"}]}]},{"type":"paragraph","content":[{"text":"✅ ","type":"text"},{"text":"Perfect for:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pure business logic that calls WordPress functions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Data transformation/validation functions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Service classes with WordPress dependencies","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Testing in continuous integration (faster CI builds)","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"❌ ","type":"text"},{"text":"NOT Suitable for:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Testing actual database operations","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Testing hook interactions between plugins","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Testing template rendering","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Testing functions that rely on WordPress state","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Installation and Setup","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Install WP_Mock and Mockery\ncomposer require --dev mockery/mockery \"^1.6\"\ncomposer require --dev 10up/wp_mock \"^1.0\"\ncomposer require --dev phpunit/phpunit \"^9.6\"","type":"text"}]},{"type":"paragraph","content":[{"text":"tests/bootstrap-wp-mock.php:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"php"},"content":[{"text":"\u003c?php\n/**\n * Bootstrap file for WP_Mock tests\n */\n\nrequire_once __DIR__ . '/../vendor/autoload.php';\n\n// WP_Mock setup\nWP_Mock::bootstrap();\n\n// Define WordPress constants if needed\nif (!defined('ABSPATH')) {\n define('ABSPATH', '/path/to/wordpress/');\n}","type":"text"}]},{"type":"paragraph","content":[{"text":"phpunit-wp-mock.xml.dist:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"xml"},"content":[{"text":"\u003c?xml version=\"1.0\"?>\n\u003cphpunit\n bootstrap=\"tests/bootstrap-wp-mock.php\"\n backupGlobals=\"false\"\n colors=\"true\"\n convertErrorsToExceptions=\"true\"\n convertNoticesToExceptions=\"true\"\n convertWarningsToExceptions=\"true\"\n>\n \u003ctestsuites>\n \u003ctestsuite name=\"unit\">\n \u003cdirectory prefix=\"test-\" suffix=\".php\">./tests/unit/\u003c/directory>\n \u003c/testsuite>\n \u003c/testsuites>\n\u003c/phpunit>","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Mocking WordPress Functions","type":"text"}]},{"type":"paragraph","content":[{"text":"tests/unit/test-data-processor.php:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"php"},"content":[{"text":"\u003c?php\nuse WP_Mock\\Tools\\TestCase;\n\nclass Test_Data_Processor extends TestCase {\n\n public function setUp(): void {\n WP_Mock::setUp();\n }\n\n public function tearDown(): void {\n WP_Mock::tearDown();\n }\n\n /**\n * Test sanitization function\n */\n public function test_sanitize_input() {\n // Mock sanitize_text_field\n WP_Mock::userFunction('sanitize_text_field', [\n 'times' => 1,\n 'args' => ['\u003cscript>alert(\"xss\")\u003c/script>'],\n 'return' => 'alert(\"xss\")', // WordPress strips tags\n ]);\n\n $processor = new MyPlugin\\DataProcessor();\n $result = $processor->sanitize_input('\u003cscript>alert(\"xss\")\u003c/script>');\n\n $this->assertEquals('alert(\"xss\")', $result);\n }\n\n /**\n * Test get_option is called\n */\n public function test_get_setting() {\n // Mock get_option call\n WP_Mock::userFunction('get_option', [\n 'times' => 1,\n 'args' => ['my_plugin_api_key', ''],\n 'return' => 'test_api_key_12345',\n ]);\n\n $processor = new MyPlugin\\DataProcessor();\n $api_key = $processor->get_api_key();\n\n $this->assertEquals('test_api_key_12345', $api_key);\n }\n\n /**\n * Test multiple function calls with different returns\n */\n public function test_user_data_retrieval() {\n $user_id = 42;\n\n // Mock get_user_meta\n WP_Mock::userFunction('get_user_meta', [\n 'times' => 1,\n 'args' => [$user_id, 'first_name', true],\n 'return' => 'John',\n ]);\n\n WP_Mock::userFunction('get_user_meta', [\n 'times' => 1,\n 'args' => [$user_id, 'last_name', true],\n 'return' => 'Doe',\n ]);\n\n $processor = new MyPlugin\\DataProcessor();\n $full_name = $processor->get_user_full_name($user_id);\n\n $this->assertEquals('John Doe', $full_name);\n }\n\n /**\n * Test function with type matcher\n */\n public function test_save_data_with_array() {\n // Accept any array as second argument\n WP_Mock::userFunction('update_option', [\n 'times' => 1,\n 'args' => [\n 'my_plugin_settings',\n WP_Mock\\Functions::type('array'),\n ],\n 'return' => true,\n ]);\n\n $processor = new MyPlugin\\DataProcessor();\n $result = $processor->save_settings(['api_key' => 'test123']);\n\n $this->assertTrue($result);\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Mocking Filters and Actions","type":"text"}]},{"type":"paragraph","content":[{"text":"Testing add_filter() Calls:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"php"},"content":[{"text":"\u003c?php\nclass Test_Hook_Registration extends WP_Mock\\Tools\\TestCase {\n\n public function setUp(): void {\n WP_Mock::setUp();\n }\n\n public function tearDown(): void {\n WP_Mock::tearDown();\n }\n\n /**\n * Test that filter is registered\n */\n public function test_content_filter_registered() {\n // Expect filter to be added\n WP_Mock::expectFilterAdded(\n 'the_content',\n 'MyPlugin\\ContentFilter::add_reading_time',\n 10,\n 1\n );\n\n // Execute function that adds the filter\n MyPlugin\\Hooks::register_filters();\n\n // Verify expectations met\n $this->assertConditionsMet();\n }\n\n /**\n * Test that action is registered\n */\n public function test_init_action_registered() {\n WP_Mock::expectActionAdded(\n 'init',\n 'MyPlugin\\PostTypes::register_custom_post_types',\n 10,\n 0\n );\n\n MyPlugin\\Hooks::register_actions();\n\n $this->assertConditionsMet();\n }\n\n /**\n * Test apply_filters modifies value\n */\n public function test_apply_custom_filter() {\n $original_value = 100;\n $filtered_value = 150;\n\n // Mock apply_filters\n WP_Mock::onFilter('my_plugin_price')\n ->with($original_value)\n ->reply($filtered_value);\n\n $processor = new MyPlugin\\PriceCalculator();\n $result = $processor->get_final_price($original_value);\n\n $this->assertEquals($filtered_value, $result);\n }\n\n /**\n * Test do_action is called\n */\n public function test_custom_action_fired() {\n $order_id = 12345;\n\n // Expect action to be fired with specific arguments\n WP_Mock::expectAction('my_plugin_order_processed', $order_id);\n\n $processor = new MyPlugin\\OrderProcessor();\n $processor->process_order($order_id);\n\n $this->assertConditionsMet();\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Testing in Isolation (No WordPress Dependency)","type":"text"}]},{"type":"paragraph","content":[{"text":"Example: Email Service Class:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"php"},"content":[{"text":"\u003c?php\nnamespace MyPlugin;\n\nclass EmailService {\n\n public function send_notification(string $to, string $message): bool {\n $subject = $this->get_email_subject();\n $headers = $this->get_email_headers();\n\n return wp_mail($to, $subject, $message, $headers);\n }\n\n protected function get_email_subject(): string {\n $site_name = get_bloginfo('name');\n return sprintf('[%s] Notification', $site_name);\n }\n\n protected function get_email_headers(): array {\n $admin_email = get_option('admin_email');\n return [\n 'From: ' . $admin_email,\n 'Content-Type: text/html; charset=UTF-8',\n ];\n }\n}","type":"text"}]},{"type":"paragraph","content":[{"text":"Unit Test Without WordPress:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"php"},"content":[{"text":"\u003c?php\nuse WP_Mock\\Tools\\TestCase;\n\nclass Test_Email_Service extends TestCase {\n\n public function setUp(): void {\n WP_Mock::setUp();\n }\n\n public function tearDown(): void {\n WP_Mock::tearDown();\n }\n\n /**\n * Test email sending logic\n */\n public function test_send_notification_email() {\n // Mock get_bloginfo\n WP_Mock::userFunction('get_bloginfo', [\n 'args' => 'name',\n 'return' => 'My WordPress Site',\n ]);\n\n // Mock get_option\n WP_Mock::userFunction('get_option', [\n 'args' => 'admin_email',\n 'return' => '[email protected]',\n ]);\n\n // Mock wp_mail and verify arguments\n WP_Mock::userFunction('wp_mail', [\n 'times' => 1,\n 'args' => [\n '[email protected]',\n '[My WordPress Site] Notification',\n 'Test message content',\n WP_Mock\\Functions::type('array'),\n ],\n 'return' => true,\n ]);\n\n $service = new MyPlugin\\EmailService();\n $result = $service->send_notification(\n '[email protected]',\n 'Test message content'\n );\n\n $this->assertTrue($result);\n }\n\n /**\n * Test email failure handling\n */\n public function test_email_send_failure() {\n WP_Mock::userFunction('get_bloginfo', [\n 'return' => 'Test Site',\n ]);\n\n WP_Mock::userFunction('get_option', [\n 'return' => '[email protected]',\n ]);\n\n // Simulate wp_mail failure\n WP_Mock::userFunction('wp_mail', [\n 'return' => false,\n ]);\n\n $service = new MyPlugin\\EmailService();\n $result = $service->send_notification('[email protected]', 'Message');\n\n $this->assertFalse($result);\n }\n}","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"PHPCS & Coding Standards","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Installing PHPCS and WPCS","type":"text"}]},{"type":"paragraph","content":[{"text":"via Composer (Recommended):","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Allow PHPCS composer installer plugin\ncomposer config allow-plugins.dealerdirect/phpcodesniffer-composer-installer true\n\n# Install WordPress Coding Standards\ncomposer require --dev wp-coding-standards/wpcs:\"^3.0\"\n\n# Install PHP Compatibility checker\ncomposer require --dev phpcompatibility/phpcompatibility-wp:\"*\"\n\n# Install PHPCS itself (if not already installed)\ncomposer require --dev squizlabs/php_codesniffer:\"^3.7\"\n\n# Verify installation\nvendor/bin/phpcs -i\n# Should show: WordPress, WordPress-Core, WordPress-Docs, WordPress-Extra","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":".phpcs.xml.dist Configuration","type":"text"}]},{"type":"paragraph","content":[{"text":"Complete Configuration File:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"xml"},"content":[{"text":"\u003c?xml version=\"1.0\"?>\n\u003cruleset xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n name=\"WordPress Plugin Coding Standards\"\n xsi:noNamespaceSchemaLocation=\"https://raw.githubusercontent.com/squizlabs/PHP_CodeSniffer/master/phpcs.xsd\">\n\n \u003cdescription>Custom coding standards for WordPress plugin\u003c/description>\n\n \u003c!-- What to scan -->\n \u003cfile>./includes\u003c/file>\n \u003cfile>./my-plugin.php\u003c/file>\n\n \u003c!-- Exclude patterns -->\n \u003cexclude-pattern>*/vendor/*\u003c/exclude-pattern>\n \u003cexclude-pattern>*/node_modules/*\u003c/exclude-pattern>\n \u003cexclude-pattern>*/tests/*\u003c/exclude-pattern>\n \u003cexclude-pattern>*/build/*\u003c/exclude-pattern>\n \u003cexclude-pattern>*/.git/*\u003c/exclude-pattern>\n\n \u003c!-- Show progress -->\n \u003carg value=\"ps\"/>\n \u003carg name=\"colors\"/>\n \u003carg name=\"extensions\" value=\"php\"/>\n \u003carg name=\"parallel\" value=\"8\"/>\n\n \u003c!-- Rules: Use WordPress-Extra ruleset -->\n \u003crule ref=\"WordPress-Extra\">\n \u003c!-- Allow short array syntax [] instead of array() -->\n \u003cexclude name=\"Generic.Arrays.DisallowShortArraySyntax\"/>\n\n \u003c!-- Allow multiple assignments in single line -->\n \u003cexclude name=\"Squiz.PHP.DisallowMultipleAssignments\"/>\n\n \u003c!-- Relax file comment requirements -->\n \u003cexclude name=\"Squiz.Commenting.FileComment\"/>\n \u003c/rule>\n\n \u003c!-- WordPress.WP.I18n: Check text domain -->\n \u003crule ref=\"WordPress.WP.I18n\">\n \u003cproperties>\n \u003cproperty name=\"text_domain\" type=\"array\">\n \u003celement value=\"my-plugin\"/>\n \u003c/property>\n \u003c/properties>\n \u003c/rule>\n\n \u003c!-- WordPress.NamingConventions.PrefixAllGlobals: Check function/class prefixes -->\n \u003crule ref=\"WordPress.NamingConventions.PrefixAllGlobals\">\n \u003cproperties>\n \u003cproperty name=\"prefixes\" type=\"array\">\n \u003celement value=\"my_plugin\"/>\n \u003celement value=\"MyPlugin\"/>\n \u003c/property>\n \u003c/properties>\n \u003c/rule>\n\n \u003c!-- PHP version compatibility -->\n \u003cconfig name=\"testVersion\" value=\"8.1-\"/>\n \u003crule ref=\"PHPCompatibilityWP\"/>\n\n \u003c!-- Minimum supported WordPress version -->\n \u003cconfig name=\"minimum_wp_version\" value=\"6.4\"/>\n\n \u003c!-- Exclude specific rules for test files -->\n \u003crule ref=\"WordPress.Files.FileName\">\n \u003cexclude-pattern>*/tests/*\u003c/exclude-pattern>\n \u003c/rule>\n\n \u003c!-- Enforce line length limit (warning at 80, error at 120) -->\n \u003crule ref=\"Generic.Files.LineLength\">\n \u003cproperties>\n \u003cproperty name=\"lineLimit\" value=\"120\"/>\n \u003cproperty name=\"absoluteLineLimit\" value=\"150\"/>\n \u003c/properties>\n \u003c/rule>\n\n \u003c!-- Allow WordPress globals to be modified -->\n \u003crule ref=\"WordPress.WP.GlobalVariablesOverride\">\n \u003ctype>error\u003c/type>\n \u003c/rule>\n\u003c/ruleset>","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Running PHPCS and PHPCBF","type":"text"}]},{"type":"paragraph","content":[{"text":"Command Line Usage:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# Check all files\nvendor/bin/phpcs\n\n# Check specific file\nvendor/bin/phpcs includes/Core.php\n\n# Show error codes\nvendor/bin/phpcs -s\n\n# Show only errors (hide warnings)\nvendor/bin/phpcs -n\n\n# Generate report summary\nvendor/bin/phpcs --report=summary\n\n# Check single file with detailed output\nvendor/bin/phpcs -v includes/Admin/Settings.php\n\n# Auto-fix fixable issues\nvendor/bin/phpcbf\n\n# Auto-fix specific file\nvendor/bin/phpcbf includes/Core.php\n\n# Dry run (show what would be fixed)\nvendor/bin/phpcbf --dry-run\n\n# Use specific standard\nvendor/bin/phpcs --standard=WordPress-Core includes/\n\n# Generate different report formats\nvendor/bin/phpcs --report=json > phpcs-report.json\nvendor/bin/phpcs --report=xml > phpcs-report.xml\nvendor/bin/phpcs --report=csv > phpcs-report.csv","type":"text"}]},{"type":"paragraph","content":[{"text":"composer.json Scripts:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"scripts\": {\n \"phpcs\": \"phpcs\",\n \"phpcbf\": \"phpcbf\",\n \"phpcs:check\": \"phpcs --report=summary\",\n \"phpcs:fix\": \"phpcbf\",\n \"test\": [\n \"@phpcs\",\n \"phpunit\"\n ]\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Pre-commit Hooks","type":"text"}]},{"type":"paragraph","content":[{"text":"Install pre-commit hook (.git/hooks/pre-commit):","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"#!/bin/bash\n\n# Run PHPCS on changed PHP files\nFILES=$(git diff --cached --name-only --diff-filter=ACM | grep '.php

WordPress Testing & Quality Assurance --- progressive disclosure: entry point: summary: "WordPress plugin and theme testing with PHPUnit, WP Mock, PHPCS, and CI/CD for quality assurance" when to use: - "Testing WordPress plugins with PHPUnit integration tests" - "Unit testing without loading WordPress core (WP Mock)" - "Enforcing coding standards with PHPCS" quick start: - "Set up PHPUnit with WordPress test suite" - "Write unit tests with WP Mock" - "Configure PHPCS with WPCS ruleset" --- Testing Strategy Testing Pyramid for WordPress The WordPress Testing Hierarchy: Test Distribution Guidel…

)\n\nif [ -z \"$FILES\" ]; then\n echo \"No PHP files to check\"\n exit 0\nfi\n\necho \"Running PHPCS on changed files...\"\n\nvendor/bin/phpcs $FILES\n\nPHPCS_EXIT=$?\n\nif [ $PHPCS_EXIT -ne 0 ]; then\n echo \"\"\n echo \"PHPCS found coding standard violations.\"\n echo \"Run 'composer phpcbf' to auto-fix issues.\"\n echo \"\"\n exit 1\nfi\n\necho \"PHPCS passed!\"\nexit 0","type":"text"}]},{"type":"paragraph","content":[{"text":"Make hook executable:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"chmod +x .git/hooks/pre-commit","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"IDE Integration","type":"text"}]},{"type":"paragraph","content":[{"text":"Visual Studio Code (.vscode/settings.json):","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\n \"phpcs.enable\": true,\n \"phpcs.standard\": \"WordPress\",\n \"phpcs.executablePath\": \"${workspaceFolder}/vendor/bin/phpcs\",\n \"phpcbf.enable\": true,\n \"phpcbf.executablePath\": \"${workspaceFolder}/vendor/bin/phpcbf\",\n \"phpcbf.onsave\": false,\n \"editor.formatOnSave\": false,\n \"[php]\": {\n \"editor.defaultFormatter\": \"bmewburn.vscode-intelephense-client\",\n \"editor.formatOnSave\": true\n }\n}","type":"text"}]},{"type":"paragraph","content":[{"text":"PHPStorm Configuration:","type":"text","marks":[{"type":"strong"}]}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Go to ","type":"text"},{"text":"Settings → PHP → Quality Tools → PHP_CodeSniffer","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Set Configuration path: ","type":"text"},{"text":"{PROJECT_ROOT}/vendor/bin/phpcs","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Go to ","type":"text"},{"text":"Settings → Editor → Inspections → PHP → Quality Tools","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Enable \"PHP_CodeSniffer validation\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Set Coding standard: \"Custom\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Set Path: ","type":"text"},{"text":"{PROJECT_ROOT}/.phpcs.xml.dist","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"GitHub Actions CI/CD","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Workflow File Structure","type":"text"}]},{"type":"paragraph","content":[{"text":".github/workflows/tests.yml:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"yaml"},"content":[{"text":"name: Test Suite\n\non:\n push:\n branches: [ main, develop ]\n pull_request:\n branches: [ main ]\n\njobs:\n # Job 1: Coding Standards Check\n phpcs:\n name: PHPCS\n runs-on: ubuntu-latest\n\n steps:\n - name: Checkout code\n uses: actions/checkout@v4\n\n - name: Setup PHP\n uses: shivammathur/setup-php@v2\n with:\n php-version: '8.3'\n tools: composer\n coverage: none\n\n - name: Install dependencies\n run: composer install --prefer-dist --no-progress --no-suggest\n\n - name: Run PHPCS\n run: vendor/bin/phpcs --report=summary\n\n # Job 2: PHPUnit Tests with Matrix\n phpunit:\n name: PHPUnit (PHP ${{ matrix.php }}, WP ${{ matrix.wordpress }})\n runs-on: ubuntu-latest\n\n strategy:\n fail-fast: false\n matrix:\n php: ['8.1', '8.2', '8.3']\n wordpress: ['6.4', '6.5', '6.6', '6.7', 'latest']\n include:\n - php: '8.3'\n wordpress: 'trunk'\n\n services:\n mysql:\n image: mysql:8.0\n env:\n MYSQL_ROOT_PASSWORD: root\n MYSQL_DATABASE: wordpress_test\n ports:\n - 3306:3306\n options: --health-cmd=\"mysqladmin ping\" --health-interval=10s --health-timeout=5s --health-retries=3\n\n steps:\n - name: Checkout code\n uses: actions/checkout@v4\n\n - name: Setup PHP\n uses: shivammathur/setup-php@v2\n with:\n php-version: ${{ matrix.php }}\n extensions: mysqli, zip\n tools: composer\n coverage: xdebug\n\n - name: Install Composer dependencies\n run: composer install --prefer-dist --no-progress\n\n - name: Install WordPress test suite\n run: |\n bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1:3306 ${{ matrix.wordpress }}\n\n - name: Run PHPUnit tests\n run: vendor/bin/phpunit --coverage-clover=coverage.xml\n\n - name: Upload coverage to Codecov\n if: matrix.php == '8.3' && matrix.wordpress == 'latest'\n uses: codecov/codecov-action@v4\n with:\n files: ./coverage.xml\n flags: unittests\n name: codecov-umbrella\n\n # Job 3: WP_Mock Unit Tests\n wp-mock:\n name: WP_Mock Unit Tests\n runs-on: ubuntu-latest\n\n steps:\n - name: Checkout code\n uses: actions/checkout@v4\n\n - name: Setup PHP\n uses: shivammathur/setup-php@v2\n with:\n php-version: '8.3'\n tools: composer\n coverage: none\n\n - name: Install dependencies\n run: composer install --prefer-dist --no-progress\n\n - name: Run WP_Mock tests\n run: vendor/bin/phpunit -c phpunit-wp-mock.xml.dist","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Matrix Testing (Multiple PHP/WP Versions)","type":"text"}]},{"type":"paragraph","content":[{"text":"Strategy Explanation:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"yaml"},"content":[{"text":"strategy:\n fail-fast: false # Continue testing other versions even if one fails\n matrix:\n php: ['8.1', '8.2', '8.3'] # Test PHP versions\n wordpress: ['6.4', '6.5', '6.6', '6.7', 'latest'] # Test WP versions\n include:\n # Add specific combination not in default matrix\n - php: '8.3'\n wordpress: 'trunk' # WordPress development version\n exclude:\n # Exclude incompatible combinations\n - php: '8.1'\n wordpress: 'trunk'","type":"text"}]},{"type":"paragraph","content":[{"text":"Matrix Results:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Creates ","type":"text"},{"text":"18 test jobs","type":"text","marks":[{"type":"strong"}]},{"text":" (3 PHP × 6 WordPress versions)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Ensures compatibility across supported versions","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Identifies version-specific issues early","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"PHPCS Checks in CI","type":"text"}]},{"type":"paragraph","content":[{"text":"Dedicated PHPCS Job:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"yaml"},"content":[{"text":"phpcs-detailed:\n name: Detailed PHPCS Report\n runs-on: ubuntu-latest\n\n steps:\n - uses: actions/checkout@v4\n\n - name: Setup PHP\n uses: shivammathur/setup-php@v2\n with:\n php-version: '8.3'\n tools: composer, cs2pr\n\n - name: Install dependencies\n run: composer install --prefer-dist --no-progress\n\n - name: Run PHPCS with annotations\n run: vendor/bin/phpcs -q --report=checkstyle | cs2pr\n\n - name: Generate PHPCS report\n if: failure()\n run: vendor/bin/phpcs --report=summary --report-file=phpcs-report.txt\n\n - name: Upload PHPCS report\n if: failure()\n uses: actions/upload-artifact@v3\n with:\n name: phpcs-report\n path: phpcs-report.txt","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"PHPUnit Test Execution","type":"text"}]},{"type":"paragraph","content":[{"text":"With Code Coverage:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"yaml"},"content":[{"text":"phpunit-coverage:\n name: PHPUnit with Coverage\n runs-on: ubuntu-latest\n\n services:\n mysql:\n image: mysql:8.0\n env:\n MYSQL_ROOT_PASSWORD: root\n MYSQL_DATABASE: wordpress_test\n ports:\n - 3306:3306\n options: --health-cmd=\"mysqladmin ping\" --health-interval=10s\n\n steps:\n - uses: actions/checkout@v4\n\n - name: Setup PHP with Xdebug\n uses: shivammathur/setup-php@v2\n with:\n php-version: '8.3'\n extensions: mysqli, zip, gd\n tools: composer\n coverage: xdebug\n ini-values: xdebug.mode=coverage\n\n - name: Install dependencies\n run: composer install --prefer-dist --no-progress\n\n - name: Install WordPress test suite\n run: bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1:3306 latest\n\n - name: Run tests with coverage\n run: vendor/bin/phpunit --coverage-html coverage-html --coverage-clover coverage.xml\n\n - name: Upload coverage HTML report\n uses: actions/upload-artifact@v3\n with:\n name: coverage-report\n path: coverage-html\n\n - name: Check coverage threshold\n run: |\n COVERAGE=$(vendor/bin/phpunit --coverage-text | grep \"Lines:\" | awk '{print $2}' | sed 's/%//')\n if (( $(echo \"$COVERAGE \u003c 80\" | bc -l) )); then\n echo \"Coverage $COVERAGE% is below 80% threshold\"\n exit 1\n fi","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Coverage Reporting","type":"text"}]},{"type":"paragraph","content":[{"text":"Codecov Integration:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"yaml"},"content":[{"text":"- name: Upload to Codecov\n uses: codecov/codecov-action@v4\n with:\n files: ./coverage.xml\n flags: unittests\n name: codecov-umbrella\n fail_ci_if_error: true\n verbose: true","type":"text"}]},{"type":"paragraph","content":[{"text":"Coveralls Integration:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"yaml"},"content":[{"text":"- name: Upload to Coveralls\n uses: coverallsapp/github-action@v2\n with:\n github-token: ${{ secrets.GITHUB_TOKEN }}\n path-to-lcov: ./coverage.xml","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Complete Workflow Example","type":"text"}]},{"type":"paragraph","content":[{"text":".github/workflows/ci.yml (Production-Ready):","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"yaml"},"content":[{"text":"name: CI Pipeline\n\non:\n push:\n branches: [ main, develop ]\n pull_request:\n branches: [ main ]\n schedule:\n - cron: '0 0 * * 0' # Weekly on Sunday\n\njobs:\n coding-standards:\n name: Coding Standards\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: shivammathur/setup-php@v2\n with:\n php-version: '8.3'\n tools: composer, cs2pr\n - run: composer install --prefer-dist --no-progress\n - run: vendor/bin/phpcs -q --report=checkstyle | cs2pr\n\n unit-tests:\n name: Unit Tests (WP_Mock)\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: shivammathur/setup-php@v2\n with:\n php-version: '8.3'\n tools: composer\n - run: composer install --prefer-dist --no-progress\n - run: vendor/bin/phpunit -c phpunit-wp-mock.xml.dist --testdox\n\n integration-tests:\n name: Integration Tests\n runs-on: ubuntu-latest\n strategy:\n matrix:\n php: ['8.1', '8.3']\n wordpress: ['6.5', 'latest']\n services:\n mysql:\n image: mysql:8.0\n env:\n MYSQL_ROOT_PASSWORD: root\n MYSQL_DATABASE: wordpress_test\n ports:\n - 3306:3306\n options: --health-cmd=\"mysqladmin ping\" --health-interval=10s\n steps:\n - uses: actions/checkout@v4\n - uses: shivammathur/setup-php@v2\n with:\n php-version: ${{ matrix.php }}\n extensions: mysqli\n tools: composer\n coverage: xdebug\n - run: composer install --prefer-dist --no-progress\n - run: bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1:3306 ${{ matrix.wordpress }}\n - run: vendor/bin/phpunit --coverage-clover=coverage.xml\n - uses: codecov/codecov-action@v4\n if: matrix.php == '8.3' && matrix.wordpress == 'latest'\n with:\n files: ./coverage.xml\n\n deploy-ready:\n name: Deployment Check\n needs: [coding-standards, unit-tests, integration-tests]\n runs-on: ubuntu-latest\n if: github.ref == 'refs/heads/main'\n steps:\n - run: echo \"All checks passed - ready for deployment\"","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Testing Best Practices","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Test Naming Conventions","type":"text"}]},{"type":"paragraph","content":[{"text":"Method Naming Pattern:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"test_[method_name]_[scenario]_[expected_result]","type":"text"}]},{"type":"paragraph","content":[{"text":"Examples:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"php"},"content":[{"text":"// ✅ GOOD: Descriptive test names\npublic function test_sanitize_email_with_valid_email_returns_email() {}\npublic function test_sanitize_email_with_invalid_email_returns_empty_string() {}\npublic function test_save_post_meta_with_valid_data_returns_true() {}\npublic function test_user_login_with_wrong_password_returns_wp_error() {}\n\n// ❌ BAD: Vague test names\npublic function test_email() {}\npublic function test_function() {}\npublic function test_it_works() {}","type":"text"}]},{"type":"paragraph","content":[{"text":"Class Naming:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"php"},"content":[{"text":"// Pattern: Test_[ClassName]\nclass Test_Email_Service extends WP_UnitTestCase {}\nclass Test_Data_Validator extends WP_Mock\\Tools\\TestCase {}\nclass Test_Post_Meta_Handler extends WP_UnitTestCase {}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Arrange-Act-Assert Pattern","type":"text"}]},{"type":"paragraph","content":[{"text":"Structure Every Test:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"php"},"content":[{"text":"public function test_calculate_discount() {\n // ARRANGE: Set up test data and conditions\n $original_price = 100;\n $discount_percent = 20;\n $calculator = new MyPlugin\\PriceCalculator();\n\n // ACT: Execute the code being tested\n $discounted_price = $calculator->apply_discount($original_price, $discount_percent);\n\n // ASSERT: Verify expected outcome\n $this->assertEquals(80, $discounted_price);\n}","type":"text"}]},{"type":"paragraph","content":[{"text":"Complete Example:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"php"},"content":[{"text":"public function test_save_user_preferences_updates_database() {\n // ARRANGE\n $user_id = $this->factory->user->create();\n $preferences = [\n 'theme' => 'dark',\n 'notifications' => true,\n ];\n $service = new MyPlugin\\UserPreferences();\n\n // ACT\n $result = $service->save_preferences($user_id, $preferences);\n\n // ASSERT\n $this->assertTrue($result);\n $saved_prefs = get_user_meta($user_id, 'preferences', true);\n $this->assertEquals('dark', $saved_prefs['theme']);\n $this->assertTrue($saved_prefs['notifications']);\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Data Providers","type":"text"}]},{"type":"paragraph","content":[{"text":"Purpose:","type":"text","marks":[{"type":"strong"}]},{"text":" Test same logic with multiple inputs","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"php"},"content":[{"text":"/**\n * @dataProvider email_validation_provider\n */\npublic function test_email_validation($email, $expected) {\n $validator = new MyPlugin\\Validator();\n $result = $validator->is_valid_email($email);\n $this->assertEquals($expected, $result);\n}\n\n/**\n * Data provider for email validation tests\n */\npublic function email_validation_provider(): array {\n return [\n 'valid email' => ['[email protected]', true],\n 'invalid no at' => ['userexample.com', false],\n 'invalid no domain' => ['user@', false],\n 'invalid spaces' => ['user @example.com', false],\n 'valid subdomain' => ['[email protected]', true],\n 'invalid special chars' => ['user#@example.com', false],\n ];\n}","type":"text"}]},{"type":"paragraph","content":[{"text":"Complex Data Provider:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"php"},"content":[{"text":"/**\n * @dataProvider discount_calculation_provider\n */\npublic function test_discount_calculation($price, $discount, $expected) {\n $calculator = new MyPlugin\\PriceCalculator();\n $result = $calculator->apply_discount($price, $discount);\n $this->assertEquals($expected, $result);\n}\n\npublic function discount_calculation_provider(): array {\n return [\n '20% off 100' => [100, 20, 80],\n '50% off 100' => [100, 50, 50],\n '0% off 100' => [100, 0, 100],\n '100% off 100' => [100, 100, 0],\n '20% off 0' => [0, 20, 0],\n ];\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Testing Hooks and Filters","type":"text"}]},{"type":"paragraph","content":[{"text":"Testing add_action/add_filter:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"php"},"content":[{"text":"public function test_init_hooks_registered() {\n // Remove all hooks first\n remove_all_actions('init');\n\n // Register plugin hooks\n MyPlugin\\Hooks::register();\n\n // Verify action was added\n $this->assertTrue(has_action('init', 'MyPlugin\\PostTypes::register'));\n $this->assertEquals(10, has_action('init', 'MyPlugin\\PostTypes::register'));\n}\n\npublic function test_content_filter_registered() {\n remove_all_filters('the_content');\n\n MyPlugin\\Hooks::register();\n\n $this->assertTrue(has_filter('the_content', 'MyPlugin\\Content::add_reading_time'));\n}","type":"text"}]},{"type":"paragraph","content":[{"text":"Testing Hook Callbacks:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"php"},"content":[{"text":"public function test_save_post_hook_saves_meta() {\n $post_id = $this->factory->post->create([\n 'post_type' => 'book',\n ]);\n\n $_POST['book_isbn'] = '978-3-16-148410-0';\n $_POST['book_nonce'] = wp_create_nonce('save_book_meta');\n\n // Manually trigger the hook callback\n do_action('save_post', $post_id);\n\n // Verify meta was saved\n $isbn = get_post_meta($post_id, '_isbn', true);\n $this->assertEquals('978-3-16-148410-0', $isbn);\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Testing AJAX Handlers","type":"text"}]},{"type":"paragraph","content":[{"text":"AJAX Test Setup:","type":"text","marks":[{"type":"strong"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":"php"},"content":[{"text":"public function test_ajax_load_more_posts() {\n // Create test posts\n $post_ids = $this->factory->post->create_many(5);\n\n // Set up AJAX request\n $_POST['action'] = 'load_more_posts';\n $_POST['page'] = 1;\n $_POST['nonce'] = wp_create_nonce('load_more_nonce');\n\n // Set current user (if authentication required)\n wp_set_current_user($this->factory->user->create(['role' => 'subscriber']));\n\n // Capture output\n try {\n $this->_handleAjax('load_more_posts');\n } catch (WPAjaxDieContinueException $e) {\n // Expected exception\n }\n\n // Get response\n $response = json_decode($this->_last_response, true);\n\n $this->assertTrue($response['success']);\n $this->assertCount(5, $response['data']['posts']);\n}","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"Common Testing Patterns","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Testing Custom Post Types","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"php"},"content":[{"text":"class Test_Book_Post_Type extends WP_UnitTestCase {\n\n public function setUp(): void {\n parent::setUp();\n // Ensure CPT is registered\n MyPlugin\\PostTypes::register_book();\n }\n\n public function test_book_post_type_exists() {\n $this->assertTrue(post_type_exists('book'));\n }\n\n public function test_book_supports_features() {\n $post_type = get_post_type_object('book');\n\n $this->assertTrue(post_type_supports('book', 'title'));\n $this->assertTrue(post_type_supports('book', 'editor'));\n $this->assertTrue(post_type_supports('book', 'thumbnail'));\n $this->assertFalse(post_type_supports('book', 'comments'));\n }\n\n public function test_book_has_rest_support() {\n $post_type = get_post_type_object('book');\n $this->assertTrue($post_type->show_in_rest);\n }\n\n public function test_create_book_post() {\n $book_id = $this->factory->post->create([\n 'post_type' => 'book',\n 'post_title' => 'The Great Gatsby',\n ]);\n\n $book = get_post($book_id);\n $this->assertEquals('book', $book->post_type);\n $this->assertEquals('The Great Gatsby', $book->post_title);\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Testing Settings/Options","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"php"},"content":[{"text":"class Test_Plugin_Settings extends WP_UnitTestCase {\n\n public function tearDown(): void {\n delete_option('my_plugin_settings');\n parent::tearDown();\n }\n\n public function test_default_settings_created() {\n $settings = MyPlugin\\Settings::get_defaults();\n\n $this->assertIsArray($settings);\n $this->assertArrayHasKey('api_key', $settings);\n $this->assertEquals('', $settings['api_key']);\n }\n\n public function test_save_settings() {\n $new_settings = [\n 'api_key' => 'test_key_123',\n 'enabled' => true,\n ];\n\n $result = MyPlugin\\Settings::save($new_settings);\n $this->assertTrue($result);\n\n $saved = get_option('my_plugin_settings');\n $this->assertEquals('test_key_123', $saved['api_key']);\n $this->assertTrue($saved['enabled']);\n }\n\n public function test_sanitize_settings() {\n $dirty_input = [\n 'api_key' => '\u003cscript>alert(\"xss\")\u003c/script>',\n 'enabled' => 'yes',\n ];\n\n $clean = MyPlugin\\Settings::sanitize($dirty_input);\n\n $this->assertEquals('alert(\"xss\")', $clean['api_key']);\n $this->assertTrue($clean['enabled']);\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Testing Database Operations","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"php"},"content":[{"text":"class Test_Database_Operations extends WP_UnitTestCase {\n\n protected static $table_name;\n\n public static function setUpBeforeClass(): void {\n parent::setUpBeforeClass();\n\n global $wpdb;\n self::$table_name = $wpdb->prefix . 'plugin_logs';\n\n $charset_collate = $wpdb->get_charset_collate();\n $sql = \"CREATE TABLE \" . self::$table_name . \" (\n id bigint(20) unsigned NOT NULL AUTO_INCREMENT,\n user_id bigint(20) unsigned NOT NULL,\n action varchar(50) NOT NULL,\n created_at datetime DEFAULT CURRENT_TIMESTAMP,\n PRIMARY KEY (id)\n ) $charset_collate;\";\n\n require_once ABSPATH . 'wp-admin/includes/upgrade.php';\n dbDelta($sql);\n }\n\n public static function tearDownAfterClass(): void {\n global $wpdb;\n $wpdb->query(\"DROP TABLE IF EXISTS \" . self::$table_name);\n parent::tearDownAfterClass();\n }\n\n public function test_insert_log_entry() {\n global $wpdb;\n\n $user_id = 1;\n $action = 'user_login';\n\n $result = $wpdb->insert(\n self::$table_name,\n [\n 'user_id' => $user_id,\n 'action' => $action,\n ],\n ['%d', '%s']\n );\n\n $this->assertEquals(1, $result);\n $this->assertGreaterThan(0, $wpdb->insert_id);\n\n // Verify data\n $log = $wpdb->get_row(\n $wpdb->prepare(\n \"SELECT * FROM \" . self::$table_name . \" WHERE id = %d\",\n $wpdb->insert_id\n )\n );\n\n $this->assertEquals($user_id, $log->user_id);\n $this->assertEquals($action, $log->action);\n }\n\n public function test_query_logs_by_user() {\n global $wpdb;\n\n $user_id = 42;\n\n // Insert test data\n $wpdb->insert(self::$table_name, ['user_id' => $user_id, 'action' => 'login'], ['%d', '%s']);\n $wpdb->insert(self::$table_name, ['user_id' => $user_id, 'action' => 'logout'], ['%d', '%s']);\n\n // Query logs\n $logs = $wpdb->get_results(\n $wpdb->prepare(\n \"SELECT * FROM \" . self::$table_name . \" WHERE user_id = %d\",\n $user_id\n )\n );\n\n $this->assertCount(2, $logs);\n }\n}","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Testing REST API Endpoints","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"php"},"content":[{"text":"class Test_REST_API extends WP_UnitTestCase {\n\n protected $server;\n\n public function setUp(): void {\n parent::setUp();\n\n global $wp_rest_server;\n $this->server = $wp_rest_server = new WP_REST_Server();\n do_action('rest_api_init');\n }\n\n public function test_endpoint_registered() {\n $routes = $this->server->get_routes();\n $this->assertArrayHasKey('/myplugin/v1/items', $routes);\n }\n\n public function test_get_items_endpoint() {\n // Create test posts\n $post_ids = $this->factory->post->create_many(3, ['post_type' => 'book']);\n\n $request = new WP_REST_Request('GET', '/myplugin/v1/items');\n $response = $this->server->dispatch($request);\n\n $this->assertEquals(200, $response->get_status());\n\n $data = $response->get_data();\n $this->assertCount(3, $data);\n }\n\n public function test_create_item_requires_authentication() {\n $request = new WP_REST_Request('POST', '/myplugin/v1/items');\n $request->set_body_params([\n 'title' => 'New Item',\n ]);\n\n $response = $this->server->dispatch($request);\n\n $this->assertEquals(401, $response->get_status());\n }\n\n public function test_create_item_with_authentication() {\n $user_id = $this->factory->user->create(['role' => 'editor']);\n wp_set_current_user($user_id);\n\n $request = new WP_REST_Request('POST', '/myplugin/v1/items');\n $request->set_body_params([\n 'title' => 'New Item',\n 'content' => 'Item content',\n ]);\n\n $response = $this->server->dispatch($request);\n\n $this->assertEquals(201, $response->get_status());\n\n $data = $response->get_data();\n $this->assertEquals('New Item', $data['title']);\n }\n}","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"paragraph","content":[{"text":"Related Skills:","type":"text","marks":[{"type":"strong"}]},{"text":" When testing WordPress applications, consider these complementary skills (available in the skill library):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"WordPress Plugin Fundamentals","type":"text","marks":[{"type":"strong"}]},{"text":": Core plugin architecture and hooks - essential foundation for understanding what to test","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"WordPress Security & Validation","type":"text","marks":[{"type":"strong"}]},{"text":": Security patterns and data validation - critical for security testing strategies","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Python pytest Testing","type":"text","marks":[{"type":"strong"}]},{"text":": Modern testing patterns - concepts applicable to WordPress testing approaches","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"GitHub Actions CI/CD","type":"text","marks":[{"type":"strong"}]},{"text":": CI/CD automation - integrate WordPress tests into automated pipelines","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Further Reading:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"WordPress PHPUnit Documentation","type":"text","marks":[{"type":"link","attrs":{"href":"https://make.wordpress.org/core/handbook/testing/automated-testing/writing-phpunit-tests/","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"WP_Mock GitHub Repository","type":"text","marks":[{"type":"link","attrs":{"href":"https://github.com/10up/wp_mock","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"WordPress Coding Standards","type":"text","marks":[{"type":"link","attrs":{"href":"https://developer.wordpress.org/coding-standards/","title":null}}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"PHPUnit Documentation","type":"text","marks":[{"type":"link","attrs":{"href":"https://phpunit.de/documentation.html","title":null}}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}}]},"metadata":{"date":"2026-06-05","name":"wordpress-testing-qa","author":"@skillopedia","source":{"stars":49,"repo_name":"claude-mpm-skills","origin_url":"https://github.com/bobmatnyc/claude-mpm-skills/blob/HEAD/toolchains/php/frameworks/wordpress/testing-qa/SKILL.md","repo_owner":"bobmatnyc","body_sha256":"b450b300160d4fe14ea1accf391acc77273e2093d1464d85d42c50d788c5ccdd","cluster_key":"dde5c3f2e3e2888224736dcfd93be38c55775117c87cfbdea37aba6a464634b3","clean_bundle":{"format":"clean-skill-bundle-v1","source":"bobmatnyc/claude-mpm-skills/toolchains/php/frameworks/wordpress/testing-qa/SKILL.md","attachments":[{"id":"fe2bf506-1c9e-5522-ad46-0e8fec4f95c7","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/fe2bf506-1c9e-5522-ad46-0e8fec4f95c7/attachment.md","path":"README.md","size":5950,"sha256":"11d6d9ae80362aefccca110504fde3d0a312ee174f7493d3be8fce4fd273cb78","contentType":"text/markdown; charset=utf-8"},{"id":"fd3c7d57-9a32-5969-8e7d-824f8f842470","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/fd3c7d57-9a32-5969-8e7d-824f8f842470/attachment.json","path":"metadata.json","size":5545,"sha256":"10bfd01bf8655ca72828e775761f0a75e72ed9db2cff521a58372a03afa723fd","contentType":"application/json; charset=utf-8"}],"bundle_sha256":"68f34d59991f31fc743875e43256c14e7b35868cee82e4ffde068610c2da110e","attachment_count":2,"text_attachments":2,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"toolchains/php/frameworks/wordpress/testing-qa/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"testing-qa","category_label":"Testing"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"testing-qa","import_tag":"clean-skills-v1","description":"WordPress plugin and theme testing with PHPUnit integration tests, WP_Mock unit tests, PHPCS coding standards, and CI/CD workflows","user-invocable":false,"progressive_disclosure":{"entry_point":{"summary":"WordPress plugin and theme testing with PHPUnit integration tests, WP_Mock unit tests, PHPCS coding standards, and CI/CD workflows","quick_start":"1. Review the core concepts below. 2. Apply patterns to your use case. 3. Follow best practices for implementation.","when_to_use":"When writing tests, implementing wordpress-testing-qa, or ensuring code quality."}},"disable-model-invocation":true}},"renderedAt":1782979428012}

WordPress Testing & Quality Assurance --- progressive disclosure: entry point: summary: "WordPress plugin and theme testing with PHPUnit, WP Mock, PHPCS, and CI/CD for quality assurance" when to use: - "Testing WordPress plugins with PHPUnit integration tests" - "Unit testing without loading WordPress core (WP Mock)" - "Enforcing coding standards with PHPCS" quick start: - "Set up PHPUnit with WordPress test suite" - "Write unit tests with WP Mock" - "Configure PHPCS with WPCS ruleset" --- Testing Strategy Testing Pyramid for WordPress The WordPress Testing Hierarchy: Test Distribution Guidel…