Complete testing framework for Forge Kernel. Tiny footprint, powerful features. Test your app, engine, and modules with comprehensive assertions, HTTP testing, database testing, and performance benchmarking.
ForgeTesting provides a complete testing framework for Forge Kernel applications. Built with a tiny footprint (CLI-only), it offers powerful features for testing your application, engine, and modules. No web dependencies, minimal overhead, maximum productivity.
Tiny Footprint: ForgeTesting is a CLI-only module (isCli: true), meaning it only loads in CLI context. No web dependencies, no overhead for production applications. Perfect for development and CI/CD pipelines.
ForgeTesting is built with performance, simplicity, and developer experience in mind. Its tiny footprint and powerful features make it ideal for both small projects and enterprise applications.
ForgeTesting is designed as a CLI-only module:
isCli: true — only loads in CLI contextTests are defined using PHP 8 attributes, not method naming conventions:
#[Test] attribute to mark test methodsTests are organized by scope for better organization:
app/tests/engine/tests/modules/{ModuleName}/src/tests/Directory hash-based caching for performance:
storage/framework/cache/test_cache.phpPerformance: Test caching means test discovery only happens when files change. This makes ForgeTesting very fast even with large test suites.
Install ForgeTesting using ForgePackageManager. The module is CLI-only and has no web dependencies.
# Install with wizard (interactive)
php forge.php package:install-module
# Install directly (skip wizard)
php forge.php package:install-module --module=ForgeTesting
# Install specific version
php forge.php package:install-module --module=ForgeTesting@0.1.4
ForgeTesting is marked as CLI-only:
#[Module(
name: "ForgeTesting",
version: "0.1.4",
description: "A Test Suite Module By Forge",
order: 9999,
isCli: true, // Only loads in CLI context
)]
This means:
ForgeTesting supports three test scopes: app, engine, and module. Run tests by scope for focused testing.
Application-specific tests located in app/tests/:
# Run all app tests
php forge.php test
# Explicitly run app tests
php forge.php test --type=app
Example: app/tests/HomeTest.php
Core engine tests located in engine/tests/:
# Run all engine tests
php forge.php test --type=engine
Examples: engine/tests/ValidatorTest.php, engine/tests/Engine/ContainerEngineTest.php
Module-specific tests located in modules/{ModuleName}/src/tests/:
# Run all module tests
php forge.php test --type=module
# Run tests for specific module
php forge.php test --type=module --module=ForgeAuth
Example: modules/ForgeAuth/src/tests/AuthenticationTest.php
Scoping Benefits: Test scoping allows you to run focused test suites. Test your application logic separately from engine tests, or test specific modules in isolation.
Group tests using the #[Group] attribute and filter by group when running tests.
Apply groups to entire test classes:
#[Group("http")]
final class HomeTest extends TestCase
{
// All tests in this class belong to "http" group
}
Apply groups to individual test methods:
#[Group("auth")]
final class AuthenticationTest extends TestCase
{
#[Test]
#[Group("Database")]
public function user_exists(): void
{
// This test belongs to both "auth" and "Database" groups
}
}
Run only tests in a specific group:
# Run only Database group tests
php forge.php test --group=Database
# Run only http group tests
php forge.php test --group=http
# Run only validator group tests
php forge.php test --group=validator
Examples from the codebase:
http — HTTP/route testsauth — Authentication testsDatabase — Database testsvalidator — Validation testscontainer — Container/Dependency Injection testshelpers — Helper function tests
All tests extend TestCase and use the #[Test] attribute to mark test methods.
use App\Modules\ForgeTesting\Attributes\Test;
use App\Modules\ForgeTesting\TestCase;
final class HomeTest extends TestCase
{
#[Test("Home / route is working")]
public function home_route_is_ok(): void
{
$response = $this->get("/");
$this->assertHttpStatus(200, $response);
}
}
Test methods can have any name — they're identified by the #[Test] attribute:
// ✅ Valid — has #[Test] attribute
#[Test]
public function my_custom_test_name(): void { }
// ✅ Valid — descriptive name
#[Test("User can login")]
public function user_can_login(): void { }
// ❌ Not a test — no #[Test] attribute
public function helper_method(): void { }
Provide optional descriptions in the #[Test] attribute:
#[Test("Home / route is working")]
public function home_route_is_ok(): void { }
// Description appears in test results
// Test: Home / route is working
// Class: App\Tests\HomeTest
// Method: home_route_is_ok
ForgeTesting uses PHP 8 attributes to define test behavior. All attributes are optional except #[Test] for marking test methods.
Required attribute to mark a method as a test:
#[Test]
public function my_test(): void { }
#[Test("Optional test description")]
public function my_test_with_description(): void { }
Group tests for filtering (class or method level):
#[Group("http")]
final class HomeTest extends TestCase { }
#[Group("Database")]
public function user_exists(): void { }
Skip tests with a reason:
#[Test]
#[Skip("Waiting on implementation")]
public function login_works(): void { }
Mark tests as incomplete with a reason:
#[Test]
#[Incomplete("Needs to check save performance in the model")]
public function create_user(): void { }
Provide multiple test data sets:
#[DataProvider("userProvider")]
#[Test]
public function multiple_users(array $users): void
{
$this->assertArrayHasKey("email", $users);
}
public function userProvider(): array
{
return [
[["email" => "user1@example.com"]],
[["email" => "user2@example.com"]],
];
}
Run tests in order based on dependencies:
#[Test]
public function create_user(): void { }
#[Test]
#[Depends("create_user")]
public function update_user(): void
{
// create_user() runs first
}
Run before each test method:
#[BeforeEach]
public function setup(): void
{
// Runs before each test
}
Run after each test method:
#[AfterEach]
public function tearDown(): void
{
// Runs after each test
}
ForgeTesting provides a comprehensive assertions library with 30+ assertion methods covering all testing needs.
$this->assertTrue($value);
$this->assertFalse($value);
$this->assertEquals($expected, $actual);
$this->assertNotEquals($expected, $actual);
$this->assertSame($expected, $actual); // ===
$this->assertNotSame($expected, $actual); // !==
$this->assertNull($value);
$this->assertNotNull($value);
$this->assertEmpty($value);
$this->assertNotEmpty($value);
$this->assertInstanceOf(ExpectedClass::class, $object);
$this->assertNotInstanceOf(ExpectedClass::class, $object);
$this->assertArrayHasKey("key", $array);
$this->assertArrayNotHasKey("key", $array);
$this->assertCount(5, $array);
$this->assertContains($needle, $haystack);
$this->assertNotContains($needle, $haystack);
$this->assertStringContainsString("needle", $haystack);
$this->assertStringNotContainsString("needle", $haystack);
$this->assertMatchesRegularExpression("/pattern/", $string);
$this->assertDoesNotMatchRegularExpression("/pattern/", $string);
$this->assertGreaterThan(10, $value);
$this->assertLessThan(10, $value);
$this->assertGreaterThanOrEqual(10, $value);
$this->assertLessThanOrEqual(10, $value);
$this->assertFileExists($filename);
$this->assertFileDoesNotExist($filename);
$this->assertJsonStringEqualsJsonString($expectedJson, $actualJson);
$this->assertHttpStatus(200, $response);
$this->assertHttpStatus(404, $response);
Expect exceptions to be thrown:
// Expect any exception
$this->shouldFail(function () {
throw new \Exception("Error");
});
// Expect specific exception type
$this->shouldFail(
function () {
throw new ValidationException("Error");
},
ValidationException::class
);
ForgeTesting provides built-in HTTP testing capabilities with CSRF token support.
// GET request
$response = $this->get("/");
// POST request
$response = $this->post("/", ["key" => "value"]);
// PATCH request
$response = $this->patch("/1", ["key" => "value"]);
// With headers
$response = $this->get("/", [], ["X-Custom-Header" => "value"]);
ForgeTesting automatically handles CSRF tokens:
// Add CSRF token to POST data
$response = $this->post("/", $this->withCsrf([
"email" => "test@example.com",
"password" => "password123",
]));
// Add CSRF token to headers
$response = $this->patch(
"/1",
["identifier" => "newuser"],
$this->csrfHeaders()
);
#[Test("Home / route is working")]
public function home_route_is_ok(): void
{
$response = $this->get("/");
$this->assertHttpStatus(200, $response);
}
#[Test("POST / with invalid data redirects back")]
public function register_route_returns_redirect_on_validation_error(): void
{
$response = $this->post(
"/",
$this->withCsrf([
"email" => "invalid-email",
"password" => "123",
]),
);
$this->assertHttpStatus(302, $response);
}
ForgeTesting provides database assertions and migration support for testing database operations.
// Assert record exists
$this->assertDatabaseHas("users", [
"identifier" => "example",
"email" => "test@example.com",
]);
// Assert record does not exist
$this->assertDatabaseMissing("users", [
"email" => "deleted@example.com",
]);
// Assert exact count
$this->assertDatabaseCount("users", 5, [
"status" => "active",
]);
Refresh database before tests:
#[BeforeEach]
public function setup(): void
{
$this->refreshDatabase(); // Runs migrations
}
Seed database with test data:
$this->seed(UserSeeder::class);
#[Test("Check a record exists in the Database by identifier")]
#[Group("Database")]
public function user_exists(): void
{
$this->assertDatabaseHas("users", [
"identifier" => $this->exampleUser["identifier"],
]);
}
ForgeTesting provides performance benchmarking and execution time tracking.
Measure performance with the benchmark() method:
#[Test("Benchmark user lookup")]
#[Group("Database")]
public function benchmark_user_lookup(): array
{
$results = $this->benchmark(function () {
$this->assertDatabaseHas("users", [
"email" => $this->exampleUser["email"],
]);
}, 1); // 1 iteration
return $results; // Returns: ['avg', 'min', 'max', 'total']
}
Benchmark results are displayed in test output:
Benchmark Results: App\Modules\ForgeAuth\Tests\AuthenticationTest::benchmark_user_lookup: | Avg Time/Iter | Min Time/Iter | Max Time/Iter | Total Time | +---------------+---------------+---------------+------------+ | 0.388 ms | 0.388 ms | 0.388 ms | 0.388 ms |
Assert maximum execution time:
$this->assertMaxExecutionTime(0.5, function () {
// Code that should complete in 0.5 seconds
$this->doSomething();
});
ForgeTesting provides utilities for testing cache behavior.
$this->flushCache(); // Clears framework cache
$this->clearLogs(); // Clears log files
ForgeTesting provides lifecycle hooks for test setup and teardown.
The setup() method runs before each test:
#[BeforeEach]
public function setup(): void
{
if (self::$kernel === null) {
$bootstrap = Bootstrap::getInstance();
self::$kernel = $bootstrap->getKernel();
}
}
The tearDown() method runs after each test:
#[AfterEach]
public function tearDown(): void
{
// Cleanup after each test
}
The kernel is initialized once and shared across all tests for performance:
protected static ?Kernel $kernel = null;
#[BeforeEach]
public function setup(): void
{
if (self::$kernel === null) {
$bootstrap = Bootstrap::getInstance();
self::$kernel = $bootstrap->getKernel();
}
}
Use data providers to run the same test with multiple data sets.
#[DataProvider("userProvider")]
#[Test]
#[Group("Database")]
public function multiple_users(array $users): void
{
$this->assertArrayHasKey("email", $users);
}
public function userProvider(): array
{
$users = $this->queryBuilder
->reset()
->setTable("users")
->select("*")
->limit(10)
->get(null);
$dataProvider = [];
foreach ($users as $user) {
$dataProvider[] = [["email" => $user["email"]]];
}
return $dataProvider;
}
The test runs once for each data set returned by the provider method.
Use #[Depends] to ensure tests run in order.
#[Test]
public function create_user(): void
{
// Create user
}
#[Test]
#[Depends("create_user")]
public function update_user(): void
{
// create_user() runs first
// Then update_user() runs
}
Mark tests as skipped or incomplete with reasons. These appear in test results.
#[Test("User login functionality")]
#[Skip("Waiting on implementation")]
public function login_works(): void
{
$this->assertTrue(true);
}
#[Test]
#[Group("smtp")]
#[Skip("Waiting on SMTP implementation")]
public function password_reset_email(): void
{
// Test implementation
}
Skipped tests appear in test results:
Skipped Tests: App\Modules\ForgeAuth\Tests\AuthenticationTest::login_works (0 ms) Reason: Waiting on implementation
#[Test("Insert a new record in the Database")]
#[Group("Database")]
#[Incomplete("Needs to check save performance in the model")]
public function create_user(): void
{
$user = new User();
$user->identifier = $this->exampleUser["identifier"];
$user->save();
$this->assertNotNull($user->id);
}
Incomplete tests appear in test results:
Incomplete Tests: App\Modules\ForgeAuth\Tests\AuthenticationTest::create_user Reason: Needs to check save performance in the model
Run tests using the test command with various options.
# Run all app tests
php forge.php test
# Run app tests explicitly
php forge.php test --type=app
# Run engine tests
php forge.php test --type=engine
# Run all module tests
php forge.php test --type=module
# Run specific module tests
php forge.php test --type=module --module=ForgeAuth
# Run only Database group tests
php forge.php test --group=Database
# Run only http group tests
php forge.php test --group=http
# Run only validator group tests
php forge.php test --group=validator
The test command supports interactive wizard mode for selecting test types and modules.
ForgeTesting uses directory hash-based caching to speed up test discovery.
storage/framework/cache/test_cache.phpreturn [
'meta' => [
'hashes' => [
'/path/to/app/tests/' => 'abc123...',
'/path/to/engine/tests/' => 'def456...',
],
'timestamp' => 1234567890,
],
'classes' => [
'App\Tests\HomeTest',
'Forge\tests\ValidatorTest',
// ... more test classes
],
];
Test caching significantly speeds up test discovery on subsequent runs, especially with large test suites.
ForgeTesting provides comprehensive test result reporting with detailed metrics.
Test Results: | Total | Passed | Failed | Skipped | Incomplete | +-------+--------+--------+---------+------------+ | 4 | 4 | 0 | 0 | 0 |
Top 5 slowest tests are displayed:
Slowest Tests: | Test | Duration | +---------------------------------------------------------------------------------+----------+ | App\Tests\HomeTest::home_route_is_ok | 14.40 ms | | App\Tests\HomeTest::register_route_returns_redirect_on_validation_error | 1.00 ms |
Benchmark Results: App\Modules\ForgeAuth\Tests\AuthenticationTest::benchmark_user_lookup: | Avg Time/Iter | Min Time/Iter | Max Time/Iter | Total Time | +---------------+---------------+---------------+------------+ | 0.388 ms | 0.388 ms | 0.388 ms | 0.388 ms |
Passed Tests: | Test | Class | Method | Duration | +---------------------------------------------------+--------------------+-------------------------------------------------------------+----------+ | Home / route is working | App\Tests\HomeTest | home_route_is_ok | 14.40 ms |
Failed tests show detailed error information including file and line number.
Skipped and incomplete tests are listed with their reasons.
Tests completed in 0.02s
Complete examples from the Forge Kernel codebase.
#[Group("http")]
final class HomeTest extends TestCase
{
#[Test("Home / route is working")]
public function home_route_is_ok(): void
{
$response = $this->get("/");
$this->assertHttpStatus(200, $response);
}
#[Test("POST / with invalid data redirects back")]
public function register_route_returns_redirect_on_validation_error(): void
{
$response = $this->post(
"/",
$this->withCsrf([
"email" => "invalid-email",
"password" => "123",
]),
);
$this->assertHttpStatus(302, $response);
}
}
#[Group("auth")]
final class AuthenticationTest extends TestCase
{
#[Test("Check a record exists in the Database by identifier")]
#[Group("Database")]
public function user_exists(): void
{
$this->assertDatabaseHas("users", [
"identifier" => $this->exampleUser["identifier"],
]);
}
#[Test("Benchmark user lookup")]
#[Group("Database")]
public function benchmark_user_lookup(): array
{
$results = $this->benchmark(function () {
$this->assertDatabaseHas("users", [
"email" => $this->exampleUser["email"],
]);
}, 1);
return $results;
}
}
#[Group('validator')]
final class ValidatorTest extends TestCase
{
#[Test('Validate required rule')]
public function validate_required_rule(): void
{
$isValid = false;
try {
$data = ["identifier" => "juan"];
$rules = ['identifier' => ["required"]];
$this->validate($data, $rules);
$isValid = true;
} catch (ValidationException) {
$isValid = false;
}
$this->assertTrue($isValid);
}
#[Test('Validate validation failing')]
public function validate_should_fail(): void
{
$this->shouldFail(function () {
$data = ["identifier" => ""];
$rules = ['identifier' => ["required"]];
$this->validate($data, $rules);
});
}
}
Recommendations for writing effective tests with ForgeTesting.
Use when:
Use when: