Advanced

Testing

Write tests for your Glueful application

Write comprehensive tests to ensure your application works correctly.

Quick Start

Glueful uses PHPUnit for testing. The framework includes a lightweight base test case (Glueful\Testing\TestCase) you can extend when you want an application instance + container, but plain PHPUnit\Framework\TestCase works for focused unit tests.

# Run all tests
vendor/bin/phpunit

# Run specific test
vendor/bin/phpunit --filter UserTest

# Run with coverage
vendor/bin/phpunit --coverage-html build/coverage

Test Structure

tests/
├── Unit/           # Unit tests
├── Integration/    # Integration tests
├── Feature/        # Feature tests
└── bootstrap.php   # Test bootstrap

Testing Helpers

  • app() and service() resolve services from the DI container in tests.
  • container() returns the PSR‑11 container (useful for constructing Router).
  • Extend Glueful\Testing\TestCase to bootstrap a minimal application per test class.

Example using the base test case:

use Glueful\Testing\TestCase;

final class UsersRepositoryTest extends TestCase
{
    private \Glueful\Repository\RepositoryFactory $repoFactory;

    protected function setUp(): void
    {
        parent::setUp();
        $this->repoFactory = app(\Glueful\Repository\RepositoryFactory::class);
        // Optional: start transaction
        app('database')->getPDO()->beginTransaction();
    }

    protected function tearDown(): void
    {
        // Optional: rollback
        app('database')->getPDO()->rollBack();
        parent::tearDown();
    }

    public function test_lists_active_users(): void
    {
        $users = $this->repoFactory->users();
        $list = $users->findWhere(['status' => 'active']);
        $this->assertIsArray($list);
    }
}

Unit Tests

Test individual classes in isolation:

namespace Tests\Unit;

use PHPUnit\Framework\TestCase;
use App\Services\CalculatorService;

class CalculatorServiceTest extends TestCase
{
    public function test_adds_numbers()
    {
        $calculator = new CalculatorService();

        $result = $calculator->add(2, 3);

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

    public function test_divides_numbers()
    {
        $calculator = new CalculatorService();

        $result = $calculator->divide(10, 2);

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

    public function test_division_by_zero_throws_exception()
    {
        $this->expectException(\DivisionByZeroError::class);

        $calculator = new CalculatorService();
        $calculator->divide(10, 0);
    }
}

Integration Tests

Test multiple components working together:

namespace Tests\Integration;

use PHPUnit\Framework\TestCase;

class UserRepositoryTest extends TestCase
{
    private \Glueful\Database\Connection $db;
    private \App\Repositories\UserRepository $userRepo;

    protected function setUp(): void
    {
        // Setup test database
        $this->db = app('database');
        $this->userRepo = new UserRepository($this->db);

        // Start transaction
        $this->db->getPDO()->beginTransaction();
    }

    protected function tearDown(): void
    {
        // Rollback after each test
        $this->db->getPDO()->rollBack();
    }

    public function test_creates_user()
    {
        $userData = [
            'name' => 'John Doe',
            'email' => '[email protected]',
            'password' => password_hash('secret', PASSWORD_DEFAULT)
        ];

        $uuid = $this->userRepo->create($userData);
        $user = $this->userRepo->find($uuid); // repositories return arrays

        $this->assertNotNull($user);
        $this->assertEquals('John Doe', $user['name']);
        $this->assertEquals('[email protected]', $user['email']);
    }

    public function test_finds_user_by_email()
    {
        // Create user
        $this->userRepo->create([
            'name' => 'Jane Doe',
            'email' => '[email protected]',
            'password' => password_hash('secret', PASSWORD_DEFAULT)
        ]);

        // Find by email
        $user = $this->userRepo->findByEmail('[email protected]');

        $this->assertNotNull($user);
        $this->assertEquals('Jane Doe', $user['name']);
    }
}

Feature Tests

Test routes end‑to‑end using the Router and Symfony Request/Response:

namespace Tests\Feature;

use PHPUnit\Framework\TestCase;
use Glueful\Routing\Router;
use Symfony\Component\HttpFoundation\Request;

final class AuthenticationTest extends TestCase
{
    public function test_user_can_register(): void
    {
        $router = new Router(container()); // or construct with a minimal PSR‑11 container
        // register routes here or load your app routes

        $request = Request::create('/auth/register', 'POST', [
            'name' => 'John Doe',
            'email' => '[email protected]',
            'password' => 'secret123',
            'password_confirmation' => 'secret123'
        ]);

        $response = $router->dispatch($request);
        $this->assertEquals(201, $response->getStatusCode());
        $body = json_decode((string) $response->getContent(), true);
        $this->assertTrue($body['success'] ?? false);
        $this->assertArrayHasKey('data', $body);
    }
}

HTTP JSON Assertions

use Glueful\Routing\Router;
use Symfony\Component\HttpFoundation\Request;

public function test_returns_json_payload(): void
{
    $router = new Router(container());

    // Minimal route for test
    $router->get('/ping', fn() => \Glueful\Http\Response::success(['pong' => true]));

    $response = $router->dispatch(Request::create('/ping', 'GET'));

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

    $data = json_decode((string) $response->getContent(), true);
    $this->assertTrue($data['success'] ?? false);
    $this->assertTrue(($data['data']['pong'] ?? false));
}

Testing Database

Use Transactions

protected function setUp(): void
{
    $this->db->beginTransaction();
}

protected function tearDown(): void
{
    $this->db->rollback();
}

Use In-Memory SQLite

phpunit.xml:

<php>
    <env name="APP_ENV" value="testing"/>
    <env name="CACHE_DRIVER" value="array"/>
    <env name="DB_CONNECTION" value="sqlite"/>
    <env name="DB_DATABASE" value=":memory:"/>
</php>

Testing Jobs

public function test_send_welcome_email_job(): void
{
    // Jobs accept array data in Glueful
    $job = new \App\Jobs\SendWelcomeEmailJob(['userId' => 123]);

    // Execute synchronously (unit level)
    $job->handle();

    // Assert side effects (e.g., check a mailer mock, outbox, or log)
    $this->assertTrue(true);
}

Testing Events

public function test_user_registered_event_is_dispatched(): void
{
    // Dispatch a framework event directly
    \Glueful\Events\Event::dispatch(new \App\Events\UserRegisteredEvent($userData));

    // Assert side effects triggered by your listener(s)
    $this->assertTrue(true);
}

Mocking

Mock Dependencies

public function test_sends_notification()
{
    $mockMailer = $this->createMock(MailerInterface::class);
    $mockMailer->expects($this->once())
        ->method('send')
        ->with($this->equalTo('[email protected]'));

    $service = new NotificationService($mockMailer);
    $service->sendWelcome('[email protected]');
}

Mock External APIs

public function test_fetches_weather()
{
    $mockClient = $this->createMock(HttpClientInterface::class);
    $mockClient->method('get')
        ->willReturn(['temp' => 72, 'condition' => 'sunny']);

    $weather = new WeatherService($mockClient);
    $result = $weather->getWeather('London');

    $this->assertEquals(72, $result['temp']);
}

Assertions

Common Assertions

// Equality
$this->assertEquals($expected, $actual);
$this->assertNotEquals($expected, $actual);

// Identity
$this->assertSame($expected, $actual);
$this->assertNotSame($expected, $actual);

// Truthiness
$this->assertTrue($value);
$this->assertFalse($value);
$this->assertNull($value);
$this->assertNotNull($value);

// Arrays
$this->assertCount(3, $array);
$this->assertContains('value', $array);
$this->assertArrayHasKey('key', $array);

// Strings
$this->assertStringContainsString('needle', $haystack);
$this->assertStringStartsWith('prefix', $string);
$this->assertStringEndsWith('suffix', $string);

// Exceptions
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Error message');

Custom Assertions

protected function assertValidUser($user)
{
    $this->assertNotNull($user->uuid);
    $this->assertNotEmpty($user->name);
    $this->assertMatchesRegularExpression('/^[\w\.-]+@[\w\.-]+\.\w+$/', $user->email);
}

Data Providers

Test multiple scenarios:

/**
 * @dataProvider validEmailProvider
 */
public function test_validates_email($email)
{
    $validator = new EmailValidator();

    $this->assertTrue($validator->isValid($email));
}

public function validEmailProvider()
{
    return [
        ['[email protected]'],
        ['[email protected]'],
        ['[email protected]'],
    ];
}

/**
 * @dataProvider invalidEmailProvider
 */
public function test_rejects_invalid_email($email)
{
    $validator = new EmailValidator();

    $this->assertFalse($validator->isValid($email));
}

public function invalidEmailProvider()
{
    return [
        ['invalid'],
        ['@example.com'],
        ['user@'],
    ];
}

Test Organization

Group Tests

/**
 * @group slow
 */
public function test_complex_calculation()
{
    // Slow test
}

Run specific group:

vendor/bin/phpunit --group slow
vendor/bin/phpunit --exclude-group slow

Test Suites

phpunit.xml:

<testsuites>
    <testsuite name="Unit">
        <directory>tests/Unit</directory>
    </testsuite>
    <testsuite name="Feature">
        <directory>tests/Feature</directory>
    </testsuite>
</testsuites>

Run specific suite:

vendor/bin/phpunit --testsuite Unit
vendor/bin/phpunit --testsuite Feature

Best Practices

AAA Pattern

public function test_creates_post()
{
    // Arrange
    $data = ['title' => 'Test Post', 'content' => 'Content'];

    // Act
    $post = $this->postRepo->create($data);

    // Assert
    $this->assertEquals('Test Post', $post->title);
}

One Assertion Per Test

// ✅ Good
public function test_post_has_title()
{
    $post = $this->createPost();
    $this->assertEquals('Test', $post->title);
}

public function test_post_has_content()
{
    $post = $this->createPost();
    $this->assertNotEmpty($post->content);
}

// ❌ Bad - multiple concerns
public function test_post()
{
    $post = $this->createPost();
    $this->assertEquals('Test', $post->title);
    $this->assertNotEmpty($post->content);
    $this->assertNotNull($post->uuid);
}

Descriptive Test Names

// ✅ Good
public function test_user_cannot_delete_other_users_posts()

// ❌ Bad
public function test_delete()

Continuous Integration

GitHub Actions

.github/workflows/tests.yml:

name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
          extensions: mbstring, pdo_sqlite

      - name: Install dependencies
        run: composer install

      - name: Run tests
        run: vendor/bin/phpunit

Next Steps