Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 12 additions & 13 deletions src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,41 +15,40 @@
use Testo\Test\Dto\Status;
use Testo\Test\Runner\SuiteRunner;
use Testo\Test\SuiteProvider;
use Testo\Test\TestCaseFactory;
use Testo\Test\Factory;

final class Application
{
private function __construct(
private readonly ObjectContainer $container,
?TestCaseFactory $testCaseFactory = null,
) {
$config = $container->get(ApplicationConfig::class);
$this->container->set($config);
$this->applyPlugins($config->plugins);

$this->container->set(
service: $testCaseFactory ?? new Factory\ReflectionFactory(),
id: TestCaseFactory::class,
);
}

/**
* Create the application instance with the provided configuration.
*/
public static function createFromConfig(
ApplicationConfig $config,
?TestCaseFactory $testCaseFactory = null,
): self {
$container = new ObjectContainer();
$container->set($config);
return new self($container);
return new self($container, $testCaseFactory);
}

/**
* Create the application instance from ENV, CLI arguments, and config file.
*
* @param Path|null $configFile Path to config file
* @param array<string, mixed> $inputOptions Command-line options
* @param array<string, mixed> $inputArguments Command-line arguments
* @param array<string, string> $environment Environment variables
*/
public static function createFromInput(
?Path $configFile = null,
array $inputOptions = [],
array $inputArguments = [],
array $environment = [],
?TestCaseFactory $testCaseFactory = null,
): self {
$container = new ObjectContainer();
$args = [
Expand Down Expand Up @@ -77,7 +76,7 @@ public static function createFromInput(
$container->addInflector($container->make(ConfigInflector::class, $args));
$container->bind(Filter::class, Filter::fromScope(...));

return new self($container);
return new self($container, $testCaseFactory);
}

public function run(?Filter $filter = null): RunResult
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,24 @@
use Testo\Interceptor\TestCaseRunInterceptor;
use Testo\Test\Dto\CaseInfo;
use Testo\Test\Dto\CaseResult;
use Testo\Test\TestCaseFactory;

/**
* Instantiate the test case class if not already instantiated.
*/
final class InstantiateTestCase implements TestCaseRunInterceptor
{
public function __construct(
private readonly TestCaseFactory $factory,
) {}

#[\Override]
public function runTestCase(CaseInfo $info, callable $next): CaseResult
{
if ($info->instance === null && $info->definition->reflection !== null) {
try {
# TODO don't instantiate if the test method is static
$instance = $info->definition->reflection->newInstance();
$instance = $this->factory->create($info->definition->reflection);
} catch (\Throwable $e) {
throw new TestCaseInstantiationException(previous: $e);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Module/Interceptor/InterceptorProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ public function fromConfig(string $class): array
FilterInterceptor::class,
new FilePostfixTestLocatorInterceptor(),
new TestoAttributesLocatorInterceptor(),
new InstantiateTestCase(),
InstantiateTestCase::class,
new AssertCollectorInterceptor(),
TestInlineFinder::class,
AttributesInterceptor::class,
Expand Down
79 changes: 79 additions & 0 deletions src/Test/Factory/CallableFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

namespace Testo\Test\Factory;

use Testo\Test\TestCaseFactory;

/**
* Factory that uses a custom callable for object creation.
*
* Provides maximum flexibility by delegating creation to a user-defined closure.
* This allows implementing custom creation logic such as:
* - Object pooling or caching
* - Conditional instantiation based on class attributes
* - Mock/stub injection for specific tests
* - Integration with custom DI systems
* - Lazy initialization patterns
* - Special constructor logic or factory methods
*
* The callable receives both the class reflection and constructor arguments,
* giving full control over the instantiation process.
*
* Example - Simple factory:
* ```php
* $factory = new CallableFactory(
* fn(\ReflectionClass $class, array $args) => $class->newInstanceArgs($args)
* );
* ```
*
* Example - Conditional mock injection:
* ```php
* $factory = new CallableFactory(
* function(\ReflectionClass $class, array $args) {
* if ($class->implementsInterface(RequiresDatabaseInterface::class)) {
* return new $class->name($mockDatabase, ...$args);
* }
* return $class->newInstanceArgs($args);
* }
* );
* ```
*
* Example - Object pooling:
* ```php
* $pool = [];
* $factory = new CallableFactory(
* function(\ReflectionClass $class, array $args) use (&$pool) {
* $key = $class->getName();
* return $pool[$key] ??= $class->newInstanceArgs($args);
* }
* );
* ```
*
* Example - Integration with custom factory:
* ```php
* $customFactory = new TestCaseFactory();
* $factory = new CallableFactory(
* fn(\ReflectionClass $class) => $customFactory->create($class->getName())
* );
* ```
*
* @template T of object
* @implements TestCaseFactory<T>
*/
final class CallableFactory implements TestCaseFactory
{
/**
* @param \Closure(\ReflectionClass<T>, array<array-key, mixed>): T $callable
*/
public function __construct(
private readonly \Closure $callable,
) {}

#[\Override]
public function create(\ReflectionClass $class, array $args = []): object
{
return ($this->callable)($class, $args);
}
}
60 changes: 60 additions & 0 deletions src/Test/Factory/ContainerFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace Testo\Test\Factory;

use Psr\Container\ContainerInterface;
use Testo\Test\TestCaseFactory;

/**
* Factory that delegates object creation to a PSR-11 dependency injection container.
*
* Example:
* ```php
* // Setup container with test dependencies
* $container = new Container();
* $container->set(DatabaseConnection::class, $db);
* $container->set(UserRepository::class, fn() => new UserRepository($db));
*
* $factory = new ContainerFactory($container);
*
* // Test class constructor receives injected dependencies
* class UserTest {
* public function __construct(
* private UserRepository $users,
* private DatabaseConnection $db
* ) {}
* }
*
* $test = $factory->create(new \ReflectionClass(UserTest::class));
* // UserTest is created with injected UserRepository and DatabaseConnection
* ```
*
* @template T of object
* @implements TestCaseFactory<T>
*/
final class ContainerFactory implements TestCaseFactory
{
/**
* @param ContainerInterface $container PSR-11 container for resolving dependencies
*/
public function __construct(
private readonly ContainerInterface $container,
) {}

/**
* @param \ReflectionClass<T> $class
* @param array<array-key, mixed> $args
*
* @return T
*
* @throws \Psr\Container\NotFoundExceptionInterface If the class is not found in the container
* @throws \Psr\Container\ContainerExceptionInterface If the container encounters an error during instantiation
*/
#[\Override]
public function create(\ReflectionClass $class, array $args = []): object
{
return $this->container->get($class->getName());
}
}
54 changes: 54 additions & 0 deletions src/Test/Factory/ReflectionFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace Testo\Test\Factory;

use Testo\Test\TestCaseFactory;

/**
* Basic factory using PHP reflection for object creation.
*
* Example:
* ```php
* $factory = new ReflectionFactory();
*
* // Simple class with no dependencies
* $test = $factory->create(new \ReflectionClass(SimpleTest::class));
*
* // Class with constructor parameters
* $test = $factory->create(
* new \ReflectionClass(ParameterizedTest::class),
* ['param1', 42]
* );
* ```
*
* @template T of object
* @implements TestCaseFactory<T>
*/
final class ReflectionFactory implements TestCaseFactory
{
/**
* Creates an instance using reflection's newInstance method.
*
* @param \ReflectionClass<T> $class Reflection of the class to instantiate
* @param array<array-key, mixed> $args Constructor arguments in the order expected by the constructor
*
* @return T New instance of the class
*
* @throws \ReflectionException If the class cannot be instantiated:
* - Class is abstract or an interface
* - Constructor is not accessible
* - Required constructor parameters are missing
* - Constructor throws an exception
*/
#[\Override]
public function create(\ReflectionClass $class, array $args = []): object
{
if ($class->hasMethod('__construct') && $class->getMethod('__construct')->isPublic()) {
return $class->newInstanceArgs($args);
}

return $class->newInstanceWithoutConstructor();
}
}
38 changes: 38 additions & 0 deletions src/Test/TestCaseFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace Testo\Test;

/**
* Factory interface for creating test case class instances.
*
* Factories provide a flexible way to create test case instances, supporting
* different creation strategies such as reflection-based creation, dependency
* injection containers, or custom factory functions.
*
* Common use cases:
* - Simple creation via reflection ({@see Factory\ReflectionFactory})
* - DI container integration ({@see Factory\ContainerFactory})
* - Custom factory logic ({@see Factory\CallableFactory})
*
* Example usage:
* ```php
* $factory = new ReflectionFactory();
* $testCase = $factory->create(new \ReflectionClass(MyTest::class));
* ```
*
* @template T of object
*/
interface TestCaseFactory
{
/**
* Creates an instance of the specified test case class.
*
* @param \ReflectionClass<T> $class Reflection of the class to create
* @param array<array-key, mixed> $args Optional constructor arguments (implementation-specific)
*
* @return T Instance of the requested class
*/
public function create(\ReflectionClass $class, array $args = []): object;
}
10 changes: 10 additions & 0 deletions tests/Fixture/Factory/AbstractTestCase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Tests\Fixture\Factory;

abstract class AbstractTestCase
{
public function testSomething(): void {}
}
12 changes: 12 additions & 0 deletions tests/Fixture/Factory/Dependency.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Tests\Fixture\Factory;

final class Dependency
{
public function __construct(
public readonly string $status = 'initialized',
) {}
}
10 changes: 10 additions & 0 deletions tests/Fixture/Factory/SimpleTestCase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Tests\Fixture\Factory;

final class SimpleTestCase
{
public string $status = 'created';
}
13 changes: 13 additions & 0 deletions tests/Fixture/Factory/TestCaseWithConstructor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Tests\Fixture\Factory;

final class TestCaseWithConstructor
{
public function __construct(
public readonly string $name,
public readonly int $value,
) {}
}
12 changes: 12 additions & 0 deletions tests/Fixture/Factory/TestCaseWithDependencies.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Tests\Fixture\Factory;

final class TestCaseWithDependencies
{
public function __construct(
public readonly Dependency $dependency,
) {}
}
Loading
Loading