ForgeTesting

Overview

ForgeTesting is a comprehensive testing framework for Forge Engine applications. It provides a robust set of tools for unit testing, integration testing, HTTP testing, database testing, and performance benchmarking.

Module Type: Test Suite | Version: 0.1.1 | CLI Command: test

Features

Comprehensive Assertions

Rich set of assertion methods for validating test outcomes

HTTP Testing

Simulate HTTP requests and test responses with CSRF support

Database Testing

Test database operations and verify data integrity

Performance Testing

Benchmark test execution and measure performance

Test Organization

Group tests, skip incomplete tests, and use data providers

CLI Integration

Run tests from command line with filtering and grouping options

Installation

ForgeTesting comes pre-installed with Forge Engine. If you need to install it separately:

# Using Forge Package Manager
forge install forge-testing

# Manual installation
composer require forge/forge-testing

Configuration

ForgeTesting is automatically configured when installed. The module registers itself and provides the test command through the CLI.

Creating Tests

Create test classes by extending the TestCase base class and using test attributes to define test methods.

<?php

declare(strict_types=1);

namespace App\Tests;

use App\Modules\ForgeTesting\Attributes\Test;
use App\Modules\ForgeTesting\TestCase;

class ExampleTest extends TestCase
{
    #[Test('This is a test description')]
    public function test_something(): void
    {
        $this->assertTrue(true);
    }
}

Test Attributes

ForgeTesting provides several attributes to organize and control test execution:

#[Test]

Marks a method as a test method

#[Test('Optional description')]
public function my_test(): void
{
    // Test implementation
}

#[Group]

Organizes tests into groups for selective execution

>#[Group('database')]
#[Test]
public function database_test(): void
{
    // Database-related test
}

#[Skip]

Skips a test with an optional reason

#[Skip('Waiting for implementation')]
#[Test]
public function skipped_test(): void
{
    // This test will be skipped
}

#[Incomplete]

Marks a test as incomplete with an optional reason

#[Incomplete('Needs performance optimization')]
#[Test]
public function incomplete_test(): void
{
    // This test is incomplete
}

#[DataProvider]

Provides data to test methods for parameterized testing

#[DataProvider('userProvider')]
#[Test]
public function test_with_data(array $userData): void
{
    $this->assertArrayHasKey('email', $userData);
}

public function userProvider(): array
{
    return [
        [['email' => 'user1@example.com']],
        [['email' => 'user2@example.com']]
    ];
}

Assertions

ForgeTesting provides a comprehensive set of assertion methods for validating test outcomes:

Basic Assertions

  • assertTrue($condition)
  • assertFalse($condition)
  • assertEquals($expected, $actual)
  • assertNotEquals($expected, $actual)

Type Assertions

  • assertInstanceOf($class, $object)
  • assertNotInstanceOf($class, $object)
  • assertSame($expected, $actual)
  • assertNotSame($expected, $actual)

Array Assertions

  • assertArrayHasKey($key, $array)
  • assertArrayNotHasKey($key, $array)
  • assertCount($expected, $actual)

Database Assertions

  • assertDatabaseHas($table, $data)
  • assertDatabaseMissing($table, $data)
// Example usage
#[Test]
public function assertion_examples(): void
{
    // Basic assertions
    $this->assertTrue(true);
    $this->assertEquals('expected', 'actual');
    
    // Type assertions
    $this->assertInstanceOf(User::class, $user);
    $this->assertSame(123, 123); // Strict comparison
    
    // Array assertions
    $this->assertArrayHasKey('email', $userData);
    $this->assertCount(5, $items);
    
    // Database assertions
    $this->assertDatabaseHas('users', ['email' => 'test@example.com']);
}

HTTP Testing

Test HTTP endpoints and responses with built-in CSRF protection support:

use App\Modules\ForgeTesting\Attributes\Test;
use App\Modules\ForgeTesting\TestCase;

class HttpTest extends TestCase
{
    #[Test]
    public function test_homepage(): void
    {
        $response = $this->get('/');
        $this->assertHttpStatus(200, $response);
    }

    #[Test]
    public function test_form_submission(): void
    {
        $response = $this->post('/login', [
            'email' => 'user@example.com',
            'password' => 'password'
        ]);
        
        $this->assertHttpStatus(302, $response);
    }

    #[Test]
    public function test_with_csrf(): void
    {
        // Load CSRF token
        $this->loadCsrfToken();
        
        $response = $this->withCsrf()->post('/api/data', [
            'data' => 'test'
        ]);
        
        $this->assertHttpStatus(200, $response);
    }
}

Tip: Use withCsrf() to automatically include CSRF tokens in your HTTP requests.

Database Testing

Test database operations and verify data integrity with database-specific assertions:

use App\Modules\ForgeTesting\Attributes\Test;
use App\Modules\ForgeTesting\TestCase;
use Forge\Core\Database\QueryBuilder;

class DatabaseTest extends TestCase
{
    private QueryBuilder $queryBuilder;

    public function __construct()
    {
        $this->queryBuilder = new QueryBuilder();
    }

    #[Test('Create and verify user record')]
    public function create_user(): void
    {
        $user = new User();
        $user->email = 'test@example.com';
        $user->name = 'Test User';
        $user->save();

        $this->assertNotNull($user->id);
        $this->assertDatabaseHas('users', ['email' => 'test@example.com']);
    }

    #[Test('Delete user record')]
    public function delete_user(): void
    {
        $deleted = $this->queryBuilder
            ->reset()
            ->where('email', '=', 'test@example.com')
            ->setTable('users')
            ->delete();

        $this->assertTrue($deleted);
        $this->assertDatabaseMissing('users', ['email' => 'test@example.com']);
    }
}

Performance Testing

Benchmark your code execution and measure performance metrics:

use App\Modules\ForgeTesting\Attributes\Test;
use App\Modules\ForgeTesting\TestCase;

class PerformanceTest extends TestCase
{
    #[Test('Benchmark database query')]
    public function benchmark_user_lookup(): array
    {
        $results = $this->benchmark(function () {
            $this->assertDatabaseHas('users', ['email' => 'test@example.com']);
        }, 10); // Run 10 iterations

        return $results;
    }

    #[Test('Measure string processing performance')]
    public function benchmark_string_operations(): void
    {
        $results = $this->benchmark(function () {
            $string = 'this is a test string';
            $camelCase = Strings::toCamelCase($string);
            $this->assertEquals('thisIsATestString', $camelCase);
        });

        // Results contain execution time, memory usage, etc.
        $this->assertLessThan(0.1, $results['average_time']);
    }
}

Running Tests

Execute tests using the Forge CLI with various filtering options:

# Run all tests
forge test

# Run tests for a specific module
forge test --module=ForgeAuth

# Run tests by group
forge test --group=database

# Run tests by type (app, engine, module)
forge test --type=app
forge test --type=engine
forge test --type=module

# Run tests with verbose output
forge test --verbose

Pro Tip: Use groups to organize related tests and run them selectively during development.

Test Examples

Here are comprehensive examples from different areas of testing:

String Helper Tests

#[Group('helpers')]
final class StringHelperTest extends TestCase
{
    #[Test('String converted to camelCase')]
    public function string_to_camel_case(): void
    {
        $expected = 'thisIsATest';
        $actual = Strings::toCamelCase('this is a test');
        $this->assertEquals($expected, $actual);
    }

    #[Test('String converted to PascalCase')]
    public function string_to_pascal_case(): void
    {
        $expected = 'ThisIsATest';
        $actual = Strings::toPascalCase('this is a test');
        $this->assertEquals($expected, $actual);
    }

    #[Test('String converted to-friendly-url')]
    public function string_to_friendly_url(): void
    {
        $expected = 'this-is-a-test';
        $actual = Strings::slugify('this is a test');
        $this->assertEquals($expected, $actual);
    }
}

Authentication Tests

#[Group('auth')]
final class AuthenticationTest extends TestCase
{
    #[Test('User login functionality')]
    #[Skip('Waiting on implementation')]
    public function login_works(): void
    {
        $this->assertTrue(true);
    }

    #[Test('Check a record exists in the database by identifier')]
    #[Group('database')]
    public function user_exists(): void
    {
        $this->assertDatabaseHas('users', ['identifier' => 'example']);
    }

    #[DataProvider('userProvider')]
    #[Test]
    #[Group('database')]
    public function multiple_users(array $users): void
    {
        $this->assertArrayHasKey('email', $users);
    }

    #[Test('Benchmark user lookup')]
    #[Group('database')]
    public function benchmark_user_lookup(): array
    {
        $results = $this->benchmark(function () {
            $this->assertDatabaseHas('users', ['email' => 'test@example.com']);
        }, 1);

        return $results;
    }
}

Best Practices

Test Organization

  • Use meaningful test method names that describe what is being tested
  • Group related tests using the #[Group] attribute
  • Keep tests focused and test one thing at a time
  • Use descriptive test descriptions in the #[Test] attribute

Test Independence

  • Ensure tests don't depend on each other's execution order
  • Clean up test data in tearDown() methods
  • Use fresh test data for each test run
  • Avoid shared state between tests

Performance Considerations

  • Use #[Skip] for tests that are not ready
  • Mark incomplete tests with #[Incomplete] and provide reasons
  • Benchmark critical code paths to detect performance regressions
  • Use data providers to test multiple scenarios efficiently