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