From c797914b57ae9f745e47df4363c2ac3750b8849a Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:29:54 +0000 Subject: [PATCH 01/18] feat(testing): add testbench-style testing infrastructure (phases 1-4.2) 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 --- .../src/Testing/AttributeParser.php | 112 ++++++++++++++++++ .../Testing/Concerns/HandlesAttributes.php | 51 ++++++++ .../Concerns/InteractsWithContainer.php | 13 ++ .../Contracts/Attributes/Actionable.php | 21 ++++ .../Testing/Contracts/Attributes/AfterAll.php | 16 +++ .../Contracts/Attributes/AfterEach.php | 18 +++ .../Contracts/Attributes/BeforeAll.php | 16 +++ .../Contracts/Attributes/BeforeEach.php | 18 +++ .../Contracts/Attributes/Invokable.php | 18 +++ .../Contracts/Attributes/Resolvable.php | 16 +++ .../Contracts/Attributes/TestingFeature.php | 12 ++ .../Testing/Features/FeaturesCollection.php | 26 ++++ .../Concerns/DefineEnvironmentTest.php | 48 ++++++++ 13 files changed, 385 insertions(+) create mode 100644 src/foundation/src/Testing/AttributeParser.php create mode 100644 src/foundation/src/Testing/Concerns/HandlesAttributes.php create mode 100644 src/foundation/src/Testing/Contracts/Attributes/Actionable.php create mode 100644 src/foundation/src/Testing/Contracts/Attributes/AfterAll.php create mode 100644 src/foundation/src/Testing/Contracts/Attributes/AfterEach.php create mode 100644 src/foundation/src/Testing/Contracts/Attributes/BeforeAll.php create mode 100644 src/foundation/src/Testing/Contracts/Attributes/BeforeEach.php create mode 100644 src/foundation/src/Testing/Contracts/Attributes/Invokable.php create mode 100644 src/foundation/src/Testing/Contracts/Attributes/Resolvable.php create mode 100644 src/foundation/src/Testing/Contracts/Attributes/TestingFeature.php create mode 100644 src/foundation/src/Testing/Features/FeaturesCollection.php create mode 100644 tests/Foundation/Testing/Concerns/DefineEnvironmentTest.php diff --git a/src/foundation/src/Testing/AttributeParser.php b/src/foundation/src/Testing/AttributeParser.php new file mode 100644 index 000000000..a33c82832 --- /dev/null +++ b/src/foundation/src/Testing/AttributeParser.php @@ -0,0 +1,112 @@ + + */ + public static function forClass(string $className): array + { + $attributes = []; + $reflection = new ReflectionClass($className); + + foreach ($reflection->getAttributes() as $attribute) { + if (! static::validAttribute($attribute->getName())) { + continue; + } + + [$name, $instance] = static::resolveAttribute($attribute); + + if ($name !== null && $instance !== null) { + $attributes[] = ['key' => $name, 'instance' => $instance]; + } + } + + $parent = $reflection->getParentClass(); + + if ($parent !== false && $parent->isSubclassOf(TestCase::class)) { + $attributes = [...static::forClass($parent->getName()), ...$attributes]; + } + + return $attributes; + } + + /** + * Parse attributes for a method. + * + * @param class-string $className + * @return array + */ + public static function forMethod(string $className, string $methodName): array + { + $attributes = []; + + foreach ((new ReflectionMethod($className, $methodName))->getAttributes() as $attribute) { + if (! static::validAttribute($attribute->getName())) { + continue; + } + + [$name, $instance] = static::resolveAttribute($attribute); + + if ($name !== null && $instance !== null) { + $attributes[] = ['key' => $name, 'instance' => $instance]; + } + } + + return $attributes; + } + + /** + * Validate if a class is a valid testing attribute. + * + * @param class-string|object $class + */ + public static function validAttribute(object|string $class): bool + { + if (\is_string($class) && ! class_exists($class)) { + return false; + } + + $implements = class_implements($class); + + return isset($implements[TestingFeature::class]) + || isset($implements[Resolvable::class]); + } + + /** + * Resolve the given attribute. + * + * @return array{0: class-string|null, 1: object|null} + */ + protected static function resolveAttribute(ReflectionAttribute $attribute): array + { + return rescue(static function () use ($attribute) { + $instance = isset(class_implements($attribute->getName())[Resolvable::class]) + ? transform($attribute->newInstance(), static fn (Resolvable $instance) => $instance->resolve()) + : $attribute->newInstance(); + + if ($instance === null) { + return [null, null]; + } + + return [$instance::class, $instance]; + }, [null, null], false); + } +} diff --git a/src/foundation/src/Testing/Concerns/HandlesAttributes.php b/src/foundation/src/Testing/Concerns/HandlesAttributes.php new file mode 100644 index 000000000..a38b96012 --- /dev/null +++ b/src/foundation/src/Testing/Concerns/HandlesAttributes.php @@ -0,0 +1,51 @@ +resolvePhpUnitAttributes() + ->filter(static fn ($attributes, string $key) => $key === $attribute && ! empty($attributes)) + ->flatten() + ->map(function ($instance) use ($app) { + if ($instance instanceof Invokable) { + return $instance($app); + } + + if ($instance instanceof Actionable) { + return $instance->handle($app, fn ($method, $parameters) => $this->{$method}(...$parameters)); + } + + return null; + }) + ->filter() + ->values(); + + return new FeaturesCollection($attributes); + } + + /** + * Resolve PHPUnit method attributes. + * + * @return \Hypervel\Support\Collection> + */ + abstract protected function resolvePhpUnitAttributes(): Collection; +} diff --git a/src/foundation/src/Testing/Concerns/InteractsWithContainer.php b/src/foundation/src/Testing/Concerns/InteractsWithContainer.php index 79e7c4842..f19284d99 100644 --- a/src/foundation/src/Testing/Concerns/InteractsWithContainer.php +++ b/src/foundation/src/Testing/Concerns/InteractsWithContainer.php @@ -90,9 +90,22 @@ protected function refreshApplication(): void /* @phpstan-ignore-next-line */ $this->app->bind(HttpDispatcher::class, TestingHttpDispatcher::class); $this->app->bind(ConnectionResolverInterface::class, DatabaseConnectionResolver::class); + + $this->defineEnvironment($this->app); + $this->app->get(ApplicationInterface::class); } + /** + * Define environment setup. + * + * @param \Hypervel\Foundation\Contracts\Application $app + */ + protected function defineEnvironment($app): void + { + // Override in subclass. + } + protected function createApplication(): ApplicationContract { return require BASE_PATH . '/bootstrap/app.php'; diff --git a/src/foundation/src/Testing/Contracts/Attributes/Actionable.php b/src/foundation/src/Testing/Contracts/Attributes/Actionable.php new file mode 100644 index 000000000..2c3c9d5eb --- /dev/null +++ b/src/foundation/src/Testing/Contracts/Attributes/Actionable.php @@ -0,0 +1,21 @@ +):void $action + */ + public function handle($app, Closure $action): mixed; +} diff --git a/src/foundation/src/Testing/Contracts/Attributes/AfterAll.php b/src/foundation/src/Testing/Contracts/Attributes/AfterAll.php new file mode 100644 index 000000000..f82834655 --- /dev/null +++ b/src/foundation/src/Testing/Contracts/Attributes/AfterAll.php @@ -0,0 +1,16 @@ +isEmpty()) { + return; + } + + $this->each($callback ?? static fn ($attribute) => value($attribute)); + } +} diff --git a/tests/Foundation/Testing/Concerns/DefineEnvironmentTest.php b/tests/Foundation/Testing/Concerns/DefineEnvironmentTest.php new file mode 100644 index 000000000..410e58121 --- /dev/null +++ b/tests/Foundation/Testing/Concerns/DefineEnvironmentTest.php @@ -0,0 +1,48 @@ +defineEnvironmentCalled = true; + $this->passedApp = $app; + + // Set a config value to verify it takes effect before providers boot + $app->get('config')->set('testing.define_environment_test', 'configured'); + } + + public function testDefineEnvironmentIsCalledDuringSetUp(): void + { + $this->assertTrue($this->defineEnvironmentCalled); + } + + public function testAppInstanceIsPassed(): void + { + $this->assertNotNull($this->passedApp); + $this->assertInstanceOf(Application::class, $this->passedApp); + $this->assertSame($this->app, $this->passedApp); + } + + public function testConfigChangesAreApplied(): void + { + $this->assertSame( + 'configured', + $this->app->get('config')->get('testing.define_environment_test') + ); + } +} From 7e42a9f7d7291f90fcb265af2af9f5f48663211e Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:30:58 +0000 Subject: [PATCH 02/18] feat(testing): add InteractsWithTestCase trait - 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 --- .../Concerns/InteractsWithTestCase.php | 221 ++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 src/foundation/src/Testing/Concerns/InteractsWithTestCase.php diff --git a/src/foundation/src/Testing/Concerns/InteractsWithTestCase.php b/src/foundation/src/Testing/Concerns/InteractsWithTestCase.php new file mode 100644 index 000000000..eaaa6a1e1 --- /dev/null +++ b/src/foundation/src/Testing/Concerns/InteractsWithTestCase.php @@ -0,0 +1,221 @@ +> + */ + protected static array $cachedTestCaseClassAttributes = []; + + /** + * Cached method attributes by "class:method" key. + * + * @var array> + */ + protected static array $cachedTestCaseMethodAttributes = []; + + /** + * Programmatically added class-level testing features. + * + * @var array + */ + protected static array $testCaseTestingFeatures = []; + + /** + * Programmatically added method-level testing features. + * + * @var array + */ + protected static array $testCaseMethodTestingFeatures = []; + + /** + * Cached traits used by test case. + * + * @var array|null + */ + protected static ?array $cachedTestCaseUses = null; + + /** + * Check if the test case uses a specific trait. + * + * @param class-string $trait + */ + public static function usesTestingConcern(string $trait): bool + { + return isset(static::cachedUsesForTestCase()[$trait]); + } + + /** + * Cache and return traits used by test case. + * + * @return array + */ + public static function cachedUsesForTestCase(): array + { + if (static::$cachedTestCaseUses === null) { + /** @var array $uses */ + $uses = array_flip(class_uses_recursive(static::class)); + static::$cachedTestCaseUses = $uses; + } + + return static::$cachedTestCaseUses; + } + + /** + * Programmatically add a testing feature attribute. + * + * @param object $attribute + */ + public static function usesTestingFeature(object $attribute, int $flag = Attribute::TARGET_CLASS): void + { + if (! AttributeParser::validAttribute($attribute)) { + return; + } + + $attribute = $attribute instanceof Resolvable ? $attribute->resolve() : $attribute; + + if ($attribute === null) { + return; + } + + if ($flag & Attribute::TARGET_CLASS) { + static::$testCaseTestingFeatures[] = [ + 'key' => $attribute::class, + 'instance' => $attribute, + ]; + } elseif ($flag & Attribute::TARGET_METHOD) { + static::$testCaseMethodTestingFeatures[] = [ + 'key' => $attribute::class, + 'instance' => $attribute, + ]; + } + } + + /** + * Resolve and cache PHPUnit attributes for current test. + * + * @return \Hypervel\Support\Collection> + */ + protected function resolvePhpUnitAttributes(): Collection + { + $className = static::class; + $methodName = $this->name(); + + // Cache class attributes + if (! isset(static::$cachedTestCaseClassAttributes[$className])) { + static::$cachedTestCaseClassAttributes[$className] = AttributeParser::forClass($className); + } + + // Cache method attributes + $cacheKey = "{$className}:{$methodName}"; + if (! isset(static::$cachedTestCaseMethodAttributes[$cacheKey])) { + static::$cachedTestCaseMethodAttributes[$cacheKey] = AttributeParser::forMethod($className, $methodName); + } + + // Merge all sources and group by attribute class + return (new Collection(array_merge( + static::$testCaseTestingFeatures, + static::$cachedTestCaseClassAttributes[$className], + static::$testCaseMethodTestingFeatures, + static::$cachedTestCaseMethodAttributes[$cacheKey], + )))->groupBy('key') + ->map(static fn ($attrs) => $attrs->pluck('instance')); + } + + /** + * Resolve attributes for class (and optionally method) - used by static lifecycle methods. + * + * @param class-string $className + * @return \Hypervel\Support\Collection> + */ + protected static function resolvePhpUnitAttributesForMethod(string $className, ?string $methodName = null): Collection + { + $attributes = array_merge( + static::$testCaseTestingFeatures, + AttributeParser::forClass($className), + ); + + if ($methodName !== null) { + $attributes = array_merge( + $attributes, + static::$testCaseMethodTestingFeatures, + AttributeParser::forMethod($className, $methodName), + ); + } + + return (new Collection($attributes)) + ->groupBy('key') + ->map(static fn ($attrs) => $attrs->pluck('instance')); + } + + /** + * Execute BeforeEach lifecycle attributes. + */ + protected function setUpTheTestEnvironmentUsingTestCase(): void + { + $this->resolvePhpUnitAttributes() + ->flatten() + ->filter(static fn ($instance) => $instance instanceof BeforeEach) + ->each(fn ($instance) => $instance->beforeEach($this->app)); + } + + /** + * Execute AfterEach lifecycle attributes. + */ + protected function tearDownTheTestEnvironmentUsingTestCase(): void + { + $this->resolvePhpUnitAttributes() + ->flatten() + ->filter(static fn ($instance) => $instance instanceof AfterEach) + ->each(fn ($instance) => $instance->afterEach($this->app)); + + static::$testCaseMethodTestingFeatures = []; + } + + /** + * Execute BeforeAll lifecycle attributes. + */ + public static function setUpBeforeClassUsingTestCase(): void + { + static::resolvePhpUnitAttributesForMethod(static::class) + ->flatten() + ->filter(static fn ($instance) => $instance instanceof BeforeAll) + ->each(static fn ($instance) => $instance->beforeAll()); + } + + /** + * Execute AfterAll lifecycle attributes and clear caches. + */ + public static function tearDownAfterClassUsingTestCase(): void + { + static::resolvePhpUnitAttributesForMethod(static::class) + ->flatten() + ->filter(static fn ($instance) => $instance instanceof AfterAll) + ->each(static fn ($instance) => $instance->afterAll()); + + static::$testCaseTestingFeatures = []; + static::$cachedTestCaseClassAttributes = []; + static::$cachedTestCaseMethodAttributes = []; + static::$cachedTestCaseUses = null; + } +} From 9edebb2a3949c8a29ec9761ec2ab6c6feb48c89a Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:31:54 +0000 Subject: [PATCH 03/18] feat(testing): add TestingFeature orchestrator 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. --- .../src/Testing/Features/TestingFeature.php | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/foundation/src/Testing/Features/TestingFeature.php diff --git a/src/foundation/src/Testing/Features/TestingFeature.php b/src/foundation/src/Testing/Features/TestingFeature.php new file mode 100644 index 000000000..1381e3147 --- /dev/null +++ b/src/foundation/src/Testing/Features/TestingFeature.php @@ -0,0 +1,58 @@ + + */ + public static function run( + object $testCase, + ?Closure $default = null, + ?Closure $attribute = null + ): Fluent { + /** @var \Hypervel\Support\Fluent $result */ + $result = new Fluent(['attribute' => new FeaturesCollection()]); + + // Inline memoization - replaces Orchestra's once() helper + $defaultHasRun = false; + $defaultResolver = static function () use ($default, &$defaultHasRun) { + if ($defaultHasRun || $default === null) { + return; + } + $defaultHasRun = true; + + return $default(); + }; + + if ($testCase instanceof PHPUnitTestCase) { + /** @phpstan-ignore-next-line */ + if ($testCase::usesTestingConcern(HandlesAttributes::class)) { + $result['attribute'] = value($attribute, $defaultResolver); + } + } + + // Safe to call - flag prevents double execution + $defaultResolver(); + + return $result; + } +} From 5a41b4b2168c8f85205c25cb85c84eff603ae81a Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:34:05 +0000 Subject: [PATCH 04/18] feat(testing): add testing attributes - 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 --- .../src/Testing/Attributes/Define.php | 36 ++++++++++ .../src/Testing/Attributes/DefineDatabase.php | 67 +++++++++++++++++++ .../Testing/Attributes/DefineEnvironment.php | 31 +++++++++ .../src/Testing/Attributes/DefineRoute.php | 34 ++++++++++ .../src/Testing/Attributes/RequiresEnv.php | 36 ++++++++++ .../Attributes/ResetRefreshDatabaseState.php | 43 ++++++++++++ .../src/Testing/Attributes/WithConfig.php | 30 +++++++++ .../src/Testing/Attributes/WithMigration.php | 43 ++++++++++++ 8 files changed, 320 insertions(+) create mode 100644 src/foundation/src/Testing/Attributes/Define.php create mode 100644 src/foundation/src/Testing/Attributes/DefineDatabase.php create mode 100644 src/foundation/src/Testing/Attributes/DefineEnvironment.php create mode 100644 src/foundation/src/Testing/Attributes/DefineRoute.php create mode 100644 src/foundation/src/Testing/Attributes/RequiresEnv.php create mode 100644 src/foundation/src/Testing/Attributes/ResetRefreshDatabaseState.php create mode 100644 src/foundation/src/Testing/Attributes/WithConfig.php create mode 100644 src/foundation/src/Testing/Attributes/WithMigration.php diff --git a/src/foundation/src/Testing/Attributes/Define.php b/src/foundation/src/Testing/Attributes/Define.php new file mode 100644 index 000000000..7c9245c46 --- /dev/null +++ b/src/foundation/src/Testing/Attributes/Define.php @@ -0,0 +1,36 @@ +group)) { + 'env' => new DefineEnvironment($this->method), + 'db' => new DefineDatabase($this->method), + 'route' => new DefineRoute($this->method), + default => null, + }; + } +} diff --git a/src/foundation/src/Testing/Attributes/DefineDatabase.php b/src/foundation/src/Testing/Attributes/DefineDatabase.php new file mode 100644 index 000000000..0aa407871 --- /dev/null +++ b/src/foundation/src/Testing/Attributes/DefineDatabase.php @@ -0,0 +1,67 @@ +):void $action + * @return \Closure|null + */ + public function handle($app, Closure $action): ?Closure + { + $resolver = function () use ($app, $action) { + \call_user_func($action, $this->method, [$app]); + }; + + if ($this->defer === false) { + $resolver(); + + return null; + } + + return $resolver; + } +} diff --git a/src/foundation/src/Testing/Attributes/DefineEnvironment.php b/src/foundation/src/Testing/Attributes/DefineEnvironment.php new file mode 100644 index 000000000..052da3b1b --- /dev/null +++ b/src/foundation/src/Testing/Attributes/DefineEnvironment.php @@ -0,0 +1,31 @@ +):void $action + */ + public function handle($app, Closure $action): void + { + \call_user_func($action, $this->method, [$app]); + } +} diff --git a/src/foundation/src/Testing/Attributes/DefineRoute.php b/src/foundation/src/Testing/Attributes/DefineRoute.php new file mode 100644 index 000000000..31f156dff --- /dev/null +++ b/src/foundation/src/Testing/Attributes/DefineRoute.php @@ -0,0 +1,34 @@ +):void $action + */ + public function handle($app, Closure $action): void + { + $router = $app->get(Router::class); + + \call_user_func($action, $this->method, [$router]); + } +} diff --git a/src/foundation/src/Testing/Attributes/RequiresEnv.php b/src/foundation/src/Testing/Attributes/RequiresEnv.php new file mode 100644 index 000000000..a1f4b3cfa --- /dev/null +++ b/src/foundation/src/Testing/Attributes/RequiresEnv.php @@ -0,0 +1,36 @@ +):void $action + */ + public function handle($app, Closure $action): void + { + $message = $this->message ?? "Missing required environment variable `{$this->key}`"; + + if (env($this->key) === null) { + \call_user_func($action, 'markTestSkipped', [$message]); + } + } +} diff --git a/src/foundation/src/Testing/Attributes/ResetRefreshDatabaseState.php b/src/foundation/src/Testing/Attributes/ResetRefreshDatabaseState.php new file mode 100644 index 000000000..957370091 --- /dev/null +++ b/src/foundation/src/Testing/Attributes/ResetRefreshDatabaseState.php @@ -0,0 +1,43 @@ +get('config')->set($this->key, $this->value); + } +} diff --git a/src/foundation/src/Testing/Attributes/WithMigration.php b/src/foundation/src/Testing/Attributes/WithMigration.php new file mode 100644 index 000000000..3b38a592f --- /dev/null +++ b/src/foundation/src/Testing/Attributes/WithMigration.php @@ -0,0 +1,43 @@ + + */ + public readonly array $paths; + + /** + * @param string ...$paths Migration paths to load + */ + public function __construct(string ...$paths) + { + $this->paths = $paths; + } + + /** + * Handle the attribute. + * + * @param \Hypervel\Foundation\Contracts\Application $app + */ + public function __invoke($app): void + { + $app->afterResolving(Migrator::class, function (Migrator $migrator) { + foreach ($this->paths as $path) { + $migrator->path($path); + } + }); + } +} From fbd99ee8c49d8639cd87d8e9c005162e5b2a7d3c Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:38:12 +0000 Subject: [PATCH 05/18] feat(testbench): add package provider, route, and database traits --- .../src/Concerns/CreatesApplication.php | 63 +++++++++++++++++++ .../src/Concerns/HandlesDatabases.php | 56 +++++++++++++++++ src/testbench/src/Concerns/HandlesRoutes.php | 48 ++++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 src/testbench/src/Concerns/CreatesApplication.php create mode 100644 src/testbench/src/Concerns/HandlesDatabases.php create mode 100644 src/testbench/src/Concerns/HandlesRoutes.php diff --git a/src/testbench/src/Concerns/CreatesApplication.php b/src/testbench/src/Concerns/CreatesApplication.php new file mode 100644 index 000000000..3cc34966c --- /dev/null +++ b/src/testbench/src/Concerns/CreatesApplication.php @@ -0,0 +1,63 @@ + + */ + protected function getPackageProviders($app): array + { + return []; + } + + /** + * Get package aliases. + * + * @param \Hypervel\Foundation\Contracts\Application $app + * @return array + */ + protected function getPackageAliases($app): array + { + return []; + } + + /** + * Register package providers. + * + * @param \Hypervel\Foundation\Contracts\Application $app + */ + protected function registerPackageProviders($app): void + { + foreach ($this->getPackageProviders($app) as $provider) { + $app->register($provider); + } + } + + /** + * Register package aliases. + * + * @param \Hypervel\Foundation\Contracts\Application $app + */ + protected function registerPackageAliases($app): void + { + $aliases = $this->getPackageAliases($app); + + if (empty($aliases)) { + return; + } + + $config = $app->get('config'); + $existing = $config->get('app.aliases', []); + $config->set('app.aliases', array_merge($existing, $aliases)); + } +} diff --git a/src/testbench/src/Concerns/HandlesDatabases.php b/src/testbench/src/Concerns/HandlesDatabases.php new file mode 100644 index 000000000..ed6d918e0 --- /dev/null +++ b/src/testbench/src/Concerns/HandlesDatabases.php @@ -0,0 +1,56 @@ +defineDatabaseMigrations(); + $this->beforeApplicationDestroyed(fn () => $this->destroyDatabaseMigrations()); + + $callback(); + + $this->defineDatabaseSeeders(); + } +} diff --git a/src/testbench/src/Concerns/HandlesRoutes.php b/src/testbench/src/Concerns/HandlesRoutes.php new file mode 100644 index 000000000..b084a67ad --- /dev/null +++ b/src/testbench/src/Concerns/HandlesRoutes.php @@ -0,0 +1,48 @@ +get(Router::class); + + $this->defineRoutes($router); + + // Wrap web routes in 'web' middleware group using Hypervel's Router API + $router->group('/', fn () => $this->defineWebRoutes($router), ['middleware' => ['web']]); + } +} From c0b27a56af5feff9879c4fde631e691b1bbc6c26 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:39:40 +0000 Subject: [PATCH 06/18] feat(testbench): integrate traits and attributes into TestCase --- src/testbench/src/TestCase.php | 51 +++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src/testbench/src/TestCase.php b/src/testbench/src/TestCase.php index 8adc46495..d06209ea7 100644 --- a/src/testbench/src/TestCase.php +++ b/src/testbench/src/TestCase.php @@ -12,18 +12,28 @@ use Hypervel\Foundation\Console\Kernel as ConsoleKernel; use Hypervel\Foundation\Contracts\Application as ApplicationContract; use Hypervel\Foundation\Exceptions\Contracts\ExceptionHandler as ExceptionHandlerContract; +use Hypervel\Foundation\Testing\Concerns\HandlesAttributes; +use Hypervel\Foundation\Testing\Concerns\InteractsWithTestCase; use Hypervel\Foundation\Testing\TestCase as BaseTestCase; use Hypervel\Queue\Queue; use Swoole\Timer; use Workbench\App\Exceptions\ExceptionHandler; /** + * Base test case for package testing with testbench features. + * * @internal * @coversNothing */ class TestCase extends BaseTestCase { - protected static $hasBootstrappedTestbench = false; + use Concerns\CreatesApplication; + use Concerns\HandlesDatabases; + use Concerns\HandlesRoutes; + use HandlesAttributes; + use InteractsWithTestCase; + + protected static bool $hasBootstrappedTestbench = false; protected function setUp(): void { @@ -38,6 +48,21 @@ protected function setUp(): void }); parent::setUp(); + + // Execute BeforeEach attributes INSIDE coroutine context + // (matches where setUpTraits runs in Foundation TestCase) + $this->runInCoroutine(fn () => $this->setUpTheTestEnvironmentUsingTestCase()); + } + + /** + * Define environment setup. + * + * @param \Hypervel\Foundation\Contracts\Application $app + */ + protected function defineEnvironment($app): void + { + $this->registerPackageProviders($app); + $this->registerPackageAliases($app); } protected function createApplication(): ApplicationContract @@ -53,8 +78,32 @@ protected function createApplication(): ApplicationContract protected function tearDown(): void { + // Execute AfterEach attributes INSIDE coroutine context + $this->runInCoroutine(fn () => $this->tearDownTheTestEnvironmentUsingTestCase()); + parent::tearDown(); Queue::createPayloadUsing(null); } + + /** + * Reload the application instance. + */ + protected function reloadApplication(): void + { + $this->tearDown(); + $this->setUp(); + } + + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + static::setUpBeforeClassUsingTestCase(); + } + + public static function tearDownAfterClass(): void + { + static::tearDownAfterClassUsingTestCase(); + parent::tearDownAfterClass(); + } } From 7442f0ae716c84104be62c2e2e306f55edba8bab Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:41:05 +0000 Subject: [PATCH 07/18] test(foundation): add AttributesTest for testing attributes --- .../Testing/Attributes/AttributesTest.php | 255 ++++++++++++++++++ 1 file changed, 255 insertions(+) create mode 100644 tests/Foundation/Testing/Attributes/AttributesTest.php diff --git a/tests/Foundation/Testing/Attributes/AttributesTest.php b/tests/Foundation/Testing/Attributes/AttributesTest.php new file mode 100644 index 000000000..1b0ad9105 --- /dev/null +++ b/tests/Foundation/Testing/Attributes/AttributesTest.php @@ -0,0 +1,255 @@ +assertInstanceOf(Actionable::class, $attribute); + $this->assertSame('someMethod', $attribute->method); + } + + public function testDefineEnvironmentCallsMethod(): void + { + $attribute = new DefineEnvironment('testMethod'); + $called = false; + $receivedArgs = null; + + $action = function (string $method, array $params) use (&$called, &$receivedArgs) { + $called = true; + $receivedArgs = [$method, $params]; + }; + + $attribute->handle($this->app, $action); + + $this->assertTrue($called); + $this->assertSame('testMethod', $receivedArgs[0]); + $this->assertSame([$this->app], $receivedArgs[1]); + } + + public function testWithConfigImplementsInvokable(): void + { + $attribute = new WithConfig('app.name', 'TestApp'); + + $this->assertInstanceOf(Invokable::class, $attribute); + $this->assertSame('app.name', $attribute->key); + $this->assertSame('TestApp', $attribute->value); + } + + public function testWithConfigSetsConfigValue(): void + { + $attribute = new WithConfig('testing.attributes.key', 'test_value'); + + $attribute($this->app); + + $this->assertSame('test_value', $this->app->get('config')->get('testing.attributes.key')); + } + + public function testDefineRouteImplementsActionable(): void + { + $attribute = new DefineRoute('defineTestRoutes'); + + $this->assertInstanceOf(Actionable::class, $attribute); + $this->assertSame('defineTestRoutes', $attribute->method); + } + + public function testDefineDatabaseImplementsRequiredInterfaces(): void + { + $attribute = new DefineDatabase('defineMigrations'); + + $this->assertInstanceOf(Actionable::class, $attribute); + $this->assertInstanceOf(BeforeEach::class, $attribute); + $this->assertInstanceOf(AfterEach::class, $attribute); + } + + public function testDefineDatabaseDeferredExecution(): void + { + $attribute = new DefineDatabase('defineMigrations', defer: true); + $called = false; + + $action = function () use (&$called) { + $called = true; + }; + + $result = $attribute->handle($this->app, $action); + + $this->assertFalse($called); + $this->assertInstanceOf(\Closure::class, $result); + + // Execute the deferred callback + $result(); + $this->assertTrue($called); + } + + public function testDefineDatabaseImmediateExecution(): void + { + $attribute = new DefineDatabase('defineMigrations', defer: false); + $called = false; + + $action = function () use (&$called) { + $called = true; + }; + + $result = $attribute->handle($this->app, $action); + + $this->assertTrue($called); + $this->assertNull($result); + } + + public function testResetRefreshDatabaseStateImplementsLifecycleInterfaces(): void + { + $attribute = new ResetRefreshDatabaseState(); + + $this->assertInstanceOf(BeforeAll::class, $attribute); + $this->assertInstanceOf(AfterAll::class, $attribute); + } + + public function testResetRefreshDatabaseStateResetsState(): void + { + // Set some state + RefreshDatabaseState::$migrated = true; + RefreshDatabaseState::$lazilyRefreshed = true; + RefreshDatabaseState::$inMemoryConnections = ['test']; + + ResetRefreshDatabaseState::run(); + + $this->assertFalse(RefreshDatabaseState::$migrated); + $this->assertFalse(RefreshDatabaseState::$lazilyRefreshed); + $this->assertEmpty(RefreshDatabaseState::$inMemoryConnections); + } + + public function testWithMigrationImplementsInvokable(): void + { + $attribute = new WithMigration('/path/to/migrations'); + + $this->assertInstanceOf(Invokable::class, $attribute); + $this->assertSame(['/path/to/migrations'], $attribute->paths); + } + + public function testWithMigrationMultiplePaths(): void + { + $attribute = new WithMigration('/path/one', '/path/two'); + + $this->assertSame(['/path/one', '/path/two'], $attribute->paths); + } + + public function testRequiresEnvImplementsActionable(): void + { + $attribute = new RequiresEnv('SOME_VAR'); + + $this->assertInstanceOf(Actionable::class, $attribute); + $this->assertSame('SOME_VAR', $attribute->key); + } + + public function testDefineImplementsResolvable(): void + { + $attribute = new Define('env', 'setupEnv'); + + $this->assertInstanceOf(Resolvable::class, $attribute); + $this->assertSame('env', $attribute->group); + $this->assertSame('setupEnv', $attribute->method); + } + + public function testDefineResolvesToDefineEnvironment(): void + { + $attribute = new Define('env', 'setupEnv'); + $resolved = $attribute->resolve(); + + $this->assertInstanceOf(DefineEnvironment::class, $resolved); + $this->assertSame('setupEnv', $resolved->method); + } + + public function testDefineResolvesToDefineDatabase(): void + { + $attribute = new Define('db', 'setupDb'); + $resolved = $attribute->resolve(); + + $this->assertInstanceOf(DefineDatabase::class, $resolved); + $this->assertSame('setupDb', $resolved->method); + } + + public function testDefineResolvesToDefineRoute(): void + { + $attribute = new Define('route', 'setupRoutes'); + $resolved = $attribute->resolve(); + + $this->assertInstanceOf(DefineRoute::class, $resolved); + $this->assertSame('setupRoutes', $resolved->method); + } + + public function testDefineReturnsNullForUnknownGroup(): void + { + $attribute = new Define('unknown', 'someMethod'); + $resolved = $attribute->resolve(); + + $this->assertNull($resolved); + } + + public function testDefineGroupIsCaseInsensitive(): void + { + $envUpper = new Define('ENV', 'method'); + $envMixed = new Define('Env', 'method'); + + $this->assertInstanceOf(DefineEnvironment::class, $envUpper->resolve()); + $this->assertInstanceOf(DefineEnvironment::class, $envMixed->resolve()); + } + + public function testAttributesHaveCorrectTargets(): void + { + $this->assertAttributeHasTargets(DefineEnvironment::class, ['TARGET_CLASS', 'TARGET_METHOD', 'IS_REPEATABLE']); + $this->assertAttributeHasTargets(WithConfig::class, ['TARGET_CLASS', 'TARGET_METHOD', 'IS_REPEATABLE']); + $this->assertAttributeHasTargets(DefineRoute::class, ['TARGET_METHOD', 'IS_REPEATABLE']); + $this->assertAttributeHasTargets(DefineDatabase::class, ['TARGET_METHOD', 'IS_REPEATABLE']); + $this->assertAttributeHasTargets(ResetRefreshDatabaseState::class, ['TARGET_CLASS']); + $this->assertAttributeHasTargets(WithMigration::class, ['TARGET_CLASS', 'TARGET_METHOD', 'IS_REPEATABLE']); + $this->assertAttributeHasTargets(RequiresEnv::class, ['TARGET_CLASS', 'TARGET_METHOD', 'IS_REPEATABLE']); + $this->assertAttributeHasTargets(Define::class, ['TARGET_CLASS', 'TARGET_METHOD', 'IS_REPEATABLE']); + } + + private function assertAttributeHasTargets(string $class, array $expectedTargets): void + { + $reflection = new ReflectionClass($class); + $attributes = $reflection->getAttributes(\Attribute::class); + + $this->assertNotEmpty($attributes, "Class {$class} should have #[Attribute]"); + + $attributeInstance = $attributes[0]->newInstance(); + $flags = $attributeInstance->flags; + + foreach ($expectedTargets as $target) { + $constant = constant("Attribute::{$target}"); + $this->assertTrue( + ($flags & $constant) !== 0, + "Class {$class} should have {$target} flag" + ); + } + } +} From d1bd785e3632abd3a674acc8c04b36874f3c4adb Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:41:21 +0000 Subject: [PATCH 08/18] test(foundation): add HandlesAttributesTest --- .../Concerns/HandlesAttributesTest.php | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 tests/Foundation/Testing/Concerns/HandlesAttributesTest.php diff --git a/tests/Foundation/Testing/Concerns/HandlesAttributesTest.php b/tests/Foundation/Testing/Concerns/HandlesAttributesTest.php new file mode 100644 index 000000000..a13d21588 --- /dev/null +++ b/tests/Foundation/Testing/Concerns/HandlesAttributesTest.php @@ -0,0 +1,68 @@ +get('config')->set('testing.method_env', 'method_value'); + } + + public function testParseTestMethodAttributesReturnsCollection(): void + { + $result = $this->parseTestMethodAttributes($this->app, WithConfig::class); + + $this->assertInstanceOf(FeaturesCollection::class, $result); + } + + #[WithConfig('testing.method_attribute', 'test_value')] + public function testParseTestMethodAttributesHandlesInvokable(): void + { + // Parse WithConfig attribute which is Invokable + $this->parseTestMethodAttributes($this->app, WithConfig::class); + + // The attribute should have set the config value + $this->assertSame('test_value', $this->app->get('config')->get('testing.method_attribute')); + } + + #[DefineEnvironment('defineConfigEnv')] + public function testParseTestMethodAttributesHandlesActionable(): void + { + // Parse DefineEnvironment attribute which is Actionable + $this->parseTestMethodAttributes($this->app, DefineEnvironment::class); + + // The attribute should have called the method which set the config value + $this->assertSame('method_value', $this->app->get('config')->get('testing.method_env')); + } + + public function testParseTestMethodAttributesReturnsEmptyCollectionForNoMatch(): void + { + $result = $this->parseTestMethodAttributes($this->app, DefineEnvironment::class); + + $this->assertInstanceOf(FeaturesCollection::class, $result); + $this->assertTrue($result->isEmpty()); + } + + #[WithConfig('testing.multi_one', 'one')] + #[WithConfig('testing.multi_two', 'two')] + public function testParseTestMethodAttributesHandlesMultipleAttributes(): void + { + $this->parseTestMethodAttributes($this->app, WithConfig::class); + + $this->assertSame('one', $this->app->get('config')->get('testing.multi_one')); + $this->assertSame('two', $this->app->get('config')->get('testing.multi_two')); + } +} From 87b8d6d09f1f12dba3ddf231c19346334c62c638 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:41:38 +0000 Subject: [PATCH 09/18] test(foundation): add InteractsWithTestCaseTest --- .../Concerns/InteractsWithTestCaseTest.php | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 tests/Foundation/Testing/Concerns/InteractsWithTestCaseTest.php diff --git a/tests/Foundation/Testing/Concerns/InteractsWithTestCaseTest.php b/tests/Foundation/Testing/Concerns/InteractsWithTestCaseTest.php new file mode 100644 index 000000000..be9cee0e8 --- /dev/null +++ b/tests/Foundation/Testing/Concerns/InteractsWithTestCaseTest.php @@ -0,0 +1,75 @@ +assertTrue(static::usesTestingConcern(HandlesAttributes::class)); + $this->assertTrue(static::usesTestingConcern(InteractsWithTestCase::class)); + } + + public function testUsesTestingConcernReturnsFalseForUnusedTrait(): void + { + $this->assertFalse(static::usesTestingConcern('NonExistentTrait')); + } + + public function testCachedUsesForTestCaseReturnsTraits(): void + { + $uses = static::cachedUsesForTestCase(); + + $this->assertIsArray($uses); + $this->assertArrayHasKey(HandlesAttributes::class, $uses); + $this->assertArrayHasKey(InteractsWithTestCase::class, $uses); + } + + public function testResolvePhpUnitAttributesReturnsCollection(): void + { + $attributes = $this->resolvePhpUnitAttributes(); + + $this->assertInstanceOf(Collection::class, $attributes); + } + + #[WithConfig('testing.method_level', 'method_value')] + public function testResolvePhpUnitAttributesMergesClassAndMethodAttributes(): void + { + $attributes = $this->resolvePhpUnitAttributes(); + + // Should have WithConfig from both class and method level + $this->assertTrue($attributes->has(WithConfig::class)); + + $withConfigInstances = $attributes->get(WithConfig::class); + $this->assertCount(2, $withConfigInstances); + } + + public function testClassLevelAttributeIsApplied(): void + { + // The WithConfig attribute at class level should be applied + $this->assertSame('class_value', $this->app->get('config')->get('testing.class_level')); + } + + 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)); + } +} From 89be1bb0924cd2a1489663c207c034729cfb0756 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:41:54 +0000 Subject: [PATCH 10/18] test(testbench): add CreatesApplicationTest --- .../Concerns/CreatesApplicationTest.php | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 tests/Testbench/Concerns/CreatesApplicationTest.php diff --git a/tests/Testbench/Concerns/CreatesApplicationTest.php b/tests/Testbench/Concerns/CreatesApplicationTest.php new file mode 100644 index 000000000..d95b0b3f7 --- /dev/null +++ b/tests/Testbench/Concerns/CreatesApplicationTest.php @@ -0,0 +1,82 @@ + TestFacade::class, + ]; + } + + public function testGetPackageProvidersReturnsProviders(): void + { + $providers = $this->getPackageProviders($this->app); + + $this->assertContains(TestServiceProvider::class, $providers); + } + + public function testGetPackageAliasesReturnsAliases(): void + { + $aliases = $this->getPackageAliases($this->app); + + $this->assertArrayHasKey('TestAlias', $aliases); + $this->assertSame(TestFacade::class, $aliases['TestAlias']); + } + + public function testRegisterPackageProvidersRegistersProviders(): void + { + // The provider should be registered via defineEnvironment + // which calls registerPackageProviders + $this->assertTrue( + $this->app->providerIsLoaded(TestServiceProvider::class), + 'TestServiceProvider should be registered' + ); + } + + public function testRegisterPackageAliasesAddsToConfig(): void + { + $aliases = $this->app->get('config')->get('app.aliases', []); + + $this->assertArrayHasKey('TestAlias', $aliases); + $this->assertSame(TestFacade::class, $aliases['TestAlias']); + } +} + +/** + * Test service provider for testing. + */ +class TestServiceProvider extends \Hypervel\Support\ServiceProvider +{ + public function register(): void + { + $this->app->bind('test.service', fn () => 'test_value'); + } +} + +/** + * Test facade for testing. + */ +class TestFacade +{ + // Empty facade class for testing +} From 8c975e52fbc2ca3ac4f567a4293faa68617000fd Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:42:08 +0000 Subject: [PATCH 11/18] test(testbench): add HandlesRoutesTest --- .../Testbench/Concerns/HandlesRoutesTest.php | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 tests/Testbench/Concerns/HandlesRoutesTest.php diff --git a/tests/Testbench/Concerns/HandlesRoutesTest.php b/tests/Testbench/Concerns/HandlesRoutesTest.php new file mode 100644 index 000000000..193bbb5b5 --- /dev/null +++ b/tests/Testbench/Concerns/HandlesRoutesTest.php @@ -0,0 +1,75 @@ +defineRoutesCalled = true; + + $router->get('/api/test', fn () => 'api_response'); + } + + protected function defineWebRoutes($router): void + { + $this->defineWebRoutesCalled = true; + + $router->get('/web/test', fn () => 'web_response'); + } + + public function testDefineRoutesMethodExists(): void + { + $this->assertTrue(method_exists($this, 'defineRoutes')); + } + + public function testDefineWebRoutesMethodExists(): void + { + $this->assertTrue(method_exists($this, 'defineWebRoutes')); + } + + public function testSetUpApplicationRoutesMethodExists(): void + { + $this->assertTrue(method_exists($this, 'setUpApplicationRoutes')); + } + + public function testSetUpApplicationRoutesCallsDefineRoutes(): void + { + $this->defineRoutesCalled = false; + $this->defineWebRoutesCalled = false; + + $this->setUpApplicationRoutes($this->app); + + $this->assertTrue($this->defineRoutesCalled); + } + + public function testSetUpApplicationRoutesCallsDefineWebRoutes(): void + { + $this->defineRoutesCalled = false; + $this->defineWebRoutesCalled = false; + + $this->setUpApplicationRoutes($this->app); + + $this->assertTrue($this->defineWebRoutesCalled); + } + + public function testRouterIsPassedToDefineRoutes(): void + { + $router = $this->app->get(Router::class); + + $this->assertInstanceOf(Router::class, $router); + } +} From 682f15264c81bb245eb12077933204f5b8566495 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:42:28 +0000 Subject: [PATCH 12/18] test(testbench): add TestCaseTest --- tests/Testbench/TestCaseTest.php | 106 +++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 tests/Testbench/TestCaseTest.php diff --git a/tests/Testbench/TestCaseTest.php b/tests/Testbench/TestCaseTest.php new file mode 100644 index 000000000..9b9ec5e47 --- /dev/null +++ b/tests/Testbench/TestCaseTest.php @@ -0,0 +1,106 @@ +defineEnvironmentCalled = true; + $app->get('config')->set('testing.define_environment', 'called'); + } + + public function testTestCaseUsesCreatesApplicationTrait(): void + { + $uses = class_uses_recursive(static::class); + + $this->assertArrayHasKey(CreatesApplication::class, $uses); + } + + public function testTestCaseUsesHandlesRoutesTrait(): void + { + $uses = class_uses_recursive(static::class); + + $this->assertArrayHasKey(HandlesRoutes::class, $uses); + } + + public function testTestCaseUsesHandlesDatabasesTrait(): void + { + $uses = class_uses_recursive(static::class); + + $this->assertArrayHasKey(HandlesDatabases::class, $uses); + } + + public function testTestCaseUsesHandlesAttributesTrait(): void + { + $uses = class_uses_recursive(static::class); + + $this->assertArrayHasKey(HandlesAttributes::class, $uses); + } + + public function testTestCaseUsesInteractsWithTestCaseTrait(): void + { + $uses = class_uses_recursive(static::class); + + $this->assertArrayHasKey(InteractsWithTestCase::class, $uses); + } + + public function testDefineEnvironmentIsCalled(): void + { + $this->assertTrue($this->defineEnvironmentCalled); + $this->assertSame('called', $this->app->get('config')->get('testing.define_environment')); + } + + public function testClassLevelAttributeIsApplied(): void + { + // The WithConfig attribute at class level should be applied + $this->assertSame('class_level', $this->app->get('config')->get('testing.testcase_class')); + } + + #[WithConfig('testing.method_attribute', 'method_level')] + public function testMethodLevelAttributeIsApplied(): void + { + // The WithConfig attribute at method level should be applied + $this->assertSame('method_level', $this->app->get('config')->get('testing.method_attribute')); + } + + public function testReloadApplicationMethodExists(): void + { + $this->assertTrue(method_exists($this, 'reloadApplication')); + } + + public function testStaticLifecycleMethodsExist(): void + { + $this->assertTrue(method_exists(static::class, 'setUpBeforeClass')); + $this->assertTrue(method_exists(static::class, 'tearDownAfterClass')); + } + + public function testUsesTestingConcernIsAvailable(): void + { + $this->assertTrue(static::usesTestingConcern(HandlesAttributes::class)); + } + + public function testAppIsAvailable(): void + { + $this->assertNotNull($this->app); + } +} From 9163bb80af6b46c8cf59dbb871cbaf4ce0a6a4f9 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:46:56 +0000 Subject: [PATCH 13/18] fix(testing): fix return types and attribute execution in testing infrastructure - 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) --- .../src/Testing/AttributeParser.php | 3 ++- .../Testing/Attributes/DefineEnvironment.php | 4 +++- .../src/Testing/Attributes/DefineRoute.php | 4 +++- .../src/Testing/Attributes/RequiresEnv.php | 4 +++- .../src/Testing/Attributes/WithConfig.php | 4 +++- .../src/Testing/Attributes/WithMigration.php | 4 +++- .../Concerns/InteractsWithTestCase.php | 23 ++++++++++++++++--- .../src/Testing/Features/TestingFeature.php | 4 ++-- 8 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/foundation/src/Testing/AttributeParser.php b/src/foundation/src/Testing/AttributeParser.php index a33c82832..189cae7bd 100644 --- a/src/foundation/src/Testing/AttributeParser.php +++ b/src/foundation/src/Testing/AttributeParser.php @@ -97,7 +97,8 @@ public static function validAttribute(object|string $class): bool */ protected static function resolveAttribute(ReflectionAttribute $attribute): array { - return rescue(static function () use ($attribute) { + /** @var array{0: class-string|null, 1: object|null} */ + return rescue(static function () use ($attribute): array { // @phpstan-ignore argument.unresolvableType $instance = isset(class_implements($attribute->getName())[Resolvable::class]) ? transform($attribute->newInstance(), static fn (Resolvable $instance) => $instance->resolve()) : $attribute->newInstance(); diff --git a/src/foundation/src/Testing/Attributes/DefineEnvironment.php b/src/foundation/src/Testing/Attributes/DefineEnvironment.php index 052da3b1b..850681982 100644 --- a/src/foundation/src/Testing/Attributes/DefineEnvironment.php +++ b/src/foundation/src/Testing/Attributes/DefineEnvironment.php @@ -24,8 +24,10 @@ public function __construct( * @param \Hypervel\Foundation\Contracts\Application $app * @param \Closure(string, array):void $action */ - public function handle($app, Closure $action): void + public function handle($app, Closure $action): mixed { \call_user_func($action, $this->method, [$app]); + + return null; } } diff --git a/src/foundation/src/Testing/Attributes/DefineRoute.php b/src/foundation/src/Testing/Attributes/DefineRoute.php index 31f156dff..62b76dfb3 100644 --- a/src/foundation/src/Testing/Attributes/DefineRoute.php +++ b/src/foundation/src/Testing/Attributes/DefineRoute.php @@ -25,10 +25,12 @@ public function __construct( * @param \Hypervel\Foundation\Contracts\Application $app * @param \Closure(string, array):void $action */ - public function handle($app, Closure $action): void + public function handle($app, Closure $action): mixed { $router = $app->get(Router::class); \call_user_func($action, $this->method, [$router]); + + return null; } } diff --git a/src/foundation/src/Testing/Attributes/RequiresEnv.php b/src/foundation/src/Testing/Attributes/RequiresEnv.php index a1f4b3cfa..afe78151f 100644 --- a/src/foundation/src/Testing/Attributes/RequiresEnv.php +++ b/src/foundation/src/Testing/Attributes/RequiresEnv.php @@ -25,12 +25,14 @@ public function __construct( * @param \Hypervel\Foundation\Contracts\Application $app * @param \Closure(string, array):void $action */ - public function handle($app, Closure $action): void + public function handle($app, Closure $action): mixed { $message = $this->message ?? "Missing required environment variable `{$this->key}`"; if (env($this->key) === null) { \call_user_func($action, 'markTestSkipped', [$message]); } + + return null; } } diff --git a/src/foundation/src/Testing/Attributes/WithConfig.php b/src/foundation/src/Testing/Attributes/WithConfig.php index 430d946d9..32841dd1e 100644 --- a/src/foundation/src/Testing/Attributes/WithConfig.php +++ b/src/foundation/src/Testing/Attributes/WithConfig.php @@ -23,8 +23,10 @@ public function __construct( * * @param \Hypervel\Foundation\Contracts\Application $app */ - public function __invoke($app): void + public function __invoke($app): mixed { $app->get('config')->set($this->key, $this->value); + + return null; } } diff --git a/src/foundation/src/Testing/Attributes/WithMigration.php b/src/foundation/src/Testing/Attributes/WithMigration.php index 3b38a592f..5d394914b 100644 --- a/src/foundation/src/Testing/Attributes/WithMigration.php +++ b/src/foundation/src/Testing/Attributes/WithMigration.php @@ -32,12 +32,14 @@ public function __construct(string ...$paths) * * @param \Hypervel\Foundation\Contracts\Application $app */ - public function __invoke($app): void + public function __invoke($app): mixed { $app->afterResolving(Migrator::class, function (Migrator $migrator) { foreach ($this->paths as $path) { $migrator->path($path); } }); + + return null; } } diff --git a/src/foundation/src/Testing/Concerns/InteractsWithTestCase.php b/src/foundation/src/Testing/Concerns/InteractsWithTestCase.php index eaaa6a1e1..100e7b585 100644 --- a/src/foundation/src/Testing/Concerns/InteractsWithTestCase.php +++ b/src/foundation/src/Testing/Concerns/InteractsWithTestCase.php @@ -6,10 +6,12 @@ use Attribute; use Hypervel\Foundation\Testing\AttributeParser; +use Hypervel\Foundation\Testing\Contracts\Attributes\Actionable; use Hypervel\Foundation\Testing\Contracts\Attributes\AfterAll; use Hypervel\Foundation\Testing\Contracts\Attributes\AfterEach; use Hypervel\Foundation\Testing\Contracts\Attributes\BeforeAll; use Hypervel\Foundation\Testing\Contracts\Attributes\BeforeEach; +use Hypervel\Foundation\Testing\Contracts\Attributes\Invokable; use Hypervel\Foundation\Testing\Contracts\Attributes\Resolvable; use Hypervel\Support\Collection; @@ -169,12 +171,27 @@ protected static function resolvePhpUnitAttributesForMethod(string $className, ? } /** - * Execute BeforeEach lifecycle attributes. + * Execute setup lifecycle attributes (Invokable, Actionable, BeforeEach). */ protected function setUpTheTestEnvironmentUsingTestCase(): void { - $this->resolvePhpUnitAttributes() - ->flatten() + $attributes = $this->resolvePhpUnitAttributes()->flatten(); + + // Execute Invokable attributes (like WithConfig) + $attributes + ->filter(static fn ($instance) => $instance instanceof Invokable) + ->each(fn ($instance) => $instance($this->app)); + + // Execute Actionable attributes (like DefineEnvironment, DefineRoute) + $attributes + ->filter(static fn ($instance) => $instance instanceof Actionable) + ->each(fn ($instance) => $instance->handle( + $this->app, + fn ($method, $parameters) => $this->{$method}(...$parameters) + )); + + // Execute BeforeEach attributes + $attributes ->filter(static fn ($instance) => $instance instanceof BeforeEach) ->each(fn ($instance) => $instance->beforeEach($this->app)); } diff --git a/src/foundation/src/Testing/Features/TestingFeature.php b/src/foundation/src/Testing/Features/TestingFeature.php index 1381e3147..ce19cd32f 100644 --- a/src/foundation/src/Testing/Features/TestingFeature.php +++ b/src/foundation/src/Testing/Features/TestingFeature.php @@ -34,13 +34,13 @@ public static function run( // Inline memoization - replaces Orchestra's once() helper $defaultHasRun = false; - $defaultResolver = static function () use ($default, &$defaultHasRun) { + $defaultResolver = static function () use ($default, &$defaultHasRun): void { if ($defaultHasRun || $default === null) { return; } $defaultHasRun = true; - return $default(); + $default(); }; if ($testCase instanceof PHPUnitTestCase) { From 406d743893e484e22c2bf5eb7bcee3f25b5f4490 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Fri, 16 Jan 2026 14:53:21 +0000 Subject: [PATCH 14/18] style: apply php-cs-fixer formatting --- src/foundation/src/Testing/AttributeParser.php | 4 ++-- src/foundation/src/Testing/Attributes/Define.php | 3 ++- src/foundation/src/Testing/Attributes/DefineDatabase.php | 6 +++--- .../src/Testing/Attributes/DefineEnvironment.php | 5 +++-- src/foundation/src/Testing/Attributes/DefineRoute.php | 5 +++-- src/foundation/src/Testing/Attributes/RequiresEnv.php | 5 +++-- src/foundation/src/Testing/Attributes/WithConfig.php | 3 ++- .../src/Testing/Concerns/InteractsWithTestCase.php | 6 ++---- .../src/Testing/Contracts/Attributes/Actionable.php | 2 +- src/foundation/src/Testing/Features/TestingFeature.php | 7 +++---- tests/Foundation/Testing/Attributes/AttributesTest.php | 6 ++++-- 11 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/foundation/src/Testing/AttributeParser.php b/src/foundation/src/Testing/AttributeParser.php index 189cae7bd..5664bc305 100644 --- a/src/foundation/src/Testing/AttributeParser.php +++ b/src/foundation/src/Testing/AttributeParser.php @@ -93,11 +93,11 @@ public static function validAttribute(object|string $class): bool /** * Resolve the given attribute. * - * @return array{0: class-string|null, 1: object|null} + * @return array{0: null|class-string, 1: null|object} */ protected static function resolveAttribute(ReflectionAttribute $attribute): array { - /** @var array{0: class-string|null, 1: object|null} */ + /** @var array{0: null|class-string, 1: null|object} */ return rescue(static function () use ($attribute): array { // @phpstan-ignore argument.unresolvableType $instance = isset(class_implements($attribute->getName())[Resolvable::class]) ? transform($attribute->newInstance(), static fn (Resolvable $instance) => $instance->resolve()) diff --git a/src/foundation/src/Testing/Attributes/Define.php b/src/foundation/src/Testing/Attributes/Define.php index 7c9245c46..1d55570b2 100644 --- a/src/foundation/src/Testing/Attributes/Define.php +++ b/src/foundation/src/Testing/Attributes/Define.php @@ -19,7 +19,8 @@ final class Define implements Resolvable public function __construct( public readonly string $group, public readonly string $method - ) {} + ) { + } /** * Resolve the actual attribute class. diff --git a/src/foundation/src/Testing/Attributes/DefineDatabase.php b/src/foundation/src/Testing/Attributes/DefineDatabase.php index 0aa407871..87f197c44 100644 --- a/src/foundation/src/Testing/Attributes/DefineDatabase.php +++ b/src/foundation/src/Testing/Attributes/DefineDatabase.php @@ -21,7 +21,8 @@ final class DefineDatabase implements Actionable, AfterEach, BeforeEach public function __construct( public readonly string $method, public readonly bool $defer = true - ) {} + ) { + } /** * Handle the attribute before each test. @@ -47,8 +48,7 @@ public function afterEach($app): void * Handle the attribute. * * @param \Hypervel\Foundation\Contracts\Application $app - * @param \Closure(string, array):void $action - * @return \Closure|null + * @param Closure(string, array):void $action */ public function handle($app, Closure $action): ?Closure { diff --git a/src/foundation/src/Testing/Attributes/DefineEnvironment.php b/src/foundation/src/Testing/Attributes/DefineEnvironment.php index 850681982..d21c7f382 100644 --- a/src/foundation/src/Testing/Attributes/DefineEnvironment.php +++ b/src/foundation/src/Testing/Attributes/DefineEnvironment.php @@ -16,13 +16,14 @@ final class DefineEnvironment implements Actionable { public function __construct( public readonly string $method - ) {} + ) { + } /** * Handle the attribute. * * @param \Hypervel\Foundation\Contracts\Application $app - * @param \Closure(string, array):void $action + * @param Closure(string, array):void $action */ public function handle($app, Closure $action): mixed { diff --git a/src/foundation/src/Testing/Attributes/DefineRoute.php b/src/foundation/src/Testing/Attributes/DefineRoute.php index 62b76dfb3..ca95943ea 100644 --- a/src/foundation/src/Testing/Attributes/DefineRoute.php +++ b/src/foundation/src/Testing/Attributes/DefineRoute.php @@ -17,13 +17,14 @@ final class DefineRoute implements Actionable { public function __construct( public readonly string $method - ) {} + ) { + } /** * Handle the attribute. * * @param \Hypervel\Foundation\Contracts\Application $app - * @param \Closure(string, array):void $action + * @param Closure(string, array):void $action */ public function handle($app, Closure $action): mixed { diff --git a/src/foundation/src/Testing/Attributes/RequiresEnv.php b/src/foundation/src/Testing/Attributes/RequiresEnv.php index afe78151f..112c682c7 100644 --- a/src/foundation/src/Testing/Attributes/RequiresEnv.php +++ b/src/foundation/src/Testing/Attributes/RequiresEnv.php @@ -17,13 +17,14 @@ final class RequiresEnv implements Actionable public function __construct( public readonly string $key, public readonly ?string $message = null - ) {} + ) { + } /** * Handle the attribute. * * @param \Hypervel\Foundation\Contracts\Application $app - * @param \Closure(string, array):void $action + * @param Closure(string, array):void $action */ public function handle($app, Closure $action): mixed { diff --git a/src/foundation/src/Testing/Attributes/WithConfig.php b/src/foundation/src/Testing/Attributes/WithConfig.php index 32841dd1e..9026f80c7 100644 --- a/src/foundation/src/Testing/Attributes/WithConfig.php +++ b/src/foundation/src/Testing/Attributes/WithConfig.php @@ -16,7 +16,8 @@ final class WithConfig implements Invokable public function __construct( public readonly string $key, public readonly mixed $value - ) {} + ) { + } /** * Handle the attribute. diff --git a/src/foundation/src/Testing/Concerns/InteractsWithTestCase.php b/src/foundation/src/Testing/Concerns/InteractsWithTestCase.php index 100e7b585..ceb18dc49 100644 --- a/src/foundation/src/Testing/Concerns/InteractsWithTestCase.php +++ b/src/foundation/src/Testing/Concerns/InteractsWithTestCase.php @@ -18,7 +18,7 @@ /** * Provides test case lifecycle and attribute caching functionality. * - * @property \Hypervel\Foundation\Contracts\Application|null $app + * @property null|\Hypervel\Foundation\Contracts\Application $app */ trait InteractsWithTestCase { @@ -53,7 +53,7 @@ trait InteractsWithTestCase /** * Cached traits used by test case. * - * @var array|null + * @var null|array */ protected static ?array $cachedTestCaseUses = null; @@ -85,8 +85,6 @@ public static function cachedUsesForTestCase(): array /** * Programmatically add a testing feature attribute. - * - * @param object $attribute */ public static function usesTestingFeature(object $attribute, int $flag = Attribute::TARGET_CLASS): void { diff --git a/src/foundation/src/Testing/Contracts/Attributes/Actionable.php b/src/foundation/src/Testing/Contracts/Attributes/Actionable.php index 2c3c9d5eb..4f339a060 100644 --- a/src/foundation/src/Testing/Contracts/Attributes/Actionable.php +++ b/src/foundation/src/Testing/Contracts/Attributes/Actionable.php @@ -15,7 +15,7 @@ interface Actionable extends TestingFeature * Handle the attribute. * * @param \Hypervel\Foundation\Contracts\Application $app - * @param \Closure(string, array):void $action + * @param Closure(string, array):void $action */ public function handle($app, Closure $action): mixed; } diff --git a/src/foundation/src/Testing/Features/TestingFeature.php b/src/foundation/src/Testing/Features/TestingFeature.php index ce19cd32f..5b57a891c 100644 --- a/src/foundation/src/Testing/Features/TestingFeature.php +++ b/src/foundation/src/Testing/Features/TestingFeature.php @@ -19,9 +19,8 @@ final class TestingFeature /** * Resolve available testing features for Testbench. * - * @param object $testCase - * @param (\Closure():void)|null $default - * @param (\Closure(\Closure):mixed)|null $attribute + * @param null|(Closure():void) $default + * @param null|(Closure(Closure):mixed) $attribute * @return \Hypervel\Support\Fluent */ public static function run( @@ -44,7 +43,7 @@ public static function run( }; if ($testCase instanceof PHPUnitTestCase) { - /** @phpstan-ignore-next-line */ + /* @phpstan-ignore-next-line */ if ($testCase::usesTestingConcern(HandlesAttributes::class)) { $result['attribute'] = value($attribute, $defaultResolver); } diff --git a/tests/Foundation/Testing/Attributes/AttributesTest.php b/tests/Foundation/Testing/Attributes/AttributesTest.php index 1b0ad9105..5b1d9369e 100644 --- a/tests/Foundation/Testing/Attributes/AttributesTest.php +++ b/tests/Foundation/Testing/Attributes/AttributesTest.php @@ -4,6 +4,8 @@ namespace Hypervel\Tests\Foundation\Testing\Attributes; +use Attribute; +use Closure; use Hypervel\Foundation\Testing\Attributes\Define; use Hypervel\Foundation\Testing\Attributes\DefineDatabase; use Hypervel\Foundation\Testing\Attributes\DefineEnvironment; @@ -102,7 +104,7 @@ public function testDefineDatabaseDeferredExecution(): void $result = $attribute->handle($this->app, $action); $this->assertFalse($called); - $this->assertInstanceOf(\Closure::class, $result); + $this->assertInstanceOf(Closure::class, $result); // Execute the deferred callback $result(); @@ -237,7 +239,7 @@ public function testAttributesHaveCorrectTargets(): void private function assertAttributeHasTargets(string $class, array $expectedTargets): void { $reflection = new ReflectionClass($class); - $attributes = $reflection->getAttributes(\Attribute::class); + $attributes = $reflection->getAttributes(Attribute::class); $this->assertNotEmpty($attributes, "Class {$class} should have #[Attribute]"); From 9d96389eec23820ddf1d3fc420dd914bc3d34908 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:16:04 +0000 Subject: [PATCH 15/18] feat(testbench): integrate route registration into TestCase lifecycle - 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 --- src/testbench/src/Concerns/HandlesRoutes.php | 8 +- src/testbench/src/TestCase.php | 3 + tests/Sanctum/AuthenticateRequestsTest.php | 98 ++++++++++--------- .../Testbench/Concerns/HandlesRoutesTest.php | 28 ++++-- .../HandlesRoutesWithoutWebRoutesTest.php | 30 ++++++ 5 files changed, 108 insertions(+), 59 deletions(-) create mode 100644 tests/Testbench/Concerns/HandlesRoutesWithoutWebRoutesTest.php diff --git a/src/testbench/src/Concerns/HandlesRoutes.php b/src/testbench/src/Concerns/HandlesRoutes.php index b084a67ad..e8402f38f 100644 --- a/src/testbench/src/Concerns/HandlesRoutes.php +++ b/src/testbench/src/Concerns/HandlesRoutes.php @@ -42,7 +42,11 @@ protected function setUpApplicationRoutes($app): void $this->defineRoutes($router); - // Wrap web routes in 'web' middleware group using Hypervel's Router API - $router->group('/', fn () => $this->defineWebRoutes($router), ['middleware' => ['web']]); + // Only set up web routes group if the method is overridden + // This prevents empty group registration from interfering with other routes + $refMethod = new \ReflectionMethod($this, 'defineWebRoutes'); + if ($refMethod->getDeclaringClass()->getName() !== self::class) { + $router->group('/', fn () => $this->defineWebRoutes($router), ['middleware' => ['web']]); + } } } diff --git a/src/testbench/src/TestCase.php b/src/testbench/src/TestCase.php index d06209ea7..2874a2e56 100644 --- a/src/testbench/src/TestCase.php +++ b/src/testbench/src/TestCase.php @@ -45,6 +45,9 @@ protected function setUp(): void $this->afterApplicationCreated(function () { Timer::clearAll(); CoordinatorManager::until(Constants::WORKER_EXIT)->resume(); + + // Setup routes after application is created (providers are booted) + $this->setUpApplicationRoutes($this->app); }); parent::setUp(); diff --git a/tests/Sanctum/AuthenticateRequestsTest.php b/tests/Sanctum/AuthenticateRequestsTest.php index af83035b0..4fc81b68d 100644 --- a/tests/Sanctum/AuthenticateRequestsTest.php +++ b/tests/Sanctum/AuthenticateRequestsTest.php @@ -4,14 +4,12 @@ namespace Hypervel\Tests\Sanctum; -use Hyperf\Contract\ConfigInterface; use Hypervel\Context\Context; use Hypervel\Foundation\Testing\Concerns\RunTestsInCoroutine; use Hypervel\Foundation\Testing\RefreshDatabase; use Hypervel\Sanctum\PersonalAccessToken; use Hypervel\Sanctum\Sanctum; use Hypervel\Sanctum\SanctumServiceProvider; -use Hypervel\Support\Facades\Route; use Hypervel\Testbench\TestCase; use Hypervel\Tests\Sanctum\Stub\TestUser; @@ -30,31 +28,60 @@ protected function setUp(): void { parent::setUp(); - $this->app->register(SanctumServiceProvider::class); - - // Configure test environment - $this->app->get(ConfigInterface::class) - ->set([ - 'app.key' => 'AckfSECXIvnK5r28GVIWUAxmbBSjTsmF', - 'auth.guards.sanctum' => [ - 'driver' => 'sanctum', - 'provider' => 'users', - ], - 'auth.guards.web' => [ - 'driver' => 'session', - 'provider' => 'users', - ], - 'auth.providers.users.model' => TestUser::class, - 'auth.providers.users.driver' => 'eloquent', - 'database.default' => 'testing', - 'sanctum.stateful' => ['localhost', '127.0.0.1'], - 'sanctum.guard' => ['web'], - ]); - - $this->defineRoutes(); $this->createUsersTable(); } + protected function getPackageProviders($app): array + { + return [ + SanctumServiceProvider::class, + ]; + } + + protected function defineEnvironment($app): void + { + parent::defineEnvironment($app); + + $app->get('config')->set([ + 'app.key' => 'AckfSECXIvnK5r28GVIWUAxmbBSjTsmF', + 'auth.guards.sanctum' => [ + 'driver' => 'sanctum', + 'provider' => 'users', + ], + 'auth.guards.web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + 'auth.providers.users.model' => TestUser::class, + 'auth.providers.users.driver' => 'eloquent', + 'sanctum.stateful' => ['localhost', '127.0.0.1'], + 'sanctum.guard' => ['web'], + ]); + } + + protected function defineRoutes($router): void + { + $router->get('/sanctum/api/user', function () { + $user = auth('sanctum')->user(); + + if (! $user) { + abort(401); + } + + return response()->json(['email' => $user->email]); + }); + + $router->get('/sanctum/web/user', function () { + $user = auth('sanctum')->user(); + + if (! $user) { + abort(401); + } + + return response()->json(['email' => $user->email]); + }); + } + protected function tearDown(): void { parent::tearDown(); @@ -89,29 +116,6 @@ protected function createUsersTable(): void }); } - protected function defineRoutes(): void - { - Route::get('/sanctum/api/user', function () { - $user = auth('sanctum')->user(); - - if (! $user) { - abort(401); - } - - return response()->json(['email' => $user->email]); - }); - - Route::get('/sanctum/web/user', function () { - $user = auth('sanctum')->user(); - - if (! $user) { - abort(401); - } - - return response()->json(['email' => $user->email]); - }); - } - public function testCanAuthorizeValidUserUsingAuthorizationHeader(): void { // Create a user in the database diff --git a/tests/Testbench/Concerns/HandlesRoutesTest.php b/tests/Testbench/Concerns/HandlesRoutesTest.php index 193bbb5b5..3c6db1513 100644 --- a/tests/Testbench/Concerns/HandlesRoutesTest.php +++ b/tests/Testbench/Concerns/HandlesRoutesTest.php @@ -4,6 +4,7 @@ namespace Hypervel\Tests\Testbench\Concerns; +use Hypervel\Foundation\Testing\Concerns\RunTestsInCoroutine; use Hypervel\Router\Router; use Hypervel\Testbench\TestCase; @@ -13,6 +14,8 @@ */ class HandlesRoutesTest extends TestCase { + use RunTestsInCoroutine; + protected bool $defineRoutesCalled = false; protected bool $defineWebRoutesCalled = false; @@ -28,6 +31,8 @@ protected function defineWebRoutes($router): void { $this->defineWebRoutesCalled = true; + // Note: Web routes are wrapped in 'web' middleware group by setUpApplicationRoutes + // We register a simple route here just to verify the method is called $router->get('/web/test', fn () => 'web_response'); } @@ -48,21 +53,15 @@ public function testSetUpApplicationRoutesMethodExists(): void public function testSetUpApplicationRoutesCallsDefineRoutes(): void { - $this->defineRoutesCalled = false; - $this->defineWebRoutesCalled = false; - - $this->setUpApplicationRoutes($this->app); - + // setUpApplicationRoutes is called automatically in setUp via afterApplicationCreated + // so defineRoutesCalled should already be true $this->assertTrue($this->defineRoutesCalled); } public function testSetUpApplicationRoutesCallsDefineWebRoutes(): void { - $this->defineRoutesCalled = false; - $this->defineWebRoutesCalled = false; - - $this->setUpApplicationRoutes($this->app); - + // setUpApplicationRoutes is called automatically in setUp via afterApplicationCreated + // so defineWebRoutesCalled should already be true $this->assertTrue($this->defineWebRoutesCalled); } @@ -72,4 +71,13 @@ public function testRouterIsPassedToDefineRoutes(): void $this->assertInstanceOf(Router::class, $router); } + + public function testDefinedRoutesAreAccessibleViaHttp(): void + { + $response = $this->get('/api/test'); + + $response->assertSuccessful(); + $response->assertContent('api_response'); + } + } diff --git a/tests/Testbench/Concerns/HandlesRoutesWithoutWebRoutesTest.php b/tests/Testbench/Concerns/HandlesRoutesWithoutWebRoutesTest.php new file mode 100644 index 000000000..8d029b432 --- /dev/null +++ b/tests/Testbench/Concerns/HandlesRoutesWithoutWebRoutesTest.php @@ -0,0 +1,30 @@ +get('/only-api', fn () => 'only_api_response'); + } + + public function testRoutesWorkWithoutDefineWebRoutes(): void + { + $this->get('/only-api')->assertSuccessful()->assertContent('only_api_response'); + } + +} From 15c8dfded4cf6842758f20a550fe5eb85c451fd6 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:21:17 +0000 Subject: [PATCH 16/18] style: apply php-cs-fixer formatting to testbench routes --- src/testbench/src/Concerns/HandlesRoutes.php | 3 ++- tests/Testbench/Concerns/HandlesRoutesTest.php | 1 - tests/Testbench/Concerns/HandlesRoutesWithoutWebRoutesTest.php | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/testbench/src/Concerns/HandlesRoutes.php b/src/testbench/src/Concerns/HandlesRoutes.php index e8402f38f..ad7a7f70e 100644 --- a/src/testbench/src/Concerns/HandlesRoutes.php +++ b/src/testbench/src/Concerns/HandlesRoutes.php @@ -5,6 +5,7 @@ namespace Hypervel\Testbench\Concerns; use Hypervel\Router\Router; +use ReflectionMethod; /** * Provides hooks for defining test routes. @@ -44,7 +45,7 @@ protected function setUpApplicationRoutes($app): void // Only set up web routes group if the method is overridden // This prevents empty group registration from interfering with other routes - $refMethod = new \ReflectionMethod($this, 'defineWebRoutes'); + $refMethod = new ReflectionMethod($this, 'defineWebRoutes'); if ($refMethod->getDeclaringClass()->getName() !== self::class) { $router->group('/', fn () => $this->defineWebRoutes($router), ['middleware' => ['web']]); } diff --git a/tests/Testbench/Concerns/HandlesRoutesTest.php b/tests/Testbench/Concerns/HandlesRoutesTest.php index 3c6db1513..2cbee9d19 100644 --- a/tests/Testbench/Concerns/HandlesRoutesTest.php +++ b/tests/Testbench/Concerns/HandlesRoutesTest.php @@ -79,5 +79,4 @@ public function testDefinedRoutesAreAccessibleViaHttp(): void $response->assertSuccessful(); $response->assertContent('api_response'); } - } diff --git a/tests/Testbench/Concerns/HandlesRoutesWithoutWebRoutesTest.php b/tests/Testbench/Concerns/HandlesRoutesWithoutWebRoutesTest.php index 8d029b432..fc2c9bddc 100644 --- a/tests/Testbench/Concerns/HandlesRoutesWithoutWebRoutesTest.php +++ b/tests/Testbench/Concerns/HandlesRoutesWithoutWebRoutesTest.php @@ -26,5 +26,4 @@ public function testRoutesWorkWithoutDefineWebRoutes(): void { $this->get('/only-api')->assertSuccessful()->assertContent('only_api_response'); } - } From e91a8dabc5fdf56b7fbfdc2a50811dca1c4a1c35 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:45:47 +0000 Subject: [PATCH 17/18] test(testing): add tests for Define meta-attribute and attribute inheritance - 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 --- .../Testing/Concerns/HandlesAttributes.php | 7 +- .../Concerns/AttributeInheritanceTest.php | 85 +++++++++++++++++++ .../Concerns/InteractsWithTestCaseTest.php | 54 ++++++++++++ 3 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 tests/Foundation/Testing/Concerns/AttributeInheritanceTest.php diff --git a/src/foundation/src/Testing/Concerns/HandlesAttributes.php b/src/foundation/src/Testing/Concerns/HandlesAttributes.php index a38b96012..af66d10fb 100644 --- a/src/foundation/src/Testing/Concerns/HandlesAttributes.php +++ b/src/foundation/src/Testing/Concerns/HandlesAttributes.php @@ -15,7 +15,12 @@ trait HandlesAttributes { /** - * Parse test method attributes. + * Parse and execute test method attributes of a specific type. + * + * Note: Attributes are already executed automatically via setUpTheTestEnvironmentUsingTestCase(). + * This method is for internal use by the testing infrastructure. + * + * @internal * * @param \Hypervel\Foundation\Contracts\Application $app * @param class-string $attribute diff --git a/tests/Foundation/Testing/Concerns/AttributeInheritanceTest.php b/tests/Foundation/Testing/Concerns/AttributeInheritanceTest.php new file mode 100644 index 000000000..0e90c411a --- /dev/null +++ b/tests/Foundation/Testing/Concerns/AttributeInheritanceTest.php @@ -0,0 +1,85 @@ + $attr['key'] === WithConfig::class + ); + + $this->assertCount(2, $withConfigAttributes); + + // Extract the config keys to verify both are present + $configKeys = array_map( + fn ($attr) => $attr['instance']->key, + $withConfigAttributes + ); + + $this->assertContains('testing.parent_class', $configKeys); + $this->assertContains('testing.child_class', $configKeys); + } + + public function testParentAttributeIsExecutedThroughLifecycle(): void + { + // The parent's #[WithConfig('testing.parent_class', 'parent_value')] should be applied + $this->assertSame( + 'parent_value', + $this->app->get('config')->get('testing.parent_class') + ); + } + + public function testChildAttributeIsExecutedThroughLifecycle(): void + { + // The child's #[WithConfig('testing.child_class', 'child_value')] should be applied + $this->assertSame( + 'child_value', + $this->app->get('config')->get('testing.child_class') + ); + } + + public function testParentAttributesAreAppliedBeforeChildAttributes(): void + { + // Parent attributes come first in the array (prepended during recursion) + $attributes = AttributeParser::forClass(static::class); + + $withConfigAttributes = array_values(array_filter( + $attributes, + fn ($attr) => $attr['key'] === WithConfig::class + )); + + // Parent should be first + $this->assertSame('testing.parent_class', $withConfigAttributes[0]['instance']->key); + // Child should be second + $this->assertSame('testing.child_class', $withConfigAttributes[1]['instance']->key); + } +} + +/** + * Abstract parent test case with class-level attributes for inheritance testing. + * + * @internal + */ +#[WithConfig('testing.parent_class', 'parent_value')] +abstract class AbstractParentTestCase extends TestCase +{ +} diff --git a/tests/Foundation/Testing/Concerns/InteractsWithTestCaseTest.php b/tests/Foundation/Testing/Concerns/InteractsWithTestCaseTest.php index be9cee0e8..d4466ab65 100644 --- a/tests/Foundation/Testing/Concerns/InteractsWithTestCaseTest.php +++ b/tests/Foundation/Testing/Concerns/InteractsWithTestCaseTest.php @@ -4,6 +4,9 @@ namespace Hypervel\Tests\Foundation\Testing\Concerns; +use Hypervel\Foundation\Testing\AttributeParser; +use Hypervel\Foundation\Testing\Attributes\Define; +use Hypervel\Foundation\Testing\Attributes\DefineEnvironment; use Hypervel\Foundation\Testing\Attributes\WithConfig; use Hypervel\Foundation\Testing\Concerns\HandlesAttributes; use Hypervel\Foundation\Testing\Concerns\InteractsWithTestCase; @@ -72,4 +75,55 @@ public function testUsesTestingFeatureAddsAttribute(): void $this->assertTrue($attributes->has(WithConfig::class)); } + + public function testDefineMetaAttributeIsResolvedByAttributeParser(): void + { + // Test that AttributeParser resolves #[Define('env', 'method')] to DefineEnvironment + $attributes = AttributeParser::forMethod( + DefineMetaAttributeTestCase::class, + 'testWithDefineAttribute' + ); + + // Should have one attribute, resolved from Define to DefineEnvironment + $this->assertCount(1, $attributes); + $this->assertSame(DefineEnvironment::class, $attributes[0]['key']); + $this->assertInstanceOf(DefineEnvironment::class, $attributes[0]['instance']); + $this->assertSame('setupDefineEnv', $attributes[0]['instance']->method); + } + + #[Define('env', 'setupDefineEnvForExecution')] + public function testDefineMetaAttributeIsExecutedThroughLifecycle(): void + { + // The #[Define('env', 'setupDefineEnvForExecution')] attribute should have been + // resolved to DefineEnvironment and executed during setUp, calling our method + $this->assertSame( + 'define_env_executed', + $this->app->get('config')->get('testing.define_meta_attribute') + ); + } + + protected function setupDefineEnvForExecution($app): void + { + $app->get('config')->set('testing.define_meta_attribute', 'define_env_executed'); + } +} + +/** + * Test fixture for Define meta-attribute parsing. + * + * @internal + * @coversNothing + */ +class DefineMetaAttributeTestCase extends TestCase +{ + #[Define('env', 'setupDefineEnv')] + public function testWithDefineAttribute(): void + { + // This method exists just to have the attribute parsed + } + + protected function setupDefineEnv($app): void + { + // Method that would be called + } } From 78f73d3e479212c8a4bbc007dc8b5b2c2a929378 Mon Sep 17 00:00:00 2001 From: Raj Siva-Rajah <5361908+binaryfire@users.noreply.github.com> Date: Fri, 16 Jan 2026 16:16:12 +0000 Subject: [PATCH 18/18] refactor(testing): add ApplicationContract and Router type hints 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. --- .../src/Testing/Attributes/DefineDatabase.php | 12 ++++-------- .../src/Testing/Attributes/DefineEnvironment.php | 4 ++-- .../src/Testing/Attributes/DefineRoute.php | 4 ++-- .../src/Testing/Attributes/RequiresEnv.php | 4 ++-- .../src/Testing/Attributes/WithConfig.php | 5 ++--- .../src/Testing/Attributes/WithMigration.php | 5 ++--- .../src/Testing/Concerns/HandlesAttributes.php | 4 ++-- .../Testing/Concerns/InteractsWithContainer.php | 4 +--- .../Testing/Contracts/Attributes/Actionable.php | 4 ++-- .../Testing/Contracts/Attributes/AfterEach.php | 6 +++--- .../Testing/Contracts/Attributes/BeforeEach.php | 6 +++--- .../Testing/Contracts/Attributes/Invokable.php | 6 +++--- .../src/Concerns/CreatesApplication.php | 16 ++++++---------- src/testbench/src/Concerns/HandlesRoutes.php | 13 ++++--------- src/testbench/src/TestCase.php | 4 +--- .../Testing/Concerns/DefineEnvironmentTest.php | 8 ++++---- tests/Sanctum/AuthenticateRequestsTest.php | 8 +++++--- tests/Sentry/Features/LogFeatureTest.php | 3 ++- .../Concerns/CreatesApplicationTest.php | 5 +++-- tests/Testbench/Concerns/HandlesRoutesTest.php | 4 ++-- .../HandlesRoutesWithoutWebRoutesTest.php | 3 ++- tests/Testbench/TestCaseTest.php | 3 ++- 22 files changed, 59 insertions(+), 72 deletions(-) diff --git a/src/foundation/src/Testing/Attributes/DefineDatabase.php b/src/foundation/src/Testing/Attributes/DefineDatabase.php index 87f197c44..d8feec7e5 100644 --- a/src/foundation/src/Testing/Attributes/DefineDatabase.php +++ b/src/foundation/src/Testing/Attributes/DefineDatabase.php @@ -6,6 +6,7 @@ use Attribute; use Closure; +use Hypervel\Foundation\Contracts\Application as ApplicationContract; use Hypervel\Foundation\Testing\Contracts\Attributes\Actionable; use Hypervel\Foundation\Testing\Contracts\Attributes\AfterEach; use Hypervel\Foundation\Testing\Contracts\Attributes\BeforeEach; @@ -26,20 +27,16 @@ public function __construct( /** * Handle the attribute before each test. - * - * @param \Hypervel\Foundation\Contracts\Application $app */ - public function beforeEach($app): void + public function beforeEach(ApplicationContract $app): void { ResetRefreshDatabaseState::run(); } /** * Handle the attribute after each test. - * - * @param \Hypervel\Foundation\Contracts\Application $app */ - public function afterEach($app): void + public function afterEach(ApplicationContract $app): void { ResetRefreshDatabaseState::run(); } @@ -47,10 +44,9 @@ public function afterEach($app): void /** * Handle the attribute. * - * @param \Hypervel\Foundation\Contracts\Application $app * @param Closure(string, array):void $action */ - public function handle($app, Closure $action): ?Closure + public function handle(ApplicationContract $app, Closure $action): ?Closure { $resolver = function () use ($app, $action) { \call_user_func($action, $this->method, [$app]); diff --git a/src/foundation/src/Testing/Attributes/DefineEnvironment.php b/src/foundation/src/Testing/Attributes/DefineEnvironment.php index d21c7f382..1ca822be4 100644 --- a/src/foundation/src/Testing/Attributes/DefineEnvironment.php +++ b/src/foundation/src/Testing/Attributes/DefineEnvironment.php @@ -6,6 +6,7 @@ use Attribute; use Closure; +use Hypervel\Foundation\Contracts\Application as ApplicationContract; use Hypervel\Foundation\Testing\Contracts\Attributes\Actionable; /** @@ -22,10 +23,9 @@ public function __construct( /** * Handle the attribute. * - * @param \Hypervel\Foundation\Contracts\Application $app * @param Closure(string, array):void $action */ - public function handle($app, Closure $action): mixed + public function handle(ApplicationContract $app, Closure $action): mixed { \call_user_func($action, $this->method, [$app]); diff --git a/src/foundation/src/Testing/Attributes/DefineRoute.php b/src/foundation/src/Testing/Attributes/DefineRoute.php index ca95943ea..fd238b0d7 100644 --- a/src/foundation/src/Testing/Attributes/DefineRoute.php +++ b/src/foundation/src/Testing/Attributes/DefineRoute.php @@ -6,6 +6,7 @@ use Attribute; use Closure; +use Hypervel\Foundation\Contracts\Application as ApplicationContract; use Hypervel\Foundation\Testing\Contracts\Attributes\Actionable; use Hypervel\Router\Router; @@ -23,10 +24,9 @@ public function __construct( /** * Handle the attribute. * - * @param \Hypervel\Foundation\Contracts\Application $app * @param Closure(string, array):void $action */ - public function handle($app, Closure $action): mixed + public function handle(ApplicationContract $app, Closure $action): mixed { $router = $app->get(Router::class); diff --git a/src/foundation/src/Testing/Attributes/RequiresEnv.php b/src/foundation/src/Testing/Attributes/RequiresEnv.php index 112c682c7..603a43044 100644 --- a/src/foundation/src/Testing/Attributes/RequiresEnv.php +++ b/src/foundation/src/Testing/Attributes/RequiresEnv.php @@ -6,6 +6,7 @@ use Attribute; use Closure; +use Hypervel\Foundation\Contracts\Application as ApplicationContract; use Hypervel\Foundation\Testing\Contracts\Attributes\Actionable; /** @@ -23,10 +24,9 @@ public function __construct( /** * Handle the attribute. * - * @param \Hypervel\Foundation\Contracts\Application $app * @param Closure(string, array):void $action */ - public function handle($app, Closure $action): mixed + public function handle(ApplicationContract $app, Closure $action): mixed { $message = $this->message ?? "Missing required environment variable `{$this->key}`"; diff --git a/src/foundation/src/Testing/Attributes/WithConfig.php b/src/foundation/src/Testing/Attributes/WithConfig.php index 9026f80c7..d4572496d 100644 --- a/src/foundation/src/Testing/Attributes/WithConfig.php +++ b/src/foundation/src/Testing/Attributes/WithConfig.php @@ -5,6 +5,7 @@ namespace Hypervel\Foundation\Testing\Attributes; use Attribute; +use Hypervel\Foundation\Contracts\Application as ApplicationContract; use Hypervel\Foundation\Testing\Contracts\Attributes\Invokable; /** @@ -21,10 +22,8 @@ public function __construct( /** * Handle the attribute. - * - * @param \Hypervel\Foundation\Contracts\Application $app */ - public function __invoke($app): mixed + public function __invoke(ApplicationContract $app): mixed { $app->get('config')->set($this->key, $this->value); diff --git a/src/foundation/src/Testing/Attributes/WithMigration.php b/src/foundation/src/Testing/Attributes/WithMigration.php index 5d394914b..31dbb3a03 100644 --- a/src/foundation/src/Testing/Attributes/WithMigration.php +++ b/src/foundation/src/Testing/Attributes/WithMigration.php @@ -6,6 +6,7 @@ use Attribute; use Hyperf\Database\Migrations\Migrator; +use Hypervel\Foundation\Contracts\Application as ApplicationContract; use Hypervel\Foundation\Testing\Contracts\Attributes\Invokable; /** @@ -29,10 +30,8 @@ public function __construct(string ...$paths) /** * Handle the attribute. - * - * @param \Hypervel\Foundation\Contracts\Application $app */ - public function __invoke($app): mixed + public function __invoke(ApplicationContract $app): mixed { $app->afterResolving(Migrator::class, function (Migrator $migrator) { foreach ($this->paths as $path) { diff --git a/src/foundation/src/Testing/Concerns/HandlesAttributes.php b/src/foundation/src/Testing/Concerns/HandlesAttributes.php index af66d10fb..c2fb5c061 100644 --- a/src/foundation/src/Testing/Concerns/HandlesAttributes.php +++ b/src/foundation/src/Testing/Concerns/HandlesAttributes.php @@ -4,6 +4,7 @@ namespace Hypervel\Foundation\Testing\Concerns; +use Hypervel\Foundation\Contracts\Application as ApplicationContract; use Hypervel\Foundation\Testing\Contracts\Attributes\Actionable; use Hypervel\Foundation\Testing\Contracts\Attributes\Invokable; use Hypervel\Foundation\Testing\Features\FeaturesCollection; @@ -22,10 +23,9 @@ trait HandlesAttributes * * @internal * - * @param \Hypervel\Foundation\Contracts\Application $app * @param class-string $attribute */ - protected function parseTestMethodAttributes($app, string $attribute): FeaturesCollection + protected function parseTestMethodAttributes(ApplicationContract $app, string $attribute): FeaturesCollection { $attributes = $this->resolvePhpUnitAttributes() ->filter(static fn ($attributes, string $key) => $key === $attribute && ! empty($attributes)) diff --git a/src/foundation/src/Testing/Concerns/InteractsWithContainer.php b/src/foundation/src/Testing/Concerns/InteractsWithContainer.php index f19284d99..50a05aeed 100644 --- a/src/foundation/src/Testing/Concerns/InteractsWithContainer.php +++ b/src/foundation/src/Testing/Concerns/InteractsWithContainer.php @@ -98,10 +98,8 @@ protected function refreshApplication(): void /** * Define environment setup. - * - * @param \Hypervel\Foundation\Contracts\Application $app */ - protected function defineEnvironment($app): void + protected function defineEnvironment(ApplicationContract $app): void { // Override in subclass. } diff --git a/src/foundation/src/Testing/Contracts/Attributes/Actionable.php b/src/foundation/src/Testing/Contracts/Attributes/Actionable.php index 4f339a060..3f22166fa 100644 --- a/src/foundation/src/Testing/Contracts/Attributes/Actionable.php +++ b/src/foundation/src/Testing/Contracts/Attributes/Actionable.php @@ -5,6 +5,7 @@ namespace Hypervel\Foundation\Testing\Contracts\Attributes; use Closure; +use Hypervel\Foundation\Contracts\Application as ApplicationContract; /** * Interface for attributes that handle actions via a callback. @@ -14,8 +15,7 @@ interface Actionable extends TestingFeature /** * Handle the attribute. * - * @param \Hypervel\Foundation\Contracts\Application $app * @param Closure(string, array):void $action */ - public function handle($app, Closure $action): mixed; + public function handle(ApplicationContract $app, Closure $action): mixed; } diff --git a/src/foundation/src/Testing/Contracts/Attributes/AfterEach.php b/src/foundation/src/Testing/Contracts/Attributes/AfterEach.php index 83bb34677..6f5a8ed48 100644 --- a/src/foundation/src/Testing/Contracts/Attributes/AfterEach.php +++ b/src/foundation/src/Testing/Contracts/Attributes/AfterEach.php @@ -4,6 +4,8 @@ namespace Hypervel\Foundation\Testing\Contracts\Attributes; +use Hypervel\Foundation\Contracts\Application as ApplicationContract; + /** * Interface for attributes that run after each test. */ @@ -11,8 +13,6 @@ interface AfterEach extends TestingFeature { /** * Handle the attribute. - * - * @param \Hypervel\Foundation\Contracts\Application $app */ - public function afterEach($app): void; + public function afterEach(ApplicationContract $app): void; } diff --git a/src/foundation/src/Testing/Contracts/Attributes/BeforeEach.php b/src/foundation/src/Testing/Contracts/Attributes/BeforeEach.php index aa81f47c4..1cd60b573 100644 --- a/src/foundation/src/Testing/Contracts/Attributes/BeforeEach.php +++ b/src/foundation/src/Testing/Contracts/Attributes/BeforeEach.php @@ -4,6 +4,8 @@ namespace Hypervel\Foundation\Testing\Contracts\Attributes; +use Hypervel\Foundation\Contracts\Application as ApplicationContract; + /** * Interface for attributes that run before each test. */ @@ -11,8 +13,6 @@ interface BeforeEach extends TestingFeature { /** * Handle the attribute. - * - * @param \Hypervel\Foundation\Contracts\Application $app */ - public function beforeEach($app): void; + public function beforeEach(ApplicationContract $app): void; } diff --git a/src/foundation/src/Testing/Contracts/Attributes/Invokable.php b/src/foundation/src/Testing/Contracts/Attributes/Invokable.php index a40cbd72d..591a897ee 100644 --- a/src/foundation/src/Testing/Contracts/Attributes/Invokable.php +++ b/src/foundation/src/Testing/Contracts/Attributes/Invokable.php @@ -4,6 +4,8 @@ namespace Hypervel\Foundation\Testing\Contracts\Attributes; +use Hypervel\Foundation\Contracts\Application as ApplicationContract; + /** * Interface for attributes that are directly invokable. */ @@ -11,8 +13,6 @@ interface Invokable extends TestingFeature { /** * Handle the attribute. - * - * @param \Hypervel\Foundation\Contracts\Application $app */ - public function __invoke($app): mixed; + public function __invoke(ApplicationContract $app): mixed; } diff --git a/src/testbench/src/Concerns/CreatesApplication.php b/src/testbench/src/Concerns/CreatesApplication.php index 3cc34966c..f9853e891 100644 --- a/src/testbench/src/Concerns/CreatesApplication.php +++ b/src/testbench/src/Concerns/CreatesApplication.php @@ -4,6 +4,8 @@ namespace Hypervel\Testbench\Concerns; +use Hypervel\Foundation\Contracts\Application as ApplicationContract; + /** * Provides hooks for registering package service providers and aliases. */ @@ -12,10 +14,9 @@ trait CreatesApplication /** * Get package providers. * - * @param \Hypervel\Foundation\Contracts\Application $app * @return array */ - protected function getPackageProviders($app): array + protected function getPackageProviders(ApplicationContract $app): array { return []; } @@ -23,20 +24,17 @@ protected function getPackageProviders($app): array /** * Get package aliases. * - * @param \Hypervel\Foundation\Contracts\Application $app * @return array */ - protected function getPackageAliases($app): array + protected function getPackageAliases(ApplicationContract $app): array { return []; } /** * Register package providers. - * - * @param \Hypervel\Foundation\Contracts\Application $app */ - protected function registerPackageProviders($app): void + protected function registerPackageProviders(ApplicationContract $app): void { foreach ($this->getPackageProviders($app) as $provider) { $app->register($provider); @@ -45,10 +43,8 @@ protected function registerPackageProviders($app): void /** * Register package aliases. - * - * @param \Hypervel\Foundation\Contracts\Application $app */ - protected function registerPackageAliases($app): void + protected function registerPackageAliases(ApplicationContract $app): void { $aliases = $this->getPackageAliases($app); diff --git a/src/testbench/src/Concerns/HandlesRoutes.php b/src/testbench/src/Concerns/HandlesRoutes.php index ad7a7f70e..3cea8604f 100644 --- a/src/testbench/src/Concerns/HandlesRoutes.php +++ b/src/testbench/src/Concerns/HandlesRoutes.php @@ -4,6 +4,7 @@ namespace Hypervel\Testbench\Concerns; +use Hypervel\Foundation\Contracts\Application as ApplicationContract; use Hypervel\Router\Router; use ReflectionMethod; @@ -14,30 +15,24 @@ trait HandlesRoutes { /** * Define routes setup. - * - * @param \Hypervel\Router\Router $router */ - protected function defineRoutes($router): void + protected function defineRoutes(Router $router): void { // Define routes. } /** * Define web routes setup. - * - * @param \Hypervel\Router\Router $router */ - protected function defineWebRoutes($router): void + protected function defineWebRoutes(Router $router): void { // Define web routes. } /** * Setup application routes. - * - * @param \Hypervel\Foundation\Contracts\Application $app */ - protected function setUpApplicationRoutes($app): void + protected function setUpApplicationRoutes(ApplicationContract $app): void { $router = $app->get(Router::class); diff --git a/src/testbench/src/TestCase.php b/src/testbench/src/TestCase.php index 2874a2e56..df85d6213 100644 --- a/src/testbench/src/TestCase.php +++ b/src/testbench/src/TestCase.php @@ -59,10 +59,8 @@ protected function setUp(): void /** * Define environment setup. - * - * @param \Hypervel\Foundation\Contracts\Application $app */ - protected function defineEnvironment($app): void + protected function defineEnvironment(ApplicationContract $app): void { $this->registerPackageProviders($app); $this->registerPackageAliases($app); diff --git a/tests/Foundation/Testing/Concerns/DefineEnvironmentTest.php b/tests/Foundation/Testing/Concerns/DefineEnvironmentTest.php index 410e58121..d8fc7549f 100644 --- a/tests/Foundation/Testing/Concerns/DefineEnvironmentTest.php +++ b/tests/Foundation/Testing/Concerns/DefineEnvironmentTest.php @@ -4,7 +4,7 @@ namespace Hypervel\Tests\Foundation\Testing\Concerns; -use Hypervel\Foundation\Contracts\Application; +use Hypervel\Foundation\Contracts\Application as ApplicationContract; use Hypervel\Testbench\TestCase; /** @@ -15,9 +15,9 @@ class DefineEnvironmentTest extends TestCase { protected bool $defineEnvironmentCalled = false; - protected ?Application $passedApp = null; + protected ?ApplicationContract $passedApp = null; - protected function defineEnvironment($app): void + protected function defineEnvironment(ApplicationContract $app): void { $this->defineEnvironmentCalled = true; $this->passedApp = $app; @@ -34,7 +34,7 @@ public function testDefineEnvironmentIsCalledDuringSetUp(): void public function testAppInstanceIsPassed(): void { $this->assertNotNull($this->passedApp); - $this->assertInstanceOf(Application::class, $this->passedApp); + $this->assertInstanceOf(ApplicationContract::class, $this->passedApp); $this->assertSame($this->app, $this->passedApp); } diff --git a/tests/Sanctum/AuthenticateRequestsTest.php b/tests/Sanctum/AuthenticateRequestsTest.php index 4fc81b68d..dc67361a5 100644 --- a/tests/Sanctum/AuthenticateRequestsTest.php +++ b/tests/Sanctum/AuthenticateRequestsTest.php @@ -5,8 +5,10 @@ namespace Hypervel\Tests\Sanctum; use Hypervel\Context\Context; +use Hypervel\Foundation\Contracts\Application as ApplicationContract; use Hypervel\Foundation\Testing\Concerns\RunTestsInCoroutine; use Hypervel\Foundation\Testing\RefreshDatabase; +use Hypervel\Router\Router; use Hypervel\Sanctum\PersonalAccessToken; use Hypervel\Sanctum\Sanctum; use Hypervel\Sanctum\SanctumServiceProvider; @@ -31,14 +33,14 @@ protected function setUp(): void $this->createUsersTable(); } - protected function getPackageProviders($app): array + protected function getPackageProviders(ApplicationContract $app): array { return [ SanctumServiceProvider::class, ]; } - protected function defineEnvironment($app): void + protected function defineEnvironment(ApplicationContract $app): void { parent::defineEnvironment($app); @@ -59,7 +61,7 @@ protected function defineEnvironment($app): void ]); } - protected function defineRoutes($router): void + protected function defineRoutes(Router $router): void { $router->get('/sanctum/api/user', function () { $user = auth('sanctum')->user(); diff --git a/tests/Sentry/Features/LogFeatureTest.php b/tests/Sentry/Features/LogFeatureTest.php index 91e69eaa8..982898318 100644 --- a/tests/Sentry/Features/LogFeatureTest.php +++ b/tests/Sentry/Features/LogFeatureTest.php @@ -5,6 +5,7 @@ namespace Hypervel\Tests\Sentry\Features; use Hyperf\Contract\ConfigInterface; +use Hypervel\Foundation\Contracts\Application as ApplicationContract; use Hypervel\Foundation\Testing\Concerns\RunTestsInCoroutine; use Hypervel\Sentry\Features\LogFeature; use Hypervel\Support\Facades\Log; @@ -26,7 +27,7 @@ class LogFeatureTest extends SentryTestCase ], ]; - protected function defineEnvironment($app): void + protected function defineEnvironment(ApplicationContract $app): void { parent::defineEnvironment($app); diff --git a/tests/Testbench/Concerns/CreatesApplicationTest.php b/tests/Testbench/Concerns/CreatesApplicationTest.php index d95b0b3f7..40dd7896e 100644 --- a/tests/Testbench/Concerns/CreatesApplicationTest.php +++ b/tests/Testbench/Concerns/CreatesApplicationTest.php @@ -4,6 +4,7 @@ namespace Hypervel\Tests\Testbench\Concerns; +use Hypervel\Foundation\Contracts\Application as ApplicationContract; use Hypervel\Testbench\TestCase; /** @@ -14,14 +15,14 @@ class CreatesApplicationTest extends TestCase { protected array $registeredProviders = []; - protected function getPackageProviders($app): array + protected function getPackageProviders(ApplicationContract $app): array { return [ TestServiceProvider::class, ]; } - protected function getPackageAliases($app): array + protected function getPackageAliases(ApplicationContract $app): array { return [ 'TestAlias' => TestFacade::class, diff --git a/tests/Testbench/Concerns/HandlesRoutesTest.php b/tests/Testbench/Concerns/HandlesRoutesTest.php index 2cbee9d19..e8b98070a 100644 --- a/tests/Testbench/Concerns/HandlesRoutesTest.php +++ b/tests/Testbench/Concerns/HandlesRoutesTest.php @@ -20,14 +20,14 @@ class HandlesRoutesTest extends TestCase protected bool $defineWebRoutesCalled = false; - protected function defineRoutes($router): void + protected function defineRoutes(Router $router): void { $this->defineRoutesCalled = true; $router->get('/api/test', fn () => 'api_response'); } - protected function defineWebRoutes($router): void + protected function defineWebRoutes(Router $router): void { $this->defineWebRoutesCalled = true; diff --git a/tests/Testbench/Concerns/HandlesRoutesWithoutWebRoutesTest.php b/tests/Testbench/Concerns/HandlesRoutesWithoutWebRoutesTest.php index fc2c9bddc..bd86e9002 100644 --- a/tests/Testbench/Concerns/HandlesRoutesWithoutWebRoutesTest.php +++ b/tests/Testbench/Concerns/HandlesRoutesWithoutWebRoutesTest.php @@ -5,6 +5,7 @@ namespace Hypervel\Tests\Testbench\Concerns; use Hypervel\Foundation\Testing\Concerns\RunTestsInCoroutine; +use Hypervel\Router\Router; use Hypervel\Testbench\TestCase; /** @@ -17,7 +18,7 @@ class HandlesRoutesWithoutWebRoutesTest extends TestCase { use RunTestsInCoroutine; - protected function defineRoutes($router): void + protected function defineRoutes(Router $router): void { $router->get('/only-api', fn () => 'only_api_response'); } diff --git a/tests/Testbench/TestCaseTest.php b/tests/Testbench/TestCaseTest.php index 9b9ec5e47..1d3ea02a8 100644 --- a/tests/Testbench/TestCaseTest.php +++ b/tests/Testbench/TestCaseTest.php @@ -4,6 +4,7 @@ namespace Hypervel\Tests\Testbench; +use Hypervel\Foundation\Contracts\Application as ApplicationContract; use Hypervel\Foundation\Testing\Attributes\WithConfig; use Hypervel\Foundation\Testing\Concerns\HandlesAttributes; use Hypervel\Foundation\Testing\Concerns\InteractsWithTestCase; @@ -21,7 +22,7 @@ class TestCaseTest extends TestCase { protected bool $defineEnvironmentCalled = false; - protected function defineEnvironment($app): void + protected function defineEnvironment(ApplicationContract $app): void { parent::defineEnvironment($app);