-
-
Notifications
You must be signed in to change notification settings - Fork 14
feat: add Laravel Testbench-style testing features #344
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: add Laravel Testbench-style testing features #344
Conversation
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.
There was a problem hiding this 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.
| 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)); | ||
| } |
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
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.
| /** | ||
| * Resolve PHPUnit method attributes. | ||
| * | ||
| * @return \Hypervel\Support\Collection<class-string, array<int, object>> |
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
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.
| * @return \Hypervel\Support\Collection<class-string, array<int, object>> | |
| * @return \Hypervel\Support\Collection<class-string, \Hypervel\Support\Collection<int, object>> |
| ->each(fn ($instance) => $instance->handle( | ||
| $this->app, | ||
| fn ($method, $parameters) => $this->{$method}(...$parameters) | ||
| )); | ||
|
|
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
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.
| ->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(); | |
| } | |
| }); |
| if ($testCase instanceof PHPUnitTestCase) { | ||
| /* @phpstan-ignore-next-line */ | ||
| if ($testCase::usesTestingConcern(HandlesAttributes::class)) { | ||
| $result['attribute'] = value($attribute, $defaultResolver); |
Copilot
AI
Jan 24, 2026
There was a problem hiding this comment.
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).
| 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; | |
| } |
|
|
||
| // Execute BeforeEach attributes INSIDE coroutine context | ||
| // (matches where setUpTraits runs in Foundation TestCase) | ||
| $this->runInCoroutine(fn () => $this->setUpTheTestEnvironmentUsingTestCase()); |
There was a problem hiding this comment.
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.
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'sdefineEnvironment()patterngetPackageProviders()andgetPackageAliases()methods for package testingdefineRoutes()anddefineWebRoutes()with automatic web middleware group wrappingdefineDatabaseMigrations(),destroyDatabaseMigrations(),defineDatabaseSeeders()Testing Attributes (Following Laravel Testbench Patterns)
#[WithConfig('key', 'value')]#[DefineEnvironment('methodName')]$appfor environment setup#[DefineRoute('methodName')]$routerfor route definition#[DefineDatabase('methodName')]#[WithMigration('path')]#[RequiresEnv('VAR')]#[ResetRefreshDatabaseState]#[Define('group', 'method')]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 instanceActionable- Method delegation with closure callbackHypervel-Specific Adaptations
While following Laravel Testbench patterns closely, this implementation includes Hypervel-specific adaptations:
BeforeEach/AfterEachattributes execute insiderunInCoroutine(), matching wheresetUpTraits()runs in Foundation TestCase$router->group()signaturesetUpApplicationRoutes()is called automatically inafterApplicationCreated, so routes defined viadefineRoutes()are available without manual setupRouteFileCollectorwhendefineWebRoutes()isn't overriddenExample Usage
Files Changed
New Files (28)
Foundation Contracts (
src/foundation/src/Testing/Contracts/Attributes/):TestingFeature.php,Resolvable.php,Actionable.php,Invokable.phpBeforeEach.php,AfterEach.php,BeforeAll.php,AfterAll.phpFoundation Attributes (
src/foundation/src/Testing/Attributes/):DefineEnvironment.php,WithConfig.php,DefineRoute.php,DefineDatabase.phpWithMigration.php,RequiresEnv.php,ResetRefreshDatabaseState.php,Define.phpFoundation Infrastructure (
src/foundation/src/Testing/):AttributeParser.php- Parses class/method attributes with Resolvable supportConcerns/HandlesAttributes.php- Attribute parsing traitConcerns/InteractsWithTestCase.php- Caching and lifecycle executionFeatures/TestingFeature.php- Orchestrator for default + attribute flowsFeatures/FeaturesCollection.php- Collection for deferred callbacksTestbench Traits (
src/testbench/src/Concerns/):CreatesApplication.php- Package providers/aliasesHandlesRoutes.php- Route definition helpersHandlesDatabases.php- Database migration helpersTests (7 test files with comprehensive coverage)
Modified Files (3)
src/foundation/src/Testing/Concerns/InteractsWithContainer.php- AddeddefineEnvironment()hooksrc/testbench/src/TestCase.php- Integrated new traits with coroutine-aware lifecycletests/Sanctum/AuthenticateRequestsTest.php- Refactored to use new testbench pattern (getPackageProviders,defineEnvironment,defineRoutes)