wordpress-testing-qa

WordPress Testing & Quality Assurance

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "wordpress-testing-qa" with this command: npx skills add bobmatnyc/claude-mpm-skills/bobmatnyc-claude-mpm-skills-wordpress-testing-qa

WordPress Testing & Quality Assurance

progressive_disclosure: entry_point: summary: "WordPress plugin and theme testing with PHPUnit, WP_Mock, PHPCS, and CI/CD for quality assurance" when_to_use:

  • "Testing WordPress plugins with PHPUnit integration tests"
  • "Unit testing without loading WordPress core (WP_Mock)"
  • "Enforcing coding standards with PHPCS" quick_start:
  • "Set up PHPUnit with WordPress test suite"
  • "Write unit tests with WP_Mock"
  • "Configure PHPCS with WPCS ruleset"

Testing Strategy

Testing Pyramid for WordPress

The WordPress Testing Hierarchy:

   /\
  /  \     E2E Tests (Playwright)
 /    \    - Full user workflows
/------\   - Browser automation

/
/ INTEG \ Integration Tests (PHPUnit + WordPress) / TESTS \ - Database operations / \ - Hook interactions

UNIT TESTS Unit Tests (WP_Mock) - Pure logic - No WordPress dependency

Test Distribution Guidelines:

  • Unit Tests (60%): Fast, isolated, no WordPress

  • Pure PHP functions

  • Class methods with clear inputs/outputs

  • Business logic without side effects

  • Integration Tests (30%): WordPress-loaded tests

  • Database operations

  • Hook/filter interactions

  • Custom post type registration

  • Settings API functionality

  • E2E Tests (10%): Browser automation

  • Critical user workflows

  • Admin panel interactions

  • Frontend form submissions

When to Use PHPUnit vs WP_Mock

Use PHPUnit (Integration Tests) when:

  • ✅ Testing database operations ($wpdb , post creation, meta data)

  • ✅ Testing WordPress hooks (actions/filters actually firing)

  • ✅ Testing template rendering and output

  • ✅ Testing plugin activation/deactivation logic

  • ✅ Testing with actual WordPress functions

Use WP_Mock (Unit Tests) when:

  • ✅ Testing pure business logic

  • ✅ Testing functions that call WordPress functions but logic is independent

  • ✅ Need fast test execution (no database setup)

  • ✅ Testing in isolation without side effects

  • ✅ Mocking external API calls

Test Coverage Goals

Minimum Coverage Requirements:

  • New Code: 80% minimum coverage

  • Critical Paths: 95% coverage (payment processing, authentication, data validation)

  • Legacy Code: Gradual improvement, prioritize high-risk areas

  • Public APIs: 100% coverage for all public methods

What to Test (Priority Order):

  • Security Functions: Nonce verification, sanitization, capability checks

  • Data Operations: Database CRUD, data validation, transformation

  • Business Logic: Calculations, workflows, state transitions

  • Hook Callbacks: Action/filter handlers

  • Public APIs: REST endpoints, WP-CLI commands

What NOT to Test:

  • ❌ WordPress core functions (assume they work)

  • ❌ Third-party library internals

  • ❌ Simple getters/setters with no logic

  • ❌ Configuration files (theme.json, block.json)

PHPUnit Integration Testing

WordPress Test Suite Setup

Step 1: Install Dependencies

Install PHPUnit and WordPress polyfills

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

Generate test scaffold with WP-CLI

wp scaffold plugin-tests my-plugin

This creates:

- tests/bootstrap.php

- tests/test-sample.php

- phpunit.xml.dist

- bin/install-wp-tests.sh

Step 2: Install WordPress Test Library

Install WordPress test suite and test database

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

bash bin/install-wp-tests.sh wordpress_test root '' localhost latest

For specific WordPress version:

bash bin/install-wp-tests.sh wordpress_test root '' localhost 6.7

Step 3: Configure phpunit.xml.dist

<?xml version="1.0"?> <phpunit bootstrap="tests/bootstrap.php" backupGlobals="false" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true" stopOnFailure="false"

&#x3C;testsuites>
    &#x3C;testsuite name="plugin">
        &#x3C;directory prefix="test-" suffix=".php">./tests/&#x3C;/directory>
        &#x3C;exclude>./tests/bootstrap.php&#x3C;/exclude>
    &#x3C;/testsuite>
&#x3C;/testsuites>

&#x3C;coverage includeUncoveredFiles="true">
    &#x3C;include>
        &#x3C;directory suffix=".php">./includes/&#x3C;/directory>
    &#x3C;/include>
    &#x3C;exclude>
        &#x3C;directory>./vendor/&#x3C;/directory>
        &#x3C;directory>./tests/&#x3C;/directory>
    &#x3C;/exclude>
    &#x3C;report>
        &#x3C;html outputDirectory="coverage-html"/>
        &#x3C;text outputFile="php://stdout" showOnlySummary="true"/>
    &#x3C;/report>
&#x3C;/coverage>

&#x3C;php>
    &#x3C;const name="WP_TESTS_PHPUNIT_POLYFILLS_PATH" value="vendor/yoast/phpunit-polyfills"/>
&#x3C;/php>

</phpunit>

WP_UnitTestCase Base Class

tests/bootstrap.php:

<?php /**

  • PHPUnit bootstrap file */

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

// WordPress tests directory $_tests_dir = getenv('WP_TESTS_DIR'); if (!$_tests_dir) { $_tests_dir = rtrim(sys_get_temp_dir(), '/\') . '/wordpress-tests-lib'; }

if (!file_exists("{$_tests_dir}/includes/functions.php")) { throw new Exception("Could not find {$_tests_dir}/includes/functions.php"); }

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

/**

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

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

Factory Objects for Test Data

Using Built-in Factories:

<?php class Test_Plugin_Integration extends WP_UnitTestCase {

/**
 * Test creating posts with factory
 */
public function test_create_post_with_meta() {
    // Create a post using factory
    $post_id = $this->factory->post->create([
        'post_title'   => 'Test Post',
        'post_content' => 'Test content for integration test',
        'post_status'  => 'publish',
        'post_type'    => 'post',
    ]);

    $this->assertIsInt($post_id);
    $this->assertGreaterThan(0, $post_id);

    // Add post meta
    add_post_meta($post_id, '_custom_field', 'custom_value');

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

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

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

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

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

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

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

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

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

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

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

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

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

}

Available Factory Objects:

  • $this->factory->post

  • Posts, pages, custom post types

  • $this->factory->user

  • Users with roles

  • $this->factory->term

  • Terms (categories, tags, custom taxonomies)

  • $this->factory->category

  • Categories specifically

  • $this->factory->tag

  • Tags specifically

  • $this->factory->comment

  • Comments

  • $this->factory->blog

  • Multisite blogs

Database Fixtures and Teardown

setUp() and tearDown() Methods:

<?php class Test_Custom_Post_Type extends WP_UnitTestCase {

protected $post_ids = [];

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

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

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

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

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

    parent::tearDown();
}

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

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

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

}

setUpBeforeClass() and tearDownAfterClass():

<?php class Test_Plugin_Database extends WP_UnitTestCase {

protected static $table_name;

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

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

    // Create custom table
    $charset_collate = $wpdb->get_charset_collate();
    $sql = "CREATE TABLE " . self::$table_name . " (
        id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
        user_id bigint(20) unsigned NOT NULL,
        data_value varchar(255) NOT NULL,
        created_at datetime DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY  (id),
        KEY user_id (user_id)
    ) $charset_collate;";

    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta($sql);
}

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

    parent::tearDownAfterClass();
}

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

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

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

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

}

Complete Plugin Test Example

tests/test-plugin-functionality.php:

<?php /**

  • Test plugin core functionality */ class Test_Plugin_Functionality extends WP_UnitTestCase {

    /**

    • Test plugin registers custom post type */ public function test_custom_post_type_registered() { $this->assertTrue(post_type_exists('book'));

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

    /**

    • Test custom taxonomy registration */ public function test_custom_taxonomy_registered() { $this->assertTrue(taxonomy_exists('genre'));

      $taxonomy = get_taxonomy('genre'); $this->assertTrue($taxonomy->hierarchical); $this->assertContains('book', $taxonomy->object_type); }

    /**

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

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

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

    /**

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

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

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

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

    /**

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

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

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

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

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

    /**

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

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

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

WP_Mock Unit Testing

What is WP_Mock and When to Use It

WP_Mock Purpose:

  • Test PHP code without loading WordPress

  • Mock WordPress functions to return expected values

  • Verify WordPress functions are called with correct arguments

  • Much faster than integration tests (no database setup)

When to Use WP_Mock:

✅ Perfect for:

  • Pure business logic that calls WordPress functions

  • Data transformation/validation functions

  • Service classes with WordPress dependencies

  • Testing in continuous integration (faster CI builds)

❌ NOT Suitable for:

  • Testing actual database operations

  • Testing hook interactions between plugins

  • Testing template rendering

  • Testing functions that rely on WordPress state

Installation and Setup

Install WP_Mock and Mockery

composer require --dev mockery/mockery "^1.6" composer require --dev 10up/wp_mock "^1.0" composer require --dev phpunit/phpunit "^9.6"

tests/bootstrap-wp-mock.php:

<?php /**

  • Bootstrap file for WP_Mock tests */

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

// WP_Mock setup WP_Mock::bootstrap();

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

phpunit-wp-mock.xml.dist:

<?xml version="1.0"?> <phpunit bootstrap="tests/bootstrap-wp-mock.php" backupGlobals="false" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true"

&#x3C;testsuites>
    &#x3C;testsuite name="unit">
        &#x3C;directory prefix="test-" suffix=".php">./tests/unit/&#x3C;/directory>
    &#x3C;/testsuite>
&#x3C;/testsuites>

</phpunit>

Mocking WordPress Functions

tests/unit/test-data-processor.php:

<?php use WP_Mock\Tools\TestCase;

class Test_Data_Processor extends TestCase {

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

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

/**
 * Test sanitization function
 */
public function test_sanitize_input() {
    // Mock sanitize_text_field
    WP_Mock::userFunction('sanitize_text_field', [
        'times' => 1,
        'args' => ['&#x3C;script>alert("xss")&#x3C;/script>'],
        'return' => 'alert("xss")', // WordPress strips tags
    ]);

    $processor = new MyPlugin\DataProcessor();
    $result = $processor->sanitize_input('&#x3C;script>alert("xss")&#x3C;/script>');

    $this->assertEquals('alert("xss")', $result);
}

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

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

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

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

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

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

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

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

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

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

    $this->assertTrue($result);
}

}

Mocking Filters and Actions

Testing add_filter() Calls:

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

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

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

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

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

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

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

    MyPlugin\Hooks::register_actions();

    $this->assertConditionsMet();
}

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

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

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

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

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

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

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

    $this->assertConditionsMet();
}

}

Testing in Isolation (No WordPress Dependency)

Example: Email Service Class:

<?php namespace MyPlugin;

class EmailService {

public function send_notification(string $to, string $message): bool {
    $subject = $this->get_email_subject();
    $headers = $this->get_email_headers();

    return wp_mail($to, $subject, $message, $headers);
}

protected function get_email_subject(): string {
    $site_name = get_bloginfo('name');
    return sprintf('[%s] Notification', $site_name);
}

protected function get_email_headers(): array {
    $admin_email = get_option('admin_email');
    return [
        'From: ' . $admin_email,
        'Content-Type: text/html; charset=UTF-8',
    ];
}

}

Unit Test Without WordPress:

<?php use WP_Mock\Tools\TestCase;

class Test_Email_Service extends TestCase {

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

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

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

    // Mock get_option
    WP_Mock::userFunction('get_option', [
        'args' => 'admin_email',
        'return' => 'admin@example.com',
    ]);

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

    $service = new MyPlugin\EmailService();
    $result = $service->send_notification(
        'user@example.com',
        'Test message content'
    );

    $this->assertTrue($result);
}

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

    WP_Mock::userFunction('get_option', [
        'return' => 'admin@test.com',
    ]);

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

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

    $this->assertFalse($result);
}

}

PHPCS & Coding Standards

Installing PHPCS and WPCS

via Composer (Recommended):

Allow PHPCS composer installer plugin

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

Install WordPress Coding Standards

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

Install PHP Compatibility checker

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

Install PHPCS itself (if not already installed)

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

Verify installation

vendor/bin/phpcs -i

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

.phpcs.xml.dist Configuration

Complete Configuration File:

<?xml version="1.0"?> <ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="WordPress Plugin Coding Standards" xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/squizlabs/PHP_CodeSniffer/master/phpcs.xsd">

&#x3C;description>Custom coding standards for WordPress plugin&#x3C;/description>

&#x3C;!-- What to scan -->
&#x3C;file>./includes&#x3C;/file>
&#x3C;file>./my-plugin.php&#x3C;/file>

&#x3C;!-- Exclude patterns -->
&#x3C;exclude-pattern>*/vendor/*&#x3C;/exclude-pattern>
&#x3C;exclude-pattern>*/node_modules/*&#x3C;/exclude-pattern>
&#x3C;exclude-pattern>*/tests/*&#x3C;/exclude-pattern>
&#x3C;exclude-pattern>*/build/*&#x3C;/exclude-pattern>
&#x3C;exclude-pattern>*/.git/*&#x3C;/exclude-pattern>

&#x3C;!-- Show progress -->
&#x3C;arg value="ps"/>
&#x3C;arg name="colors"/>
&#x3C;arg name="extensions" value="php"/>
&#x3C;arg name="parallel" value="8"/>

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

    &#x3C;!-- Allow multiple assignments in single line -->
    &#x3C;exclude name="Squiz.PHP.DisallowMultipleAssignments"/>

    &#x3C;!-- Relax file comment requirements -->
    &#x3C;exclude name="Squiz.Commenting.FileComment"/>
&#x3C;/rule>

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

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

&#x3C;!-- PHP version compatibility -->
&#x3C;config name="testVersion" value="8.1-"/>
&#x3C;rule ref="PHPCompatibilityWP"/>

&#x3C;!-- Minimum supported WordPress version -->
&#x3C;config name="minimum_wp_version" value="6.4"/>

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

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

&#x3C;!-- Allow WordPress globals to be modified -->
&#x3C;rule ref="WordPress.WP.GlobalVariablesOverride">
    &#x3C;type>error&#x3C;/type>
&#x3C;/rule>

</ruleset>

Running PHPCS and PHPCBF

Command Line Usage:

Check all files

vendor/bin/phpcs

Check specific file

vendor/bin/phpcs includes/Core.php

Show error codes

vendor/bin/phpcs -s

Show only errors (hide warnings)

vendor/bin/phpcs -n

Generate report summary

vendor/bin/phpcs --report=summary

Check single file with detailed output

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

Auto-fix fixable issues

vendor/bin/phpcbf

Auto-fix specific file

vendor/bin/phpcbf includes/Core.php

Dry run (show what would be fixed)

vendor/bin/phpcbf --dry-run

Use specific standard

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

Generate different report formats

vendor/bin/phpcs --report=json > phpcs-report.json vendor/bin/phpcs --report=xml > phpcs-report.xml vendor/bin/phpcs --report=csv > phpcs-report.csv

composer.json Scripts:

{ "scripts": { "phpcs": "phpcs", "phpcbf": "phpcbf", "phpcs:check": "phpcs --report=summary", "phpcs:fix": "phpcbf", "test": [ "@phpcs", "phpunit" ] } }

Pre-commit Hooks

Install pre-commit hook (.git/hooks/pre-commit):

#!/bin/bash

Run PHPCS on changed PHP files

FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '.php$')

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

echo "Running PHPCS on changed files..."

vendor/bin/phpcs $FILES

PHPCS_EXIT=$?

if [ $PHPCS_EXIT -ne 0 ]; then echo "" echo "PHPCS found coding standard violations." echo "Run 'composer phpcbf' to auto-fix issues." echo "" exit 1 fi

echo "PHPCS passed!" exit 0

Make hook executable:

chmod +x .git/hooks/pre-commit

IDE Integration

Visual Studio Code (.vscode/settings.json):

{ "phpcs.enable": true, "phpcs.standard": "WordPress", "phpcs.executablePath": "${workspaceFolder}/vendor/bin/phpcs", "phpcbf.enable": true, "phpcbf.executablePath": "${workspaceFolder}/vendor/bin/phpcbf", "phpcbf.onsave": false, "editor.formatOnSave": false, "[php]": { "editor.defaultFormatter": "bmewburn.vscode-intelephense-client", "editor.formatOnSave": true } }

PHPStorm Configuration:

  • Go to Settings → PHP → Quality Tools → PHP_CodeSniffer

  • Set Configuration path: {PROJECT_ROOT}/vendor/bin/phpcs

  • Go to Settings → Editor → Inspections → PHP → Quality Tools

  • Enable "PHP_CodeSniffer validation"

  • Set Coding standard: "Custom"

  • Set Path: {PROJECT_ROOT}/.phpcs.xml.dist

GitHub Actions CI/CD

Workflow File Structure

.github/workflows/tests.yml:

name: Test Suite

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

jobs:

Job 1: Coding Standards Check

phpcs: name: PHPCS runs-on: ubuntu-latest

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

  - name: Setup PHP
    uses: shivammathur/setup-php@v2
    with:
      php-version: '8.3'
      tools: composer
      coverage: none

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

  - name: Run PHPCS
    run: vendor/bin/phpcs --report=summary

Job 2: PHPUnit Tests with Matrix

phpunit: name: PHPUnit (PHP ${{ matrix.php }}, WP ${{ matrix.wordpress }}) runs-on: ubuntu-latest

strategy:
  fail-fast: false
  matrix:
    php: ['8.1', '8.2', '8.3']
    wordpress: ['6.4', '6.5', '6.6', '6.7', 'latest']
    include:
      - php: '8.3'
        wordpress: 'trunk'

services:
  mysql:
    image: mysql:8.0
    env:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: wordpress_test
    ports:
      - 3306:3306
    options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3

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

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

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

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

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

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

Job 3: WP_Mock Unit Tests

wp-mock: name: WP_Mock Unit Tests runs-on: ubuntu-latest

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

  - name: Setup PHP
    uses: shivammathur/setup-php@v2
    with:
      php-version: '8.3'
      tools: composer
      coverage: none

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

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

Matrix Testing (Multiple PHP/WP Versions)

Strategy Explanation:

strategy: fail-fast: false # Continue testing other versions even if one fails matrix: php: ['8.1', '8.2', '8.3'] # Test PHP versions wordpress: ['6.4', '6.5', '6.6', '6.7', 'latest'] # Test WP versions include: # Add specific combination not in default matrix - php: '8.3' wordpress: 'trunk' # WordPress development version exclude: # Exclude incompatible combinations - php: '8.1' wordpress: 'trunk'

Matrix Results:

  • Creates 18 test jobs (3 PHP × 6 WordPress versions)

  • Ensures compatibility across supported versions

  • Identifies version-specific issues early

PHPCS Checks in CI

Dedicated PHPCS Job:

phpcs-detailed: name: Detailed PHPCS Report runs-on: ubuntu-latest

steps: - uses: actions/checkout@v4

- name: Setup PHP
  uses: shivammathur/setup-php@v2
  with:
    php-version: '8.3'
    tools: composer, cs2pr

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

- name: Run PHPCS with annotations
  run: vendor/bin/phpcs -q --report=checkstyle | cs2pr

- name: Generate PHPCS report
  if: failure()
  run: vendor/bin/phpcs --report=summary --report-file=phpcs-report.txt

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

PHPUnit Test Execution

With Code Coverage:

phpunit-coverage: name: PHPUnit with Coverage runs-on: ubuntu-latest

services: mysql: image: mysql:8.0 env: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: wordpress_test ports: - 3306:3306 options: --health-cmd="mysqladmin ping" --health-interval=10s

steps: - uses: actions/checkout@v4

- name: Setup PHP with Xdebug
  uses: shivammathur/setup-php@v2
  with:
    php-version: '8.3'
    extensions: mysqli, zip, gd
    tools: composer
    coverage: xdebug
    ini-values: xdebug.mode=coverage

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

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

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

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

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

Coverage Reporting

Codecov Integration:

  • name: Upload to Codecov uses: codecov/codecov-action@v4 with: files: ./coverage.xml flags: unittests name: codecov-umbrella fail_ci_if_error: true verbose: true

Coveralls Integration:

  • name: Upload to Coveralls uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} path-to-lcov: ./coverage.xml

Complete Workflow Example

.github/workflows/ci.yml (Production-Ready):

name: CI Pipeline

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

jobs: coding-standards: name: Coding Standards runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: '8.3' tools: composer, cs2pr - run: composer install --prefer-dist --no-progress - run: vendor/bin/phpcs -q --report=checkstyle | cs2pr

unit-tests: name: Unit Tests (WP_Mock) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: '8.3' tools: composer - run: composer install --prefer-dist --no-progress - run: vendor/bin/phpunit -c phpunit-wp-mock.xml.dist --testdox

integration-tests: name: Integration Tests runs-on: ubuntu-latest strategy: matrix: php: ['8.1', '8.3'] wordpress: ['6.5', 'latest'] services: mysql: image: mysql:8.0 env: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: wordpress_test ports: - 3306:3306 options: --health-cmd="mysqladmin ping" --health-interval=10s steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} extensions: mysqli tools: composer coverage: xdebug - run: composer install --prefer-dist --no-progress - run: bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1:3306 ${{ matrix.wordpress }} - run: vendor/bin/phpunit --coverage-clover=coverage.xml - uses: codecov/codecov-action@v4 if: matrix.php == '8.3' && matrix.wordpress == 'latest' with: files: ./coverage.xml

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

Testing Best Practices

Test Naming Conventions

Method Naming Pattern:

test_[method_name][scenario][expected_result]

Examples:

// ✅ GOOD: Descriptive test names public function test_sanitize_email_with_valid_email_returns_email() {} public function test_sanitize_email_with_invalid_email_returns_empty_string() {} public function test_save_post_meta_with_valid_data_returns_true() {} public function test_user_login_with_wrong_password_returns_wp_error() {}

// ❌ BAD: Vague test names public function test_email() {} public function test_function() {} public function test_it_works() {}

Class Naming:

// Pattern: Test_[ClassName] class Test_Email_Service extends WP_UnitTestCase {} class Test_Data_Validator extends WP_Mock\Tools\TestCase {} class Test_Post_Meta_Handler extends WP_UnitTestCase {}

Arrange-Act-Assert Pattern

Structure Every Test:

public function test_calculate_discount() { // ARRANGE: Set up test data and conditions $original_price = 100; $discount_percent = 20; $calculator = new MyPlugin\PriceCalculator();

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

// ASSERT: Verify expected outcome
$this->assertEquals(80, $discounted_price);

}

Complete Example:

public function test_save_user_preferences_updates_database() { // ARRANGE $user_id = $this->factory->user->create(); $preferences = [ 'theme' => 'dark', 'notifications' => true, ]; $service = new MyPlugin\UserPreferences();

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

// ASSERT
$this->assertTrue($result);
$saved_prefs = get_user_meta($user_id, 'preferences', true);
$this->assertEquals('dark', $saved_prefs['theme']);
$this->assertTrue($saved_prefs['notifications']);

}

Data Providers

Purpose: Test same logic with multiple inputs

/**

  • @dataProvider email_validation_provider */ public function test_email_validation($email, $expected) { $validator = new MyPlugin\Validator(); $result = $validator->is_valid_email($email); $this->assertEquals($expected, $result); }

/**

  • Data provider for email validation tests */ public function email_validation_provider(): array { return [ 'valid email' => ['user@example.com', true], 'invalid no at' => ['userexample.com', false], 'invalid no domain' => ['user@', false], 'invalid spaces' => ['user @example.com', false], 'valid subdomain' => ['user@mail.example.com', true], 'invalid special chars' => ['user#@example.com', false], ]; }

Complex Data Provider:

/**

  • @dataProvider discount_calculation_provider */ public function test_discount_calculation($price, $discount, $expected) { $calculator = new MyPlugin\PriceCalculator(); $result = $calculator->apply_discount($price, $discount); $this->assertEquals($expected, $result); }

public function discount_calculation_provider(): array { return [ '20% off 100' => [100, 20, 80], '50% off 100' => [100, 50, 50], '0% off 100' => [100, 0, 100], '100% off 100' => [100, 100, 0], '20% off 0' => [0, 20, 0], ]; }

Testing Hooks and Filters

Testing add_action/add_filter:

public function test_init_hooks_registered() { // Remove all hooks first remove_all_actions('init');

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

// Verify action was added
$this->assertTrue(has_action('init', 'MyPlugin\PostTypes::register'));
$this->assertEquals(10, has_action('init', 'MyPlugin\PostTypes::register'));

}

public function test_content_filter_registered() { remove_all_filters('the_content');

MyPlugin\Hooks::register();

$this->assertTrue(has_filter('the_content', 'MyPlugin\Content::add_reading_time'));

}

Testing Hook Callbacks:

public function test_save_post_hook_saves_meta() { $post_id = $this->factory->post->create([ 'post_type' => 'book', ]);

$_POST['book_isbn'] = '978-3-16-148410-0';
$_POST['book_nonce'] = wp_create_nonce('save_book_meta');

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

// Verify meta was saved
$isbn = get_post_meta($post_id, '_isbn', true);
$this->assertEquals('978-3-16-148410-0', $isbn);

}

Testing AJAX Handlers

AJAX Test Setup:

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

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

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

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

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

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

}

Common Testing Patterns

Testing Custom Post Types

class Test_Book_Post_Type extends WP_UnitTestCase {

public function setUp(): void {
    parent::setUp();
    // Ensure CPT is registered
    MyPlugin\PostTypes::register_book();
}

public function test_book_post_type_exists() {
    $this->assertTrue(post_type_exists('book'));
}

public function test_book_supports_features() {
    $post_type = get_post_type_object('book');

    $this->assertTrue(post_type_supports('book', 'title'));
    $this->assertTrue(post_type_supports('book', 'editor'));
    $this->assertTrue(post_type_supports('book', 'thumbnail'));
    $this->assertFalse(post_type_supports('book', 'comments'));
}

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

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

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

}

Testing Settings/Options

class Test_Plugin_Settings extends WP_UnitTestCase {

public function tearDown(): void {
    delete_option('my_plugin_settings');
    parent::tearDown();
}

public function test_default_settings_created() {
    $settings = MyPlugin\Settings::get_defaults();

    $this->assertIsArray($settings);
    $this->assertArrayHasKey('api_key', $settings);
    $this->assertEquals('', $settings['api_key']);
}

public function test_save_settings() {
    $new_settings = [
        'api_key' => 'test_key_123',
        'enabled' => true,
    ];

    $result = MyPlugin\Settings::save($new_settings);
    $this->assertTrue($result);

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

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

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

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

}

Testing Database Operations

class Test_Database_Operations extends WP_UnitTestCase {

protected static $table_name;

public static function setUpBeforeClass(): void {
    parent::setUpBeforeClass();

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

    $charset_collate = $wpdb->get_charset_collate();
    $sql = "CREATE TABLE " . self::$table_name . " (
        id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
        user_id bigint(20) unsigned NOT NULL,
        action varchar(50) NOT NULL,
        created_at datetime DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY  (id)
    ) $charset_collate;";

    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta($sql);
}

public static function tearDownAfterClass(): void {
    global $wpdb;
    $wpdb->query("DROP TABLE IF EXISTS " . self::$table_name);
    parent::tearDownAfterClass();
}

public function test_insert_log_entry() {
    global $wpdb;

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

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

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

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

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

public function test_query_logs_by_user() {
    global $wpdb;

    $user_id = 42;

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

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

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

}

Testing REST API Endpoints

class Test_REST_API extends WP_UnitTestCase {

protected $server;

public function setUp(): void {
    parent::setUp();

    global $wp_rest_server;
    $this->server = $wp_rest_server = new WP_REST_Server();
    do_action('rest_api_init');
}

public function test_endpoint_registered() {
    $routes = $this->server->get_routes();
    $this->assertArrayHasKey('/myplugin/v1/items', $routes);
}

public function test_get_items_endpoint() {
    // Create test posts
    $post_ids = $this->factory->post->create_many(3, ['post_type' => 'book']);

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

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

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

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

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

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

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

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

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

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

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

}

Related Skills: When testing WordPress applications, consider these complementary skills (available in the skill library):

  • WordPress Plugin Fundamentals: Core plugin architecture and hooks - essential foundation for understanding what to test

  • WordPress Security & Validation: Security patterns and data validation - critical for security testing strategies

  • Python pytest Testing: Modern testing patterns - concepts applicable to WordPress testing approaches

  • GitHub Actions CI/CD: CI/CD automation - integrate WordPress tests into automated pipelines

Further Reading:

  • WordPress PHPUnit Documentation

  • WP_Mock GitHub Repository

  • WordPress Coding Standards

  • PHPUnit Documentation

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

drizzle-orm

No summary provided by upstream source.

Repository SourceNeeds Review
General

pydantic

No summary provided by upstream source.

Repository SourceNeeds Review
General

playwright-e2e-testing

No summary provided by upstream source.

Repository SourceNeeds Review