Unit Testing

Unit tests verify individual classes work correctly in isolation, without WordPress.

Setup

Bootstrap File

<?php
// tests/Unit/bootstrap.php

declare(strict_types=1);

require_once dirname(__DIR__, 2) . '/vendor/autoload.php';

// Mock WordPress functions for unit tests
if (!function_exists('__')) {
    function __(string $text, string $domain = 'default'): string {
        return $text;
    }
}

if (!function_exists('esc_html')) {
    function esc_html(string $text): string {
        return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
    }
}

if (!defined('ABSPATH')) {
    define('ABSPATH', '/tmp/wordpress/');
}

Test Base Class

<?php
// tests/Unit/TestCase.php

namespace Vendor\MyPlugin\Tests\Unit;

use PHPUnit\Framework\TestCase as BaseTestCase;
use WPZylos\Framework\Core\PluginContext;
use WPZylos\Framework\Container\Container;

abstract class TestCase extends BaseTestCase
{
    protected PluginContext $context;
    protected Container $container;

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

        $this->context = new PluginContext(
            '/path/to/plugin.php',
            'test-plugin',
            'tp_',
            'test-plugin',
            '1.0.0'
        );

        $this->container = new Container();
    }
}

Writing Unit Tests

Service Test

<?php
// tests/Unit/Services/ContactServiceTest.php

namespace Vendor\MyPlugin\Tests\Unit\Services;

use Vendor\MyPlugin\Tests\Unit\TestCase;
use Vendor\MyPlugin\Services\ContactService;
use WPZylos\Framework\Database\Connection;

class ContactServiceTest extends TestCase
{
    private ContactService $service;
    private Connection $mockDb;

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

        $this->mockDb = $this->createMock(Connection::class);
        $this->service = new ContactService($this->mockDb, $this->context);
    }

    public function testCreateReturnsInsertId(): void
    {
        $this->mockDb->expects($this->once())
            ->method('insert')
            ->with(
                $this->stringContains('contacts'),
                ['name' => 'John', 'email' => '[email protected]']
            )
            ->willReturn(123);

        $id = $this->service->create([
            'name' => 'John',
            'email' => '[email protected]',
        ]);

        $this->assertSame(123, $id);
    }

    public function testFindReturnsContact(): void
    {
        // Test implementation
    }
}

Validator Test

<?php
// tests/Unit/Validation/ValidatorTest.php

namespace Vendor\MyPlugin\Tests\Unit\Validation;

use PHPUnit\Framework\TestCase;
use WPZylos\Framework\Validation\Validator;

class ValidatorTest extends TestCase
{
    private Validator $validator;

    protected function setUp(): void
    {
        $this->validator = new Validator();
    }

    public function testRequiredRuleFailsOnEmpty(): void
    {
        $result = $this->validator->validate(
            ['name' => ''],
            ['name' => 'required']
        );

        $this->assertTrue($result->fails());
        $this->assertArrayHasKey('name', $result->errors());
    }

    public function testEmailRuleValidatesFormat(): void
    {
        $result = $this->validator->validate(
            ['email' => 'invalid'],
            ['email' => 'email']
        );

        $this->assertTrue($result->fails());

        $result = $this->validator->validate(
            ['email' => '[email protected]'],
            ['email' => 'email']
        );

        $this->assertTrue($result->passes());
    }

    /**
     * @dataProvider validationRulesProvider
     */
    public function testValidationRules(array $data, array $rules, bool $shouldPass): void
    {
        $result = $this->validator->validate($data, $rules);

        $this->assertSame($shouldPass, $result->passes());
    }

    public static function validationRulesProvider(): array
    {
        return [
            'required passes' => [['name' => 'John'], ['name' => 'required'], true],
            'required fails' => [['name' => ''], ['name' => 'required'], false],
            'max passes' => [['name' => 'John'], ['name' => 'max:10'], true],
            'max fails' => [['name' => 'John Doe Long Name'], ['name' => 'max:10'], false],
        ];
    }
}

Container Test

<?php
// tests/Unit/Container/ContainerTest.php

namespace Vendor\MyPlugin\Tests\Unit\Container;

use PHPUnit\Framework\TestCase;
use WPZylos\Framework\Container\Container;

class ContainerTest extends TestCase
{
    public function testBindAndResolve(): void
    {
        $container = new Container();

        $container->bind('key', fn() => 'value');

        $this->assertSame('value', $container->get('key'));
    }

    public function testSingletonReturnsSameInstance(): void
    {
        $container = new Container();

        $container->singleton('service', fn() => new \stdClass());

        $a = $container->get('service');
        $b = $container->get('service');

        $this->assertSame($a, $b);
    }

    public function testAutoWiresConstructorDependencies(): void
    {
        $container = new Container();
        $container->bind(DependencyInterface::class, ConcreteDependency::class);

        $service = $container->get(ServiceWithDependency::class);

        $this->assertInstanceOf(ServiceWithDependency::class, $service);
    }
}

Mocking WordPress Functions

Function Stubs

// tests/Unit/bootstrap.php

if (!function_exists('sanitize_text_field')) {
    function sanitize_text_field(string $str): string {
        return trim(strip_tags($str));
    }
}

if (!function_exists('wp_create_nonce')) {
    function wp_create_nonce(string $action): string {
        return md5($action . 'salt');
    }
}

if (!function_exists('current_user_can')) {
    function current_user_can(string $capability): bool {
        return true; // Or use a global flag to control this
    }
}

Using Brain\Monkey

For more robust WordPress mocking:

composer require --dev brain/monkey
<?php

use Brain\Monkey;
use Brain\Monkey\Functions;

class MyTest extends TestCase
{
    protected function setUp(): void
    {
        parent::setUp();
        Monkey\setUp();
    }

    protected function tearDown(): void
    {
        Monkey\tearDown();
        parent::tearDown();
    }

    public function testUsesWordPressFunction(): void
    {
        Functions\when('get_option')->justReturn('value');

        $service = new MyService();
        $result = $service->getSetting('key');

        $this->assertSame('value', $result);
    }
}

Running Unit Tests

# Run all unit tests
vendor/bin/phpunit --testsuite=Unit

# Run specific test file
vendor/bin/phpunit tests/Unit/Services/ContactServiceTest.php

# Run specific test method
vendor/bin/phpunit --filter testCreateReturnsInsertId