Skip to content

Conversation

@binaryfire
Copy link
Contributor

Summary

This PR ports Laravel Orchestral Testbench's testing infrastructure to Hypervel, bringing familiar testing patterns and PHP 8 attribute-based test configuration to Hypervel.

Key Features Ported from Laravel Testbench

  • defineEnvironment() hook - Pre-boot application configuration, matching Laravel's defineEnvironment() pattern
  • Package provider/alias registration - getPackageProviders() and getPackageAliases() methods for package testing
  • Route definition helpers - defineRoutes() and defineWebRoutes() with automatic web middleware group wrapping
  • Database migration hooks - defineDatabaseMigrations(), destroyDatabaseMigrations(), defineDatabaseSeeders()
  • PHP 8 Testing Attributes - Full attribute-based test configuration system

Testing Attributes (Following Laravel Testbench Patterns)

Attribute Description
#[WithConfig('key', 'value')] Set config values directly
#[DefineEnvironment('methodName')] Call a method with $app for environment setup
#[DefineRoute('methodName')] Call a method with $router for route definition
#[DefineDatabase('methodName')] Call a method for database setup (supports deferred execution)
#[WithMigration('path')] Load migration paths
#[RequiresEnv('VAR')] Skip test if environment variable is missing
#[ResetRefreshDatabaseState] Reset database state between test classes
#[Define('group', 'method')] Meta-attribute shorthand (resolves to DefineEnvironment/DefineDatabase/DefineRoute)

Lifecycle Hooks (Matching Laravel Testbench)

  • BeforeAll / AfterAll - Class-level lifecycle (static, runs once per test class)
  • BeforeEach / AfterEach - Test-level lifecycle (runs for each test method)
  • Invokable - Direct invocation with app instance
  • Actionable - Method delegation with closure callback

Hypervel-Specific Adaptations

While following Laravel Testbench patterns closely, this implementation includes Hypervel-specific adaptations:

  • Coroutine context awareness - BeforeEach/AfterEach attributes execute inside runInCoroutine(), matching where setUpTraits() runs in Foundation TestCase
  • Hypervel Router API - Route group middleware uses Hypervel's $router->group() signature
  • Automatic route registration - setUpApplicationRoutes() is called automatically in afterApplicationCreated, so routes defined via defineRoutes() are available without manual setup
  • Smart web routes handling - Uses reflection to skip empty web routes group registration, preventing interference with Hypervel's RouteFileCollector when defineWebRoutes() isn't overridden
  • No route caching - Skipped as Hypervel/Hyperf doesn't have Laravel-style route caching
  • No annotation/pest support - Simplified TestingFeature orchestrator without legacy patterns

Example Usage

#[WithConfig('app.name', 'Test App')]
#[ResetRefreshDatabaseState]
class MyPackageTest extends \Hypervel\Testbench\TestCase
{
    protected function getPackageProviders($app): array
    {
        return [MyServiceProvider::class];
    }

    protected function defineRoutes($router): void
    {
        $router->get('/api/test', fn() => 'ok');
    }

    protected function defineWebRoutes($router): void
    {
        $router->get('/test', fn() => 'ok'); // Has 'web' middleware
    }

    #[DefineEnvironment('setupCustomEnv')]
    #[Define('env', 'setupAnotherEnv')]  // Meta-attribute shorthand
    public function testSomething(): void
    {
        $this->assertEquals('Test App', config('app.name'));
    }

    protected function setupCustomEnv($app): void
    {
        $app->get('config')->set('custom.key', 'value');
    }
}

Files Changed

New Files (28)

Foundation Contracts (src/foundation/src/Testing/Contracts/Attributes/):

  • TestingFeature.php, Resolvable.php, Actionable.php, Invokable.php
  • BeforeEach.php, AfterEach.php, BeforeAll.php, AfterAll.php

Foundation Attributes (src/foundation/src/Testing/Attributes/):

  • DefineEnvironment.php, WithConfig.php, DefineRoute.php, DefineDatabase.php
  • WithMigration.php, RequiresEnv.php, ResetRefreshDatabaseState.php, Define.php

Foundation Infrastructure (src/foundation/src/Testing/):

  • AttributeParser.php - Parses class/method attributes with Resolvable support
  • Concerns/HandlesAttributes.php - Attribute parsing trait
  • Concerns/InteractsWithTestCase.php - Caching and lifecycle execution
  • Features/TestingFeature.php - Orchestrator for default + attribute flows
  • Features/FeaturesCollection.php - Collection for deferred callbacks

Testbench Traits (src/testbench/src/Concerns/):

  • CreatesApplication.php - Package providers/aliases
  • HandlesRoutes.php - Route definition helpers
  • HandlesDatabases.php - Database migration helpers

Tests (7 test files with comprehensive coverage)

Modified Files (3)

  • src/foundation/src/Testing/Concerns/InteractsWithContainer.php - Added defineEnvironment() hook
  • src/testbench/src/TestCase.php - Integrated new traits with coroutine-aware lifecycle
  • tests/Sanctum/AuthenticateRequestsTest.php - Refactored to use new testbench pattern (getPackageProviders, defineEnvironment, defineRoutes)

Phase 1: defineEnvironment hook
- Add defineEnvironment($app) call in refreshApplication() before app boot
- Add empty defineEnvironment() method for subclass override
- Add DefineEnvironmentTest

Phase 2: Attribute contracts
- TestingFeature: marker interface
- Resolvable: resolve() for meta-attributes (does NOT extend TestingFeature)
- Actionable: handle($app, Closure $action)
- Invokable: __invoke($app)
- BeforeEach/AfterEach: per-test lifecycle hooks
- BeforeAll/AfterAll: per-class lifecycle hooks

Phase 3: AttributeParser and FeaturesCollection
- AttributeParser: parses class/method attributes with inheritance and Resolvable support
- FeaturesCollection: collection for deferred attribute callbacks

Phase 4.2: HandlesAttributes trait
- parseTestMethodAttributes() for executing attribute callbacks
- Static caching for class/method attributes
- usesTestingConcern() to check trait usage
- usesTestingFeature() for programmatic attribute registration
- resolvePhpUnitAttributes() merges all attribute sources
- Lifecycle methods: setUpTheTestEnvironmentUsingTestCase,
  tearDownTheTestEnvironmentUsingTestCase,
  setUpBeforeClassUsingTestCase, tearDownAfterClassUsingTestCase
Simplified orchestrator for default + attribute flows.
Uses inline flag-based memoization instead of Orchestra's once() helper.
No annotation/pest support - not needed for Hypervel.
- DefineEnvironment: calls test method with $app
- WithConfig: sets config value directly
- DefineRoute: calls test method with $router
- DefineDatabase: deferred execution, resets RefreshDatabaseState
- ResetRefreshDatabaseState: resets database state before/after all tests
- WithMigration: loads explicit migration paths
- RequiresEnv: skips test if env var missing
- Define: meta-attribute resolving to env/db/route attributes
…rastructure

- Change Actionable::handle() implementations to return mixed instead of void
- Change Invokable::__invoke() implementations to return mixed instead of void
- Fix TestingFeature orchestrator closure return type
- Add @phpstan-ignore for rescue callback type resolution
- Update setUpTheTestEnvironmentUsingTestCase() to execute all attribute types (Invokable, Actionable, BeforeEach)
- Call setUpApplicationRoutes() automatically in afterApplicationCreated
- Add reflection check to skip empty web routes group registration
- Refactor Sanctum tests to use new testbench pattern (getPackageProviders,
  defineEnvironment, defineRoutes)
- Add tests for route accessibility and routing without defineWebRoutes
…ritance

- Mark parseTestMethodAttributes() as @internal to prevent misuse
- Add test verifying Define meta-attribute is resolved by AttributeParser
- Add test verifying Define meta-attribute is executed through lifecycle
- Add tests verifying attributes are inherited from parent TestCase classes
Add proper type hints following existing codebase conventions:
- Use `ApplicationContract` alias for contract type hints
- Use `Router` type hints for route definition methods
- Update all contracts, attributes, traits, and test files

This improves type safety while maintaining consistency with the
existing codebase pattern where 20+ files use the ApplicationContract
alias convention.
@albertcht albertcht requested a review from Copilot January 24, 2026 06:27
@albertcht albertcht added the feature New feature or request label Jan 24, 2026
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Ports Laravel Testbench-style package testing infrastructure into Hypervel, including hooks for environment/routes/database setup and PHP 8 attribute-based test configuration.

Changes:

  • Added a new attribute-based testing feature system (parser, lifecycle execution, attribute contracts, and built-in attributes like WithConfig, DefineEnvironment, etc.).
  • Added Testbench traits for package provider/alias registration, route helpers (including optional web-group wrapping), and database hooks; integrated them into Hypervel\Testbench\TestCase.
  • Added/updated tests to validate the new Testbench patterns and attribute/lifecycle behavior (including refactoring Sanctum test setup).

Reviewed changes

Copilot reviewed 37 out of 37 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/Testbench/TestCaseTest.php Adds coverage that the new Testbench TestCase composes the expected traits and applies attributes/hooks.
tests/Testbench/Concerns/HandlesRoutesWithoutWebRoutesTest.php Verifies API routes work when defineWebRoutes() is not overridden.
tests/Testbench/Concerns/HandlesRoutesTest.php Verifies defineRoutes() / defineWebRoutes() are executed and routes are reachable.
tests/Testbench/Concerns/CreatesApplicationTest.php Verifies package providers/aliases hooks work for package testing.
tests/Sentry/Features/LogFeatureTest.php Updates defineEnvironment() signature to the new typed hook.
tests/Sanctum/AuthenticateRequestsTest.php Refactors test setup to use Testbench hooks (getPackageProviders, defineEnvironment, defineRoutes).
tests/Foundation/Testing/Concerns/InteractsWithTestCaseTest.php Adds tests for trait caching and attribute parsing/execution behavior.
tests/Foundation/Testing/Concerns/HandlesAttributesTest.php Adds coverage for parsing/executing method attributes via the concern.
tests/Foundation/Testing/Concerns/DefineEnvironmentTest.php Verifies defineEnvironment() hook is invoked during setup and can mutate config.
tests/Foundation/Testing/Concerns/AttributeInheritanceTest.php Verifies attribute inheritance from parent test case classes.
tests/Foundation/Testing/Attributes/AttributesTest.php Adds coverage for attribute contracts/targets and attribute behaviors.
src/testbench/src/TestCase.php Integrates new Testbench traits and coroutine-aware lifecycle execution into the base Testbench test case.
src/testbench/src/Concerns/HandlesRoutes.php Adds defineRoutes / defineWebRoutes hooks and automatic application route setup.
src/testbench/src/Concerns/HandlesDatabases.php Adds migration/seeder hook structure and a setUpDatabaseRequirements() helper.
src/testbench/src/Concerns/CreatesApplication.php Adds package provider/alias hook APIs and registration helpers.
src/foundation/src/Testing/Features/TestingFeature.php Adds an orchestrator intended to coordinate default vs attribute-based feature execution.
src/foundation/src/Testing/Features/FeaturesCollection.php Adds a collection type to manage and execute deferred feature callbacks.
src/foundation/src/Testing/Contracts/Attributes/TestingFeature.php Adds a marker interface for test feature attributes.
src/foundation/src/Testing/Contracts/Attributes/Resolvable.php Adds a contract for meta-attributes that resolve into concrete attributes.
src/foundation/src/Testing/Contracts/Attributes/Invokable.php Adds a contract for directly invokable attributes (e.g., WithConfig).
src/foundation/src/Testing/Contracts/Attributes/BeforeEach.php Adds lifecycle contract for “before each test” attributes.
src/foundation/src/Testing/Contracts/Attributes/BeforeAll.php Adds lifecycle contract for “before all tests in class” attributes.
src/foundation/src/Testing/Contracts/Attributes/AfterEach.php Adds lifecycle contract for “after each test” attributes.
src/foundation/src/Testing/Contracts/Attributes/AfterAll.php Adds lifecycle contract for “after all tests in class” attributes.
src/foundation/src/Testing/Contracts/Attributes/Actionable.php Adds contract for attributes that dispatch method execution via a callback.
src/foundation/src/Testing/Concerns/InteractsWithTestCase.php Adds attribute caching plus lifecycle execution (Before/After Each/All) for PHPUnit test cases.
src/foundation/src/Testing/Concerns/InteractsWithContainer.php Adds and invokes the defineEnvironment() hook during application refresh.
src/foundation/src/Testing/Concerns/HandlesAttributes.php Adds an internal API for parsing/executing attributes of a specific type.
src/foundation/src/Testing/Attributes/WithMigration.php Adds attribute to register migration paths via the Migrator.
src/foundation/src/Testing/Attributes/WithConfig.php Adds attribute to set config key/value pairs.
src/foundation/src/Testing/Attributes/ResetRefreshDatabaseState.php Adds attribute to reset RefreshDatabaseState before/after a test class.
src/foundation/src/Testing/Attributes/RequiresEnv.php Adds attribute to skip tests when required environment variables are missing.
src/foundation/src/Testing/Attributes/DefineRoute.php Adds attribute to invoke a route-definition method with the Router.
src/foundation/src/Testing/Attributes/DefineEnvironment.php Adds attribute to invoke an environment-definition method with the app container.
src/foundation/src/Testing/Attributes/DefineDatabase.php Adds attribute to invoke database setup methods with deferred execution support.
src/foundation/src/Testing/Attributes/Define.php Adds meta-attribute resolving shorthand (env/db/route) to concrete attributes.
src/foundation/src/Testing/AttributeParser.php Adds parser to collect (and resolve) testing-related attributes from classes/methods.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +68 to +77
public function testUsesTestingFeatureAddsAttribute(): void
{
// Add a testing feature programmatically
static::usesTestingFeature(new WithConfig('testing.programmatic', 'added'));

// Re-resolve attributes to include the programmatically added one
$attributes = $this->resolvePhpUnitAttributes();

$this->assertTrue($attributes->has(WithConfig::class));
}
Copy link

Copilot AI Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test adds a programmatic testing feature via static::usesTestingFeature(...) without cleaning it up. Because it registers at class scope by default, the added attribute can leak into later tests in the same class and make assertions order-dependent (e.g., counts of WithConfig instances). Use the method-level flag (e.g., Attribute::TARGET_METHOD) for this test, or explicitly reset the static feature state after the assertion.

Copilot uses AI. Check for mistakes.
/**
* Resolve PHPUnit method attributes.
*
* @return \Hypervel\Support\Collection<class-string, array<int, object>>
Copy link

Copilot AI Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The phpdoc for resolvePhpUnitAttributes() says it returns Collection<class-string, array<int, object>>, but the primary implementation in InteractsWithTestCase returns Collection<class-string, Collection<int, object>> (it groups and then pluck()s into collections). Update this docblock (or the return shape) so the contract matches actual usage.

Suggested change
* @return \Hypervel\Support\Collection<class-string, array<int, object>>
* @return \Hypervel\Support\Collection<class-string, \Hypervel\Support\Collection<int, object>>

Copilot uses AI. Check for mistakes.
Comment on lines +186 to +190
->each(fn ($instance) => $instance->handle(
$this->app,
fn ($method, $parameters) => $this->{$method}(...$parameters)
));

Copy link

Copilot AI Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setUpTheTestEnvironmentUsingTestCase() executes Actionable attributes but ignores the return value of handle(). This breaks deferred attributes like DefineDatabase, whose handle() returns a Closure when defer is true—those callbacks are never stored or executed, so the associated setup method will never run. Capture handle() results and either execute returned closures at the intended point in the lifecycle or collect them (e.g., into a FeaturesCollection) so they can be executed later.

Suggested change
->each(fn ($instance) => $instance->handle(
$this->app,
fn ($method, $parameters) => $this->{$method}(...$parameters)
));
->each(function ($instance): void {
$result = $instance->handle(
$this->app,
fn ($method, $parameters) => $this->{$method}(...$parameters)
);
if ($result instanceof \Closure) {
$result();
}
});

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +48
if ($testCase instanceof PHPUnitTestCase) {
/* @phpstan-ignore-next-line */
if ($testCase::usesTestingConcern(HandlesAttributes::class)) {
$result['attribute'] = value($attribute, $defaultResolver);
Copy link

Copilot AI Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TestingFeature::run() unconditionally calls $testCase::usesTestingConcern(...) for any PHPUnitTestCase. If the passed test case doesn't use InteractsWithTestCase (or otherwise define that method), this will fatally error. Also, value($attribute, $defaultResolver) can return null when $attribute is null, which would overwrite $result['attribute'] with a non-FeaturesCollection value. Consider guarding the call with method_exists($testCase, 'usesTestingConcern') (or narrowing the parameter type) and ensuring the assigned value is always a FeaturesCollection (defaulting to an empty collection when $attribute is null).

Suggested change
if ($testCase instanceof PHPUnitTestCase) {
/* @phpstan-ignore-next-line */
if ($testCase::usesTestingConcern(HandlesAttributes::class)) {
$result['attribute'] = value($attribute, $defaultResolver);
if ($testCase instanceof PHPUnitTestCase && method_exists($testCase, 'usesTestingConcern')) {
/* @phpstan-ignore-next-line */
if ($testCase::usesTestingConcern(HandlesAttributes::class)) {
$resolvedFeatures = value($attribute, $defaultResolver);
if ($resolvedFeatures instanceof FeaturesCollection) {
$result['attribute'] = $resolvedFeatures;
}

Copilot uses AI. Check for mistakes.

// Execute BeforeEach attributes INSIDE coroutine context
// (matches where setUpTraits runs in Foundation TestCase)
$this->runInCoroutine(fn () => $this->setUpTheTestEnvironmentUsingTestCase());
Copy link
Member

@albertcht albertcht Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@binaryfire I'm thinking if we need to wrap setUpTheTestEnvironmentUsingTestCase in coroutines. The runInCoroutine for calling setUpTraits in TestCase is to ensure RefreshDatabase can be executed in a coroutine environment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants