Testing

This guide covers testing WPZylos Scaffold plugins.

Setup

The scaffold includes PHPUnit for testing. Tests are located in tests/.

Directory Structure

tests/
+-- Unit/           # Unit tests
+-- Feature/        # Feature/integration tests (create as needed)
+-- bootstrap.php   # Test bootstrap

Running Tests

# Run all tests
composer test
# Or directly
./vendor/bin/phpunit

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

# Run specific test method
./vendor/bin/phpunit --filter testMethodName

# Run with coverage (requires Xdebug)
./vendor/bin/phpunit --coverage-html coverage/

Writing Tests

Basic Unit Test

<?php
// tests/Unit/PluginContextTest.php

namespace MyPlugin\Tests\Unit;

use PHPUnit\Framework\TestCase;
use MyPlugin\Core\PluginContext;

class PluginContextTest extends TestCase
{
    private PluginContext $context;

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

        $this->context = PluginContext::create([
            'file'       => '/path/to/plugin.php',
            'slug'       => 'test-plugin',
            'prefix'     => 'testplugin_',
            'textDomain' => 'test-plugin',
            'version'    => '1.0.0',
        ]);
    }

    public function testSlugReturnsCorrectValue(): void
    {
        $this->assertEquals('test-plugin', $this->context->slug());
    }

    public function testPrefixReturnsCorrectValue(): void
    {
        $this->assertEquals('testplugin_', $this->context->prefix());
    }

    public function testHookPrefixesName(): void
    {
        $this->assertEquals('testplugin_init', $this->context->hook('init'));
    }

    public function testOptionKeyPrefixesKey(): void
    {
        $this->assertEquals('testplugin_settings', $this->context->optionKey('settings'));
    }

    public function testMetaKeyPrefixesWithUnderscore(): void
    {
        $this->assertEquals('_testplugin_data', $this->context->metaKey('data'));
    }
}

Testing with Mocks

<?php
// tests/Unit/ItemServiceTest.php

namespace MyPlugin\Tests\Unit;

use PHPUnit\Framework\TestCase;
use MyPlugin\Services\ItemService;
use WPZylos\Framework\Database\Connection;
use WPZylos\Framework\Core\Contracts\ContextInterface;

class ItemServiceTest extends TestCase
{
    public function testAllReturnsArrayOfItems(): void
    {
        // Create mocks
        $db = $this->createMock(Connection::class);
        $context = $this->createMock(ContextInterface::class);

        // Configure mocks
        $context->method('tableName')
            ->with('items')
            ->willReturn('wp_test_items');

        $db->method('getResults')
            ->willReturn([
                (object) ['id' => 1, 'name' => 'Item 1'],
                (object) ['id' => 2, 'name' => 'Item 2'],
            ]);

        // Create service
        $service = new ItemService($db, $context);

        // Assert
        $items = $service->all();
        $this->assertCount(2, $items);
        $this->assertEquals('Item 1', $items[0]->name);
    }

    public function testFindReturnsItem(): void
    {
        $db = $this->createMock(Connection::class);
        $context = $this->createMock(ContextInterface::class);

        $context->method('tableName')->willReturn('wp_test_items');
        $db->method('prepare')->willReturn('SELECT * FROM wp_test_items WHERE id = 1');
        $db->method('getRow')->willReturn((object) ['id' => 1, 'name' => 'Item 1']);

        $service = new ItemService($db, $context);

        $item = $service->find(1);
        $this->assertEquals(1, $item->id);
    }

    public function testFindReturnsNullWhenNotFound(): void
    {
        $db = $this->createMock(Connection::class);
        $context = $this->createMock(ContextInterface::class);

        $context->method('tableName')->willReturn('wp_test_items');
        $db->method('prepare')->willReturn('');
        $db->method('getRow')->willReturn(null);

        $service = new ItemService($db, $context);

        $this->assertNull($service->find(999));
    }
}

Testing Controllers

<?php
// tests/Unit/DashboardControllerTest.php

namespace MyPlugin\Tests\Unit;

use PHPUnit\Framework\TestCase;
use MyPlugin\Http\Controllers\DashboardController;
use WPZylos\Framework\Views\ViewFactory;
use WPZylos\Framework\Core\Contracts\ContextInterface;

class DashboardControllerTest extends TestCase
{
    public function testIndexRendersView(): void
    {
        $view = $this->createMock(ViewFactory::class);
        $context = $this->createMock(ContextInterface::class);

        $context->method('textDomain')->willReturn('test-plugin');

        $view->expects($this->once())
            ->method('render')
            ->with('admin/dashboard', $this->isType('array'))
            ->willReturn('<div>Dashboard</div>');

        $controller = new DashboardController($view, $context);

        ob_start();
        $controller->index();
        $output = ob_get_clean();

        $this->assertStringContainsString('Dashboard', $output);
    }
}

Code Coverage

Generate HTML coverage report:

./vendor/bin/phpunit --coverage-html coverage/

View the report by opening coverage/index.html in a browser.

Continuous Integration

GitHub Actions Example

# .github/workflows/tests.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        php: ["8.0", "8.1", "8.2"]

    steps:
      - uses: actions/checkout@v3

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          extensions: json, mbstring
          coverage: xdebug

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

      - name: Run tests
        run: composer test

Best Practices

  1. Keep tests isolated — Each test should be independent
  2. Use descriptive namestestMethodNameDoesExpectedBehavior
  3. Test edge cases — Empty inputs, nulls, invalid data
  4. Mock external dependencies — Database, APIs, WordPress functions
  5. Aim for high coverage — Target 80%+ code coverage

Resources