├── docker-compose.yml ├── Dockerfile ├── src ├── Exceptions │ ├── RuntimeException.php │ └── InvalidArgumentException.php ├── Generators │ ├── Tests │ │ ├── Laravel │ │ │ ├── Listener │ │ │ │ └── ListenerTestGenerator.php │ │ │ ├── UnitClassFactory.php │ │ │ ├── FeatureClassFactory.php │ │ │ ├── LaravelTestGenerator.php │ │ │ ├── Concerns │ │ │ │ ├── HasInstanceBinding.php │ │ │ │ └── UsesUserModel.php │ │ │ ├── Job │ │ │ │ ├── JobTestGenerator.php │ │ │ │ └── JobMethodFactory.php │ │ │ ├── Rule │ │ │ │ ├── RuleTestGenerator.php │ │ │ │ └── RuleMethodFactory.php │ │ │ ├── Channel │ │ │ │ ├── ChannelTestGenerator.php │ │ │ │ └── ChannelMethodFactory.php │ │ │ ├── Resource │ │ │ │ ├── ResourceTestGenerator.php │ │ │ │ └── ResourceMethodFactory.php │ │ │ ├── Controller │ │ │ │ ├── ControllerTestGenerator.php │ │ │ │ └── ControllerMethodFactory.php │ │ │ ├── Command │ │ │ │ ├── CommandTestGenerator.php │ │ │ │ └── CommandMethodFactory.php │ │ │ └── Policy │ │ │ │ ├── PolicyTestGenerator.php │ │ │ │ └── PolicyMethodFactory.php │ │ ├── Concerns │ │ │ ├── ChecksMethods.php │ │ │ └── MocksParameters.php │ │ ├── Basic │ │ │ ├── BasicTestGenerator.php │ │ │ ├── ManagesGetterAndSetter.php │ │ │ └── BasicMethodFactory.php │ │ └── DelegateTestGenerator.php │ ├── Mocks │ │ ├── PhpUnitMockGenerator.php │ │ └── MockeryMockGenerator.php │ ├── Concerns │ │ └── InstantiatesClass.php │ └── Factories │ │ ├── ImportFactory.php │ │ ├── TypeFactory.php │ │ ├── ValueFactory.php │ │ ├── ClassFactory.php │ │ ├── DocumentationFactory.php │ │ ├── StatementFactory.php │ │ ├── PropertyFactory.php │ │ └── MethodFactory.php ├── Contracts │ ├── Generators │ │ ├── DelegateTestGenerator.php │ │ ├── Factories │ │ │ ├── ImportFactory.php │ │ │ ├── ValueFactory.php │ │ │ ├── ClassFactory.php │ │ │ ├── TypeFactory.php │ │ │ ├── DocumentationFactory.php │ │ │ ├── PropertyFactory.php │ │ │ ├── StatementFactory.php │ │ │ └── MethodFactory.php │ │ ├── MockGenerator.php │ │ └── TestGenerator.php │ ├── Parsers │ │ ├── Source.php │ │ └── CodeParser.php │ ├── Renderers │ │ ├── Rendered.php │ │ ├── Renderable.php │ │ └── Renderer.php │ ├── Aware │ │ ├── ConfigAware.php │ │ ├── TypeFactoryAware.php │ │ ├── ClassFactoryAware.php │ │ ├── MockGeneratorAware.php │ │ ├── TestGeneratorAware.php │ │ ├── ValueFactoryAware.php │ │ ├── ImportFactoryAware.php │ │ ├── MethodFactoryAware.php │ │ ├── PropertyFactoryAware.php │ │ ├── StatementFactoryAware.php │ │ └── DocumentationFactoryAware.php │ └── Config │ │ └── Config.php ├── Models │ ├── Concerns │ │ ├── HasType.php │ │ ├── HasTestClassParent.php │ │ ├── HasTestMethodParent.php │ │ ├── HasTestDocumentation.php │ │ └── HasLines.php │ ├── TestDocumentation.php │ ├── TestStatement.php │ ├── TestTrait.php │ ├── TestProperty.php │ ├── TestParameter.php │ ├── TestProvider.php │ ├── TestImport.php │ ├── TestMethod.php │ └── TestClass.php ├── Parsers │ ├── Sources │ │ ├── StringSource.php │ │ └── LocalFileSource.php │ └── CodeParser.php ├── Aware │ ├── ConfigAwareTrait.php │ ├── TypeFactoryAwareTrait.php │ ├── ClassFactoryAwareTrait.php │ ├── ValueFactoryAwareTrait.php │ ├── MockGeneratorAwareTrait.php │ ├── TestGeneratorAwareTrait.php │ ├── ImportFactoryAwareTrait.php │ ├── MethodFactoryAwareTrait.php │ ├── PropertyFactoryAwareTrait.php │ ├── StatementFactoryAwareTrait.php │ └── DocumentationFactoryAwareTrait.php ├── Renderers │ ├── RenderedString.php │ └── RenderedLine.php ├── Container │ ├── CoreContainerFactory.php │ └── ReflectionServiceProvider.php ├── CoreApplication.php ├── Reflection │ └── ReflectionType.php ├── Helpers │ ├── Str.php │ └── Reflect.php └── Config │ └── Config.php ├── LICENSE ├── composer.json ├── Makefile ├── CHANGELOG.md ├── README.md └── config └── phpunitgen.php /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | php: 3 | container_name: php 4 | build: . 5 | working_dir: /var/www 6 | volumes: 7 | - ./:/var/www 8 | tty: true 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.2-cli 2 | 3 | RUN apt-get update && apt-get install -y zlib1g-dev libzip-dev unzip 4 | RUN docker-php-ext-install zip 5 | 6 | RUN pecl install xdebug-3.2.1 7 | RUN docker-php-ext-enable xdebug 8 | 9 | COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer 10 | -------------------------------------------------------------------------------- /src/Exceptions/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | class RuntimeException extends BaseRuntimeException 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | class InvalidArgumentException extends BaseInvalidArgumentException 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /src/Generators/Tests/Laravel/Listener/ListenerTestGenerator.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | class ListenerTestGenerator extends JobTestGenerator 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /src/Contracts/Generators/DelegateTestGenerator.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | interface Source 17 | { 18 | /** 19 | * Get the source code as a string. 20 | * 21 | * @return string 22 | */ 23 | public function toString(): string; 24 | } 25 | -------------------------------------------------------------------------------- /src/Contracts/Renderers/Rendered.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | interface Rendered 17 | { 18 | /** 19 | * Get the rendered source code as a string. 20 | * 21 | * @return string 22 | */ 23 | public function toString(): string; 24 | } 25 | -------------------------------------------------------------------------------- /src/Generators/Tests/Laravel/UnitClassFactory.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | class UnitClassFactory extends ClassFactory 17 | { 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function getTestSubNamespace(): string 22 | { 23 | return '\\Unit'; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Contracts/Aware/ConfigAware.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | interface ConfigAware 17 | { 18 | /** 19 | * @return Config 20 | */ 21 | public function getConfig(): Config; 22 | 23 | /** 24 | * @param Config $config 25 | */ 26 | public function setConfig(Config $config): void; 27 | } 28 | -------------------------------------------------------------------------------- /src/Generators/Tests/Laravel/FeatureClassFactory.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | class FeatureClassFactory extends ClassFactory 17 | { 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function getTestSubNamespace(): string 22 | { 23 | return '\\Feature'; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Contracts/Aware/TypeFactoryAware.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | interface TypeFactoryAware 17 | { 18 | /** 19 | * @return TypeFactory 20 | */ 21 | public function getTypeFactory(): TypeFactory; 22 | 23 | /** 24 | * @param TypeFactory $config 25 | */ 26 | public function setTypeFactory(TypeFactory $config): void; 27 | } 28 | -------------------------------------------------------------------------------- /src/Contracts/Aware/ClassFactoryAware.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | interface ClassFactoryAware 17 | { 18 | /** 19 | * @return ClassFactory 20 | */ 21 | public function getClassFactory(): ClassFactory; 22 | 23 | /** 24 | * @param ClassFactory $config 25 | */ 26 | public function setClassFactory(ClassFactory $config): void; 27 | } 28 | -------------------------------------------------------------------------------- /src/Contracts/Aware/MockGeneratorAware.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | interface MockGeneratorAware 17 | { 18 | /** 19 | * @return MockGenerator 20 | */ 21 | public function getMockGenerator(): MockGenerator; 22 | 23 | /** 24 | * @param MockGenerator $config 25 | */ 26 | public function setMockGenerator(MockGenerator $config): void; 27 | } 28 | -------------------------------------------------------------------------------- /src/Contracts/Aware/TestGeneratorAware.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | interface TestGeneratorAware 17 | { 18 | /** 19 | * @return TestGenerator 20 | */ 21 | public function getTestGenerator(): TestGenerator; 22 | 23 | /** 24 | * @param TestGenerator $config 25 | */ 26 | public function setTestGenerator(TestGenerator $config): void; 27 | } 28 | -------------------------------------------------------------------------------- /src/Contracts/Aware/ValueFactoryAware.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | interface ValueFactoryAware 17 | { 18 | /** 19 | * @return ValueFactory 20 | */ 21 | public function getValueFactory(): ValueFactory; 22 | 23 | /** 24 | * @param ValueFactory $config 25 | */ 26 | public function setValueFactory(ValueFactory $config): void; 27 | } 28 | -------------------------------------------------------------------------------- /src/Contracts/Renderers/Renderable.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | interface Renderable 17 | { 18 | /** 19 | * Accept a renderer to visit render this object. 20 | * 21 | * @param Renderer $renderer 22 | * 23 | * @return Renderer 24 | */ 25 | public function accept(Renderer $renderer): Renderer; 26 | } 27 | -------------------------------------------------------------------------------- /src/Contracts/Aware/ImportFactoryAware.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | interface ImportFactoryAware 17 | { 18 | /** 19 | * @return ImportFactory 20 | */ 21 | public function getImportFactory(): ImportFactory; 22 | 23 | /** 24 | * @param ImportFactory $config 25 | */ 26 | public function setImportFactory(ImportFactory $config): void; 27 | } 28 | -------------------------------------------------------------------------------- /src/Contracts/Aware/MethodFactoryAware.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | interface MethodFactoryAware 17 | { 18 | /** 19 | * @return MethodFactory 20 | */ 21 | public function getMethodFactory(): MethodFactory; 22 | 23 | /** 24 | * @param MethodFactory $config 25 | */ 26 | public function setMethodFactory(MethodFactory $config): void; 27 | } 28 | -------------------------------------------------------------------------------- /src/Contracts/Aware/PropertyFactoryAware.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | interface PropertyFactoryAware 17 | { 18 | /** 19 | * @return PropertyFactory 20 | */ 21 | public function getPropertyFactory(): PropertyFactory; 22 | 23 | /** 24 | * @param PropertyFactory $config 25 | */ 26 | public function setPropertyFactory(PropertyFactory $config): void; 27 | } 28 | -------------------------------------------------------------------------------- /src/Contracts/Aware/StatementFactoryAware.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | interface StatementFactoryAware 17 | { 18 | /** 19 | * @return StatementFactory 20 | */ 21 | public function getStatementFactory(): StatementFactory; 22 | 23 | /** 24 | * @param StatementFactory $config 25 | */ 26 | public function setStatementFactory(StatementFactory $config): void; 27 | } 28 | -------------------------------------------------------------------------------- /src/Contracts/Aware/DocumentationFactoryAware.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | interface DocumentationFactoryAware 17 | { 18 | /** 19 | * @return DocumentationFactory 20 | */ 21 | public function getDocumentationFactory(): DocumentationFactory; 22 | 23 | /** 24 | * @param DocumentationFactory $config 25 | */ 26 | public function setDocumentationFactory(DocumentationFactory $config): void; 27 | } 28 | -------------------------------------------------------------------------------- /src/Contracts/Generators/Factories/ImportFactory.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Killian Hascoët 17 | * @license MIT 18 | */ 19 | interface ImportFactory 20 | { 21 | /** 22 | * Create an import for the given type and add it to the given class if not already added. 23 | * 24 | * @param TestClass $class 25 | * @param string $type 26 | * 27 | * @return TestImport 28 | */ 29 | public function make(TestClass $class, string $type): TestImport; 30 | } 31 | -------------------------------------------------------------------------------- /src/Contracts/Generators/Factories/ValueFactory.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Killian Hascoët 17 | * @license MIT 18 | */ 19 | interface ValueFactory 20 | { 21 | /** 22 | * Generate a PHP value for the given type. 23 | * 24 | * @param TestClass $class 25 | * @param ReflectionType|null $reflectionType 26 | * 27 | * @return string 28 | */ 29 | public function make(TestClass $class, ?ReflectionType $reflectionType): string; 30 | } 31 | -------------------------------------------------------------------------------- /src/Generators/Tests/Laravel/LaravelTestGenerator.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Killian Hascoët 15 | * @license MIT 16 | */ 17 | class LaravelTestGenerator extends BasicTestGenerator 18 | { 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public static function implementations(): array 23 | { 24 | return array_merge(parent::implementations(), [ 25 | ClassFactoryContract::class => UnitClassFactory::class, 26 | ]); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Contracts/Parsers/CodeParser.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Killian Hascoët 17 | * @license MIT 18 | */ 19 | interface CodeParser 20 | { 21 | /** 22 | * Parse a code source and build the ReflectionClass from it. 23 | * 24 | * @param Source $source 25 | * 26 | * @return ReflectionClass 27 | * 28 | * @throws InvalidArgumentException 29 | */ 30 | public function parse(Source $source): ReflectionClass; 31 | } 32 | -------------------------------------------------------------------------------- /src/Models/Concerns/HasType.php: -------------------------------------------------------------------------------- 1 | 11 | * @author Killian Hascoët 12 | * @license MIT 13 | */ 14 | trait HasType 15 | { 16 | /** 17 | * @var string|null The string type (might be used in documentation or PHP code). 18 | */ 19 | protected $type; 20 | 21 | /** 22 | * @return string|null 23 | */ 24 | public function getType(): ?string 25 | { 26 | return $this->type; 27 | } 28 | 29 | /** 30 | * @param string|null $type 31 | * 32 | * @return static 33 | */ 34 | public function setType(?string $type): self 35 | { 36 | $this->type = $type; 37 | 38 | return $this; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Parsers/Sources/StringSource.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | class StringSource implements Source 17 | { 18 | /** 19 | * @var string The source code. 20 | */ 21 | protected $code; 22 | 23 | /** 24 | * StringSource constructor. 25 | * 26 | * @param string $code 27 | */ 28 | public function __construct(string $code) 29 | { 30 | $this->code = $code; 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function toString(): string 37 | { 38 | return $this->code; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Aware/ConfigAwareTrait.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Killian Hascoët 17 | * @license MIT 18 | */ 19 | trait ConfigAwareTrait 20 | { 21 | /** 22 | * @var Config 23 | */ 24 | protected $config; 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function setConfig(Config $config): void 30 | { 31 | $this->config = $config; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function getConfig(): Config 38 | { 39 | return $this->config; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Renderers/RenderedString.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | class RenderedString implements Rendered 17 | { 18 | /** 19 | * @var string The rendered source code as string. 20 | */ 21 | protected $rendered; 22 | 23 | /** 24 | * RenderedString constructor. 25 | * 26 | * @param string $rendered 27 | */ 28 | public function __construct(string $rendered) 29 | { 30 | $this->rendered = $rendered; 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function toString(): string 37 | { 38 | return $this->rendered; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Container/CoreContainerFactory.php: -------------------------------------------------------------------------------- 1 | 15 | * @author Killian Hascoët 16 | * @license MIT 17 | */ 18 | class CoreContainerFactory 19 | { 20 | /** 21 | * Make a container for the given configuration. 22 | * 23 | * @param Config $config 24 | * 25 | * @return ContainerInterface 26 | */ 27 | public static function make(Config $config): ContainerInterface 28 | { 29 | $container = new Container(); 30 | $container->addServiceProvider( 31 | new CoreServiceProvider($config) 32 | ); 33 | 34 | return $container; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Models/Concerns/HasTestClassParent.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | trait HasTestClassParent 17 | { 18 | /** 19 | * @var TestClass The parent test class. 20 | */ 21 | protected $testClass; 22 | 23 | /** 24 | * @return TestClass 25 | */ 26 | public function getTestClass(): TestClass 27 | { 28 | return $this->testClass; 29 | } 30 | 31 | /** 32 | * @param TestClass $testClass 33 | * 34 | * @return static 35 | */ 36 | public function setTestClass(TestClass $testClass): self 37 | { 38 | $this->testClass = $testClass; 39 | 40 | return $this; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Generators/Tests/Concerns/ChecksMethods.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | trait ChecksMethods 17 | { 18 | /** 19 | * Check if the given method corresponds to the given criteria. 20 | * 21 | * @param ReflectionMethod $reflectionMethod 22 | * @param string $name 23 | * @param bool $static 24 | * 25 | * @return bool 26 | */ 27 | protected function isMethod(ReflectionMethod $reflectionMethod, string $name, bool $static = false): bool 28 | { 29 | return $reflectionMethod->isStatic() === $static 30 | && $reflectionMethod->getShortName() === $name; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Models/Concerns/HasTestMethodParent.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | trait HasTestMethodParent 17 | { 18 | /** 19 | * @var TestMethod The parent test method. 20 | */ 21 | protected $testMethod; 22 | 23 | /** 24 | * @return TestMethod 25 | */ 26 | public function getTestMethod(): TestMethod 27 | { 28 | return $this->testMethod; 29 | } 30 | 31 | /** 32 | * @param TestMethod $testMethod 33 | * 34 | * @return static 35 | */ 36 | public function setTestMethod(TestMethod $testMethod): self 37 | { 38 | $this->testMethod = $testMethod; 39 | 40 | return $this; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Aware/TypeFactoryAwareTrait.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Killian Hascoët 17 | * @license MIT 18 | */ 19 | trait TypeFactoryAwareTrait 20 | { 21 | /** 22 | * @var TypeFactory 23 | */ 24 | protected $typeFactory; 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function setTypeFactory(TypeFactory $typeFactory): void 30 | { 31 | $this->typeFactory = $typeFactory; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function getTypeFactory(): TypeFactory 38 | { 39 | return $this->typeFactory; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Aware/ClassFactoryAwareTrait.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Killian Hascoët 17 | * @license MIT 18 | */ 19 | trait ClassFactoryAwareTrait 20 | { 21 | /** 22 | * @var ClassFactory 23 | */ 24 | protected $classFactory; 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function setClassFactory(ClassFactory $classFactory): void 30 | { 31 | $this->classFactory = $classFactory; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function getClassFactory(): ClassFactory 38 | { 39 | return $this->classFactory; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Aware/ValueFactoryAwareTrait.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Killian Hascoët 17 | * @license MIT 18 | */ 19 | trait ValueFactoryAwareTrait 20 | { 21 | /** 22 | * @var ValueFactory 23 | */ 24 | protected $valueFactory; 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function setValueFactory(ValueFactory $valueFactory): void 30 | { 31 | $this->valueFactory = $valueFactory; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function getValueFactory(): ValueFactory 38 | { 39 | return $this->valueFactory; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Aware/MockGeneratorAwareTrait.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Killian Hascoët 17 | * @license MIT 18 | */ 19 | trait MockGeneratorAwareTrait 20 | { 21 | /** 22 | * @var MockGenerator 23 | */ 24 | protected $mockGenerator; 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function setMockGenerator(MockGenerator $mockGenerator): void 30 | { 31 | $this->mockGenerator = $mockGenerator; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function getMockGenerator(): MockGenerator 38 | { 39 | return $this->mockGenerator; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Aware/TestGeneratorAwareTrait.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Killian Hascoët 17 | * @license MIT 18 | */ 19 | trait TestGeneratorAwareTrait 20 | { 21 | /** 22 | * @var TestGenerator 23 | */ 24 | protected $testGenerator; 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function setTestGenerator(TestGenerator $testGenerator): void 30 | { 31 | $this->testGenerator = $testGenerator; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function getTestGenerator(): TestGenerator 38 | { 39 | return $this->testGenerator; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Aware/ImportFactoryAwareTrait.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Killian Hascoët 17 | * @license MIT 18 | */ 19 | trait ImportFactoryAwareTrait 20 | { 21 | /** 22 | * @var ImportFactory 23 | */ 24 | protected $importFactory; 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function setImportFactory(ImportFactory $importFactory): void 30 | { 31 | $this->importFactory = $importFactory; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function getImportFactory(): ImportFactory 38 | { 39 | return $this->importFactory; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Aware/MethodFactoryAwareTrait.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Killian Hascoët 17 | * @license MIT 18 | */ 19 | trait MethodFactoryAwareTrait 20 | { 21 | /** 22 | * @var MethodFactory 23 | */ 24 | protected $methodFactory; 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function setMethodFactory(MethodFactory $importFactory): void 30 | { 31 | $this->methodFactory = $importFactory; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function getMethodFactory(): MethodFactory 38 | { 39 | return $this->methodFactory; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Models/TestDocumentation.php: -------------------------------------------------------------------------------- 1 | 15 | * @author Killian Hascoët 16 | * @license MIT 17 | */ 18 | class TestDocumentation implements Renderable 19 | { 20 | use HasLines; 21 | 22 | /** 23 | * TestDocumentation constructor. 24 | * 25 | * @param string|null $firstLine 26 | */ 27 | public function __construct(?string $firstLine = null) 28 | { 29 | $this->initializeLines($firstLine); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function accept(Renderer $renderer): Renderer 36 | { 37 | return $renderer->visitTestDocumentation($this); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Aware/PropertyFactoryAwareTrait.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Killian Hascoët 17 | * @license MIT 18 | */ 19 | trait PropertyFactoryAwareTrait 20 | { 21 | /** 22 | * @var PropertyFactory 23 | */ 24 | protected $propertyFactory; 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function setPropertyFactory(PropertyFactory $propertyFactory): void 30 | { 31 | $this->propertyFactory = $propertyFactory; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function getPropertyFactory(): PropertyFactory 38 | { 39 | return $this->propertyFactory; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Contracts/Generators/MockGenerator.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Killian Hascoët 17 | * @license MIT 18 | */ 19 | interface MockGenerator 20 | { 21 | /** 22 | * Get the mock to document a mocked property. 23 | * 24 | * @param TestClass $class 25 | * 26 | * @return TestImport 27 | */ 28 | public function getMockType(TestClass $class): TestImport; 29 | 30 | /** 31 | * Returns the mock creation for the given reflection type as a string. 32 | * 33 | * @param TestClass $class 34 | * @param string|null $type 35 | * 36 | * @return string 37 | */ 38 | public function generateMock(TestClass $class, string $type): string; 39 | } 40 | -------------------------------------------------------------------------------- /src/Aware/StatementFactoryAwareTrait.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Killian Hascoët 17 | * @license MIT 18 | */ 19 | trait StatementFactoryAwareTrait 20 | { 21 | /** 22 | * @var StatementFactory 23 | */ 24 | protected $statementFactory; 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function setStatementFactory(StatementFactory $statementFactory): void 30 | { 31 | $this->statementFactory = $statementFactory; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function getStatementFactory(): StatementFactory 38 | { 39 | return $this->statementFactory; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Models/Concerns/HasTestDocumentation.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | trait HasTestDocumentation 17 | { 18 | /** 19 | * @var TestDocumentation|null The test documentation. 20 | */ 21 | protected $documentation; 22 | 23 | /** 24 | * @return TestDocumentation|null 25 | */ 26 | public function getDocumentation(): ?TestDocumentation 27 | { 28 | return $this->documentation; 29 | } 30 | 31 | /** 32 | * @param TestDocumentation|null $documentation 33 | * 34 | * @return static 35 | */ 36 | public function setDocumentation(?TestDocumentation $documentation): self 37 | { 38 | $this->documentation = $documentation; 39 | 40 | return $this; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Models/TestStatement.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Killian Hascoët 17 | * @license MIT 18 | */ 19 | class TestStatement implements Renderable 20 | { 21 | use HasLines; 22 | use HasTestMethodParent; 23 | 24 | /** 25 | * TestStatement constructor. 26 | * 27 | * @param string|null $firstLine 28 | */ 29 | public function __construct(string $firstLine = null) 30 | { 31 | $this->initializeLines($firstLine); 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function accept(Renderer $renderer): Renderer 38 | { 39 | return $renderer->visitTestStatement($this); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Aware/DocumentationFactoryAwareTrait.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Killian Hascoët 17 | * @license MIT 18 | */ 19 | trait DocumentationFactoryAwareTrait 20 | { 21 | /** 22 | * @var DocumentationFactory 23 | */ 24 | protected $documentationFactory; 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function setDocumentationFactory(DocumentationFactory $documentationFactory): void 30 | { 31 | $this->documentationFactory = $documentationFactory; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function getDocumentationFactory(): DocumentationFactory 38 | { 39 | return $this->documentationFactory; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Paul Thébaud 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Generators/Tests/Laravel/Concerns/HasInstanceBinding.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | trait HasInstanceBinding 17 | { 18 | use InstantiatesClass; 19 | 20 | /** 21 | * Make a statement which bind the tested instance on Laravel app. 22 | * 23 | * @param ReflectionClass $reflectionClass 24 | * 25 | * @return TestStatement 26 | */ 27 | protected function makeInstanceBindingStatement(ReflectionClass $reflectionClass): TestStatement 28 | { 29 | return (new TestStatement('$this->app->instance(')) 30 | ->append($reflectionClass->getShortName()) 31 | ->append('::class, $this->') 32 | ->append($this->getPropertyName($reflectionClass)) 33 | ->append(')'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Contracts/Generators/Factories/ClassFactory.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Killian Hascoët 17 | * @license MIT 18 | */ 19 | interface ClassFactory 20 | { 21 | /** 22 | * Create an empty test class from the given reflection class. 23 | * 24 | * @param ReflectionClass $reflectionClass 25 | * 26 | * @return TestClass 27 | */ 28 | public function make(ReflectionClass $reflectionClass): TestClass; 29 | 30 | /** 31 | * Get the base namespace of a test case. 32 | * 33 | * @return string 34 | */ 35 | public function getTestBaseNamespace(): string; 36 | 37 | /** 38 | * Get the sub namespace of a test case (for example "Unit" or "Feature" on Laravel). 39 | * 40 | * @return string 41 | */ 42 | public function getTestSubNamespace(): string; 43 | } 44 | -------------------------------------------------------------------------------- /src/Parsers/Sources/LocalFileSource.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Killian Hascoët 15 | * @license MIT 16 | */ 17 | class LocalFileSource implements Source 18 | { 19 | /** 20 | * @var string The source code. 21 | */ 22 | protected $code; 23 | 24 | /** 25 | * LocalFileSource constructor. 26 | * 27 | * @param string $absolutePath 28 | */ 29 | public function __construct(string $absolutePath) 30 | { 31 | if (! is_file($absolutePath)) { 32 | throw new InvalidArgumentException( 33 | "the file at {$absolutePath} does not exists" 34 | ); 35 | } 36 | 37 | $this->code = file_get_contents($absolutePath); 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function toString(): string 44 | { 45 | return $this->code; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Models/TestTrait.php: -------------------------------------------------------------------------------- 1 | 15 | * @author Killian Hascoët 16 | * @license MIT 17 | */ 18 | class TestTrait implements Renderable 19 | { 20 | use HasTestClassParent; 21 | 22 | /** 23 | * @var string The name of the class (not including namespace). 24 | */ 25 | protected $name; 26 | 27 | /** 28 | * TestTrait constructor. 29 | * 30 | * @param string $name 31 | */ 32 | public function __construct(string $name) 33 | { 34 | $this->name = $name; 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function accept(Renderer $renderer): Renderer 41 | { 42 | return $renderer->visitTestTrait($this); 43 | } 44 | 45 | /** 46 | * @return string 47 | */ 48 | public function getName(): string 49 | { 50 | return $this->name; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Generators/Mocks/PhpUnitMockGenerator.php: -------------------------------------------------------------------------------- 1 | 19 | * @author Killian Hascoët 20 | * @license MIT 21 | */ 22 | class PhpUnitMockGenerator implements MockGenerator, ImportFactoryAware 23 | { 24 | use ImportFactoryAwareTrait; 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function getMockType(TestClass $class): TestImport 30 | { 31 | return $this->importFactory->make($class, 'PHPUnit\\Framework\\MockObject\\MockObject'); 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function generateMock(TestClass $class, string $type): string 38 | { 39 | $mockedType = $this->importFactory->make($class, $type); 40 | 41 | return "\$this->createMock({$mockedType->getFinalName()}::class)"; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Models/TestProperty.php: -------------------------------------------------------------------------------- 1 | 17 | * @author Killian Hascoët 18 | * @license MIT 19 | */ 20 | class TestProperty implements Renderable 21 | { 22 | use HasTestClassParent; 23 | use HasTestDocumentation; 24 | use HasType; 25 | 26 | /** 27 | * @var string The name of the property. 28 | */ 29 | protected $name; 30 | 31 | /** 32 | * TestProperty constructor. 33 | * 34 | * @param string $name 35 | */ 36 | public function __construct(string $name) 37 | { 38 | $this->name = $name; 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function accept(Renderer $renderer): Renderer 45 | { 46 | return $renderer->visitTestProperty($this); 47 | } 48 | 49 | /** 50 | * @return string 51 | */ 52 | public function getName(): string 53 | { 54 | return $this->name; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Generators/Tests/Laravel/Job/JobTestGenerator.php: -------------------------------------------------------------------------------- 1 | 17 | * @author Killian Hascoët 18 | * @license MIT 19 | */ 20 | class JobTestGenerator extends LaravelTestGenerator 21 | { 22 | use ChecksMethods; 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public static function implementations(): array 28 | { 29 | return array_merge(parent::implementations(), [ 30 | MethodFactoryContract::class => JobMethodFactory::class, 31 | ]); 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | protected function isTestable(TestClass $class, ReflectionMethod $reflectionMethod): bool 38 | { 39 | return $this->config->automaticGeneration() 40 | && ($this->isGetterOrSetter($reflectionMethod) || $this->isMethod($reflectionMethod, 'handle')); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Models/TestParameter.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Killian Hascoët 17 | * @license MIT 18 | */ 19 | class TestParameter implements Renderable 20 | { 21 | use HasTestMethodParent; 22 | use HasType; 23 | 24 | /** 25 | * @var string The name of the parameter. 26 | */ 27 | protected $name; 28 | 29 | /** 30 | * TestParameter constructor. 31 | * 32 | * @param string $name 33 | * @param string|null $type 34 | */ 35 | public function __construct(string $name, ?string $type = null) 36 | { 37 | $this->name = $name; 38 | $this->type = $type; 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function accept(Renderer $renderer): Renderer 45 | { 46 | return $renderer->visitTestParameter($this); 47 | } 48 | 49 | /** 50 | * @return string 51 | */ 52 | public function getName(): string 53 | { 54 | return $this->name; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpunitgen/core", 3 | "description": "The PhpUnitGen core features for tests generation.", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "test", 8 | "generation", 9 | "generator" 10 | ], 11 | "support": { 12 | "issues": "https://github.com/paul-thebaud/phpunitgen-core/issues", 13 | "source": "https://github.com/paul-thebaud/phpunitgen-core" 14 | }, 15 | "authors": [ 16 | { 17 | "name": "Paul Thébaud", 18 | "email": "paul.thebaud29@gmail.com" 19 | }, 20 | { 21 | "name": "Killian Hascoët", 22 | "email": "killianh@live.fr" 23 | } 24 | ], 25 | "require": { 26 | "php": "~8.1.0 || ~8.2.0 || ~8.3.0", 27 | "league/container": "^3.2", 28 | "phpdocumentor/reflection-docblock": "^5.2", 29 | "phpdocumentor/type-resolver": "^1.6", 30 | "roave/better-reflection": "^5.0 || ^6.0", 31 | "tightenco/collect": "^8.0 || ^9.0" 32 | }, 33 | "require-dev": { 34 | "mockery/mockery": "^1.3", 35 | "phpunit/phpunit": "^10.0" 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "PhpUnitGen\\Core\\": "src/" 40 | } 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "Tests\\PhpUnitGen\\Core\\": "tests/" 45 | } 46 | }, 47 | "config": { 48 | "sort-packages": true 49 | }, 50 | "minimum-stability": "dev", 51 | "prefer-stable": true 52 | } 53 | -------------------------------------------------------------------------------- /src/Generators/Tests/Laravel/Rule/RuleTestGenerator.php: -------------------------------------------------------------------------------- 1 | 17 | * @author Killian Hascoët 18 | * @license MIT 19 | */ 20 | class RuleTestGenerator extends LaravelTestGenerator 21 | { 22 | use ChecksMethods; 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public static function implementations(): array 28 | { 29 | return array_merge(parent::implementations(), [ 30 | MethodFactoryContract::class => RuleMethodFactory::class, 31 | ]); 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | protected function isTestable(TestClass $class, ReflectionMethod $reflectionMethod): bool 38 | { 39 | return $this->config->automaticGeneration() 40 | && ($this->isGetterOrSetter($reflectionMethod) || $this->isMethod($reflectionMethod, 'passes')); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Generators/Tests/Laravel/Channel/ChannelTestGenerator.php: -------------------------------------------------------------------------------- 1 | 17 | * @author Killian Hascoët 18 | * @license MIT 19 | */ 20 | class ChannelTestGenerator extends PolicyTestGenerator 21 | { 22 | use ChecksMethods; 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public static function implementations(): array 28 | { 29 | return array_merge(parent::implementations(), [ 30 | MethodFactoryContract::class => ChannelMethodFactory::class, 31 | ]); 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | protected function isTestable(TestClass $class, ReflectionMethod $reflectionMethod): bool 38 | { 39 | return $this->config->automaticGeneration() 40 | && ($this->isGetterOrSetter($reflectionMethod) || $this->isMethod($reflectionMethod, 'join')); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Generators/Tests/Laravel/Resource/ResourceTestGenerator.php: -------------------------------------------------------------------------------- 1 | 17 | * @author Killian Hascoët 18 | * @license MIT 19 | */ 20 | class ResourceTestGenerator extends LaravelTestGenerator 21 | { 22 | use ChecksMethods; 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public static function implementations(): array 28 | { 29 | return array_merge(parent::implementations(), [ 30 | MethodFactoryContract::class => ResourceMethodFactory::class, 31 | ]); 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | protected function isTestable(TestClass $class, ReflectionMethod $reflectionMethod): bool 38 | { 39 | return $this->config->automaticGeneration() 40 | && ($this->isGetterOrSetter($reflectionMethod) || $this->isMethod($reflectionMethod, 'toArray')); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Generators/Mocks/MockeryMockGenerator.php: -------------------------------------------------------------------------------- 1 | 19 | * @author Killian Hascoët 20 | * @license MIT 21 | */ 22 | class MockeryMockGenerator implements MockGenerator, ImportFactoryAware 23 | { 24 | use ImportFactoryAwareTrait; 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function getMockType(TestClass $class): TestImport 30 | { 31 | return $this->importFactory->make($class, 'Mockery\\Mock'); 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function generateMock(TestClass $class, string $type): string 38 | { 39 | // Mockery must be imported to mock classes. 40 | $mockeryType = $this->importFactory->make($class, 'Mockery'); 41 | $mockedType = $this->importFactory->make($class, $type); 42 | 43 | return "{$mockeryType->getFinalName()}::mock({$mockedType->getFinalName()}::class)"; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Contracts/Generators/Factories/TypeFactory.php: -------------------------------------------------------------------------------- 1 | 17 | * @author Killian Hascoët 18 | * @license MIT 19 | */ 20 | interface TypeFactory 21 | { 22 | /** 23 | * Get the type to use from the string version of type. 24 | * 25 | * @param TestClass $class 26 | * @param string $type 27 | * @param bool $isBuiltIn 28 | * 29 | * @return TestImport|string 30 | */ 31 | public function makeFromString(TestClass $class, string $type, bool $isBuiltIn): TestImport|string; 32 | 33 | /** 34 | * Format a string or TestImport type. 35 | * 36 | * @param TestImport|string $type 37 | * 38 | * @return string 39 | */ 40 | public function formatType(TestImport|string $type): string; 41 | 42 | /** 43 | * Join the given types list into a PHP style string type. 44 | * 45 | * @param Collection $types 46 | * @param string $separator 47 | * 48 | * @return string 49 | */ 50 | public function formatTypes(Collection $types, string $separator = '|'): string; 51 | } 52 | -------------------------------------------------------------------------------- /src/Generators/Tests/Basic/BasicTestGenerator.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Killian Hascoët 17 | * @license MIT 18 | */ 19 | class BasicTestGenerator extends AbstractTestGenerator 20 | { 21 | use ManagesGetterAndSetter; 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public static function implementations(): array 27 | { 28 | return array_merge(parent::implementations(), [ 29 | MethodFactoryContract::class => BasicMethodFactory::class, 30 | ]); 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | protected function isTestable(TestClass $class, ReflectionMethod $reflectionMethod): bool 37 | { 38 | return $this->config->automaticGeneration() && $this->isGetterOrSetter($reflectionMethod); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | protected function handleForTestable(TestClass $class, ReflectionMethod $reflectionMethod): void 45 | { 46 | $this->methodFactory->makeTestable($class, $reflectionMethod); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Generators/Tests/Laravel/Controller/ControllerTestGenerator.php: -------------------------------------------------------------------------------- 1 | 19 | * @author Killian Hascoët 20 | * @license MIT 21 | */ 22 | class ControllerTestGenerator extends LaravelTestGenerator 23 | { 24 | use ChecksMethods; 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public static function implementations(): array 30 | { 31 | return array_merge(parent::implementations(), [ 32 | ClassFactoryContract::class => FeatureClassFactory::class, 33 | MethodFactoryContract::class => ControllerMethodFactory::class, 34 | ]); 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | protected function isTestable(TestClass $class, ReflectionMethod $reflectionMethod): bool 41 | { 42 | return $this->config->automaticGeneration(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Generators/Concerns/InstantiatesClass.php: -------------------------------------------------------------------------------- 1 | 15 | * @author Killian Hascoët 16 | * @license MIT 17 | */ 18 | trait InstantiatesClass 19 | { 20 | /** 21 | * Retrieve the name of the class instance property. 22 | * 23 | * @param ReflectionClass $reflectionClass 24 | * 25 | * @return string 26 | */ 27 | protected function getPropertyName(ReflectionClass $reflectionClass): string 28 | { 29 | return lcfirst($reflectionClass->getShortName()); 30 | } 31 | 32 | /** 33 | * Retrieve the class constructor only if it is public and non-abstract. 34 | * 35 | * @param ReflectionClass $reflectionClass 36 | * 37 | * @return ReflectionMethod|null 38 | */ 39 | protected function getConstructor(ReflectionClass $reflectionClass): ?ReflectionMethod 40 | { 41 | $constructor = Reflect::method($reflectionClass, '__construct'); 42 | 43 | if ($constructor 44 | && $constructor->isPublic() 45 | && ! $constructor->isAbstract() 46 | && ! $constructor->isStatic() 47 | ) { 48 | return $constructor; 49 | } 50 | 51 | return null; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Contracts/Generators/Factories/DocumentationFactory.php: -------------------------------------------------------------------------------- 1 | 19 | * @author Killian Hascoët 20 | * @license MIT 21 | */ 22 | interface DocumentationFactory 23 | { 24 | /** 25 | * Create the documentation for a test class. 26 | * 27 | * @param TestClass $class 28 | * 29 | * @return TestDocumentation 30 | */ 31 | public function makeForClass(TestClass $class): TestDocumentation; 32 | 33 | /** 34 | * Create the documentation for a test class property with the given type(s). 35 | * 36 | * @param TestProperty $property 37 | * @param Collection $types 38 | * 39 | * @return TestDocumentation 40 | */ 41 | public function makeForProperty(TestProperty $property, Collection $types): TestDocumentation; 42 | 43 | /** 44 | * Create the documentation for an inherited method. 45 | * 46 | * @param TestMethod $method 47 | * 48 | * @return TestDocumentation 49 | */ 50 | public function makeForInheritedMethod(TestMethod $method): TestDocumentation; 51 | } 52 | -------------------------------------------------------------------------------- /src/Generators/Tests/Concerns/MocksParameters.php: -------------------------------------------------------------------------------- 1 | 19 | * @author Killian Hascoët 20 | * @license MIT 21 | */ 22 | trait MocksParameters 23 | { 24 | use StatementFactoryAwareTrait; 25 | use ValueFactoryAwareTrait; 26 | 27 | protected function mockParametersAndAddStatements(TestMethod $method, Collection $parameters): void 28 | { 29 | $parametersNotEmpty = $parameters 30 | ->each(function (ReflectionParameter $reflectionParameter) use ($method) { 31 | $name = $reflectionParameter->getName(); 32 | $value = $this->valueFactory->make( 33 | $method->getTestClass(), 34 | Reflect::parameterType($reflectionParameter) 35 | ); 36 | 37 | $method->addStatement( 38 | $this->statementFactory->makeAffect($name, $value, false) 39 | ); 40 | }) 41 | ->isNotEmpty(); 42 | 43 | if ($parametersNotEmpty) { 44 | $method->addStatement(new TestStatement('')); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Generators/Tests/Laravel/Command/CommandTestGenerator.php: -------------------------------------------------------------------------------- 1 | 19 | * @author Killian Hascoët 20 | * @license MIT 21 | */ 22 | class CommandTestGenerator extends LaravelTestGenerator 23 | { 24 | use ChecksMethods; 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public static function implementations(): array 30 | { 31 | return array_merge(parent::implementations(), [ 32 | ClassFactoryContract::class => FeatureClassFactory::class, 33 | MethodFactoryContract::class => CommandMethodFactory::class, 34 | ]); 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | protected function isTestable(TestClass $class, ReflectionMethod $reflectionMethod): bool 41 | { 42 | return $this->config->automaticGeneration() 43 | && ($this->isGetterOrSetter($reflectionMethod) || $this->isMethod($reflectionMethod, 'handle')); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Models/TestProvider.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Killian Hascoët 17 | * @license MIT 18 | */ 19 | class TestProvider implements Renderable 20 | { 21 | use HasTestMethodParent; 22 | use HasTestDocumentation; 23 | 24 | /** 25 | * @var string The name of the provider method. 26 | */ 27 | protected $name; 28 | 29 | /** 30 | * @var array[] The data this provider provides. 31 | */ 32 | protected $data; 33 | 34 | /** 35 | * TestProvider constructor. 36 | * 37 | * @param string $name 38 | * @param array[] $data 39 | */ 40 | public function __construct(string $name, array $data) 41 | { 42 | $this->name = $name; 43 | $this->data = $data; 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function accept(Renderer $renderer): Renderer 50 | { 51 | return $renderer->visitTestProvider($this); 52 | } 53 | 54 | /** 55 | * @return string 56 | */ 57 | public function getName(): string 58 | { 59 | return $this->name; 60 | } 61 | 62 | /** 63 | * @return array[] 64 | */ 65 | public function getData(): array 66 | { 67 | return $this->data; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Contracts/Generators/TestGenerator.php: -------------------------------------------------------------------------------- 1 | 18 | * @author Killian Hascoët 19 | * @license MIT 20 | */ 21 | interface TestGenerator 22 | { 23 | /** 24 | * Get the implementations that this generator will use (for factories). 25 | * 26 | * @return array 27 | */ 28 | public static function implementations(): array; 29 | 30 | /** 31 | * Generate the tests models for the given reflection class. 32 | * 33 | * @param ReflectionClass $reflectionClass 34 | * 35 | * @return TestClass 36 | * 37 | * @throws InvalidArgumentException 38 | */ 39 | public function generate(ReflectionClass $reflectionClass): TestClass; 40 | 41 | /** 42 | * Check if the test generator can generate models for the given reflection class. 43 | * 44 | * @param ReflectionClass $reflectionClass 45 | * 46 | * @return bool 47 | */ 48 | public function canGenerateFor(ReflectionClass $reflectionClass): bool; 49 | 50 | /** 51 | * Retrieve the class factory (used when generating path of test case on command line). 52 | * 53 | * @return ClassFactory 54 | */ 55 | public function getClassFactory(): ClassFactory; 56 | } 57 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Executables 2 | DOCKER_COMPOSE?=docker compose 3 | DOCKER_EXEC?=$(DOCKER_COMPOSE) exec -it 4 | COMPOSER?=$(DOCKER_EXEC) php composer 5 | 6 | # Misc 7 | default: help 8 | 9 | ## 10 | ## —— Setup ———————————————————————————————————————————————————————————————————— 11 | 12 | .PHONY: build 13 | build: ## Build and start containers. 14 | @$(DOCKER_COMPOSE) up --build --no-recreate -d 15 | 16 | .PHONY: rebuild 17 | rebuild: ## Force rebuild and start all containers. 18 | @$(DOCKER_COMPOSE) up --build --force-recreate --remove-orphans -d 19 | 20 | .PHONY: up 21 | up: ## Start containers without building. 22 | @$(DOCKER_COMPOSE) up -d 23 | 24 | .PHONY: stop 25 | stop: ## Stop containers. 26 | @$(DOCKER_COMPOSE) stop 27 | 28 | .PHONY: down 29 | down: ## Stop and remove containers. 30 | @$(DOCKER_COMPOSE) down --remove-orphans --timeout=2 31 | 32 | ## 33 | ## —— Executables —————————————————————————————————————————————————————————————— 34 | 35 | .PHONY: composer 36 | composer: ## Run a Composer command (e.g. make composer c="update"). 37 | @$(COMPOSER) $(c) 38 | 39 | ## 40 | ## —— Tests ———————————————————————————————————————————————————————————————————— 41 | 42 | .PHONY: test 43 | test: ## Run tests. 44 | @$(DOCKER_EXEC) php php -d xdebug.mode=coverage vendor/bin/phpunit -c phpunit.xml.dist 45 | 46 | ## 47 | ## —— Utilities ———————————————————————————————————————————————————————————————— 48 | 49 | .PHONY: sh 50 | sh: ## Run BASH on PHP container. 51 | @$(DOCKER_EXEC) php bash 52 | 53 | .PHONY: help 54 | help: ## Show help for each of the Makefile recipes. 55 | @grep -E '(^[a-zA-Z0-9\./_-]+:.*?##.*$$)|(^##)' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}{printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}' | sed -e 's/\[32m##/[33m/' 56 | -------------------------------------------------------------------------------- /src/Generators/Factories/ImportFactory.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Killian Hascoët 17 | * @license MIT 18 | */ 19 | class ImportFactory implements ImportFactoryContract 20 | { 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function make(TestClass $class, string $type): TestImport 25 | { 26 | $import = $class->getImports() 27 | ->first(function (TestImport $import) use ($type) { 28 | return $import->getName() === $type; 29 | }); 30 | 31 | if ($import) { 32 | return $import; 33 | } 34 | 35 | $shortName = Str::afterLast('\\', $type); 36 | 37 | do { 38 | $aliased = $class->getImports() 39 | ->contains(function (TestImport $import) use (&$shortName) { 40 | if ($import->getFinalName() === $shortName) { 41 | $shortName .= 'Alias'; 42 | 43 | return true; 44 | } 45 | 46 | return false; 47 | }); 48 | } while ($aliased); 49 | 50 | $alias = Str::afterLast('\\', $type) === $shortName ? null : $shortName; 51 | 52 | $import = new TestImport($type, $alias); 53 | $class->addImport($import); 54 | 55 | return $import; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 4.1.0 4 | 5 | **Added** 6 | 7 | - Add `php@8.3` support (see paul-thebaud/phpunitgen-console#13). 8 | 9 | ## 4.0.0 10 | 11 | **Added** 12 | 13 | - Add `phpHeaderDoc` option to generate a documentation block in generated file header (closes #30). 14 | 15 | **Changed** 16 | 17 | - **BREAKING** Drop support for PHP 8.0. 18 | - Change PHPUnit methods calls (`markTestIncomplete`, etc.) from instance to static calls (closes #29). 19 | - Remove `setAccessible` calls (methods are accessible by default since PHP 8.1) (closes #27). 20 | 21 | ## 3.1.0 22 | 23 | **Added** 24 | 25 | - Add compatibility with `php@8.2` and `roave/better-reflection@^6.0`. 26 | 27 | ## 3.0.0 28 | 29 | **Added** 30 | 31 | - **BREAKING** Add `testClassFinal`, `testClassStrictTypes` and `testClassTypedProperties` in `Config` interface. 32 | - **BREAKING** Add `TypeFactory` interface with default implementation. 33 | - Add `testClassFinal` option and update rendering to create final test class (see #19). 34 | - Add `testClassStrictTypes` option and update rendering to prepend strict types declaration test class (see #19). 35 | - Add `testClassTypedProperties` option and update rendering to strictly type test class properties (see #20). 36 | - Support for `tightenco/collect` version `^9.0`. 37 | 38 | **Changed** 39 | 40 | - **BREAKING** Change `makeForProperty` signature in `DocumentationFactory` interface. 41 | - Test class properties are now declared as private properties instead of protected. 42 | 43 | ## 2.x.x 44 | 45 | See the [2.x.x CHANGELOG](https://github.com/paul-thebaud/phpunitgen-core/blob/2.x.x/CHANGELOG.md). 46 | 47 | ## 1.x.x 48 | 49 | See the [1.x.x CHANGELOG](https://github.com/paul-thebaud/phpunitgen-core/blob/1.x.x/CHANGELOG.md). 50 | -------------------------------------------------------------------------------- /src/Contracts/Generators/Factories/PropertyFactory.php: -------------------------------------------------------------------------------- 1 | 17 | * @author Killian Hascoët 18 | * @license MIT 19 | */ 20 | interface PropertyFactory 21 | { 22 | /** 23 | * Create the property for the class to test instantiation. 24 | * 25 | * @param TestClass $class 26 | * 27 | * @return TestProperty 28 | */ 29 | public function makeForClass(TestClass $class): TestProperty; 30 | 31 | /** 32 | * Create the property for a class parameter. 33 | * 34 | * @param TestClass $class 35 | * @param ReflectionParameter $reflectionParameter 36 | * 37 | * @return TestProperty 38 | */ 39 | public function makeForParameter(TestClass $class, ReflectionParameter $reflectionParameter): TestProperty; 40 | 41 | /** 42 | * Create the property for a class with custom specs. 43 | * 44 | * @param TestClass $class 45 | * @param string $name 46 | * @param string $type 47 | * @param bool $isBuiltIn 48 | * @param bool $isMock 49 | * 50 | * @return TestProperty 51 | */ 52 | public function makeCustom( 53 | TestClass $class, 54 | string $name, 55 | string $type, 56 | bool $isBuiltIn = false, 57 | bool $isMock = true 58 | ): TestProperty; 59 | } 60 | -------------------------------------------------------------------------------- /src/Generators/Factories/TypeFactory.php: -------------------------------------------------------------------------------- 1 | 18 | * @author Killian Hascoët 19 | * @license MIT 20 | */ 21 | class TypeFactory implements ImportFactoryAware, TypeFactoryContract 22 | { 23 | use ImportFactoryAwareTrait; 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function makeFromString(TestClass $class, string $type, bool $isBuiltIn): TestImport|string 29 | { 30 | if (in_array($type, ['parent', 'self', 'static'])) { 31 | return $this->importFactory->make($class, $class->getReflectionClass()->getName()); 32 | } 33 | 34 | if ($isBuiltIn) { 35 | return $type; 36 | } 37 | 38 | return $this->importFactory->make($class, $type); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function formatType(string|TestImport $type): string 45 | { 46 | return is_string($type) ? $type : $type->getFinalName(); 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public function formatTypes(Collection $types, string $separator = '|'): string 53 | { 54 | return $types->map(fn (TestImport|string $t) => $this->formatType($t))->implode($separator); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Models/TestImport.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Killian Hascoët 17 | * @license MIT 18 | */ 19 | class TestImport implements Renderable 20 | { 21 | use HasTestClassParent; 22 | 23 | /** 24 | * @var string The complete name of the class (including namespace). 25 | */ 26 | protected $name; 27 | 28 | /** 29 | * @var string|null The alias of this import. 30 | */ 31 | protected $alias; 32 | 33 | /** 34 | * TestImport constructor. 35 | * 36 | * @param string $name 37 | * @param string|null $alias 38 | */ 39 | public function __construct(string $name, ?string $alias = null) 40 | { 41 | $this->name = $name; 42 | $this->alias = $alias; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function accept(Renderer $renderer): Renderer 49 | { 50 | return $renderer->visitTestImport($this); 51 | } 52 | 53 | /** 54 | * @return string 55 | */ 56 | public function getName(): string 57 | { 58 | return $this->name; 59 | } 60 | 61 | /** 62 | * @return string|null 63 | */ 64 | public function getAlias(): ?string 65 | { 66 | return $this->alias; 67 | } 68 | 69 | /** 70 | * @return string 71 | */ 72 | public function getFinalName(): string 73 | { 74 | return $this->alias ?? Str::afterLast('\\', $this->name); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Generators/Tests/Laravel/Policy/PolicyTestGenerator.php: -------------------------------------------------------------------------------- 1 | 18 | * @author Killian Hascoët 19 | * @license MIT 20 | */ 21 | class PolicyTestGenerator extends LaravelTestGenerator implements StatementFactoryAware 22 | { 23 | use UsesUserModel; 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public static function implementations(): array 29 | { 30 | return array_merge(parent::implementations(), [ 31 | MethodFactoryContract::class => PolicyMethodFactory::class, 32 | ]); 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | protected function isTestable(TestClass $class, ReflectionMethod $reflectionMethod): bool 39 | { 40 | return $this->config->automaticGeneration() 41 | && ($this->isGetterOrSetter($reflectionMethod) || ! $reflectionMethod->isStatic()); 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | protected function addProperties(TestClass $class): void 48 | { 49 | parent::addProperties($class); 50 | 51 | $class->addProperty( 52 | $this->propertyFactory->makeCustom($class, 'user', $this->getUserClassAsString(), false, false) 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Generators/Tests/Laravel/Concerns/UsesUserModel.php: -------------------------------------------------------------------------------- 1 | 18 | * @author Killian Hascoët 19 | * @license MIT 20 | */ 21 | trait UsesUserModel 22 | { 23 | use ConfigAwareTrait; 24 | use ImportFactoryAwareTrait; 25 | use StatementFactoryAwareTrait; 26 | 27 | /** 28 | * Retrieve the Laravel User model class import. 29 | * 30 | * @param TestClass $class 31 | * 32 | * @return TestImport 33 | */ 34 | protected function getUserClass(TestClass $class): TestImport 35 | { 36 | return $this->importFactory->make($class, $this->getUserClassAsString()); 37 | } 38 | 39 | /** 40 | * Get the Laravel user class as a string. 41 | * 42 | * @return string 43 | */ 44 | protected function getUserClassAsString(): string 45 | { 46 | return $this->config->getOption('laravel.user', 'App\\User'); 47 | } 48 | 49 | /** 50 | * Make the user affect statement. 51 | * 52 | * @param TestClass $class 53 | * @param TestMethod $method 54 | */ 55 | protected function makeUserAffectStatement(TestClass $class, TestMethod $method): void 56 | { 57 | $userImport = $this->getUserClass($class)->getFinalName(); 58 | $method->addStatement( 59 | $this->statementFactory->makeAffect('user', "new {$userImport}()") 60 | ); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Contracts/Generators/Factories/StatementFactory.php: -------------------------------------------------------------------------------- 1 | 17 | * @author Killian Hascoët 18 | * @license MIT 19 | */ 20 | interface StatementFactory 21 | { 22 | /** 23 | * Create a statement with a "@todo" annotation. 24 | * 25 | * @param string $todo 26 | * 27 | * @return TestStatement 28 | */ 29 | public function makeTodo(string $todo): TestStatement; 30 | 31 | /** 32 | * Create an affect statement for the given name and value. Use "$this" if it is a property. 33 | * 34 | * @param string $name 35 | * @param string $value 36 | * @param bool $isProperty 37 | * 38 | * @return TestStatement 39 | */ 40 | public function makeAffect(string $name, string $value, bool $isProperty = true): TestStatement; 41 | 42 | /** 43 | * Create a PHPUnit assert statement for the given assert type (same, true...) and parameters. 44 | * 45 | * @param string $assert 46 | * @param string[] $parameters 47 | * 48 | * @return TestStatement 49 | */ 50 | public function makeAssert(string $assert, string ...$parameters): TestStatement; 51 | 52 | /** 53 | * Create the class instantiation statement. 54 | * 55 | * @param TestClass $class 56 | * @param Collection $parameters 57 | * 58 | * @return TestStatement 59 | */ 60 | public function makeInstantiation(TestClass $class, Collection $parameters): TestStatement; 61 | } 62 | -------------------------------------------------------------------------------- /src/Renderers/RenderedLine.php: -------------------------------------------------------------------------------- 1 | 11 | * @author Killian Hascoët 12 | * @license MIT 13 | */ 14 | class RenderedLine 15 | { 16 | /** 17 | * @var int The line indentation. 18 | */ 19 | protected $indentation; 20 | 21 | /** 22 | * @var string The line content. 23 | */ 24 | protected $content; 25 | 26 | /** 27 | * RenderedLine constructor. 28 | * 29 | * @param int $indentation 30 | * @param string $content 31 | */ 32 | public function __construct(int $indentation, string $content) 33 | { 34 | $this->indentation = $indentation; 35 | $this->content = $content; 36 | } 37 | 38 | /** 39 | * @return string 40 | */ 41 | public function getContent(): string 42 | { 43 | return $this->content; 44 | } 45 | 46 | /** 47 | * Appends the given content to the current line content. 48 | * 49 | * @param string $content 50 | * 51 | * @return static 52 | */ 53 | public function prepend(string $content): self 54 | { 55 | $this->content = $content.$this->content; 56 | 57 | return $this; 58 | } 59 | 60 | /** 61 | * Appends the given content to the current line content. 62 | * 63 | * @param string $content 64 | * 65 | * @return static 66 | */ 67 | public function append(string $content): self 68 | { 69 | $this->content .= $content; 70 | 71 | return $this; 72 | } 73 | 74 | /** 75 | * Render the line as string with correct indentation. 76 | * 77 | * @return string 78 | */ 79 | public function render(): string 80 | { 81 | if ($this->content === '') { 82 | return ''; 83 | } 84 | 85 | return str_repeat(' ', $this->indentation).$this->content; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Contracts/Generators/Factories/MethodFactory.php: -------------------------------------------------------------------------------- 1 | 18 | * @author Killian Hascoët 19 | * @license MIT 20 | */ 21 | interface MethodFactory 22 | { 23 | /** 24 | * Create the "setUp" method for the class. 25 | * 26 | * @param TestClass $class 27 | * 28 | * @return TestMethod 29 | */ 30 | public function makeSetUp(TestClass $class): TestMethod; 31 | 32 | /** 33 | * Create the "tearDown" method for the class. 34 | * 35 | * @param TestClass $class 36 | * 37 | * @return TestMethod 38 | */ 39 | public function makeTearDown(TestClass $class): TestMethod; 40 | 41 | /** 42 | * Create an empty method and append suffix to its name. 43 | * 44 | * @param ReflectionMethod $reflectionMethod 45 | * @param string $suffix 46 | * 47 | * @return TestMethod 48 | */ 49 | public function makeEmpty(ReflectionMethod $reflectionMethod, string $suffix = ''): TestMethod; 50 | 51 | /** 52 | * Create an incomplete method (when it cannot have automatic generation or it is disabled). 53 | * 54 | * @param ReflectionMethod $reflectionMethod 55 | * 56 | * @return TestMethod 57 | */ 58 | public function makeIncomplete(ReflectionMethod $reflectionMethod): TestMethod; 59 | 60 | /** 61 | * Create method(s) for testable reflection method and it (or them) to the test class. 62 | * 63 | * @param TestClass $class 64 | * @param ReflectionMethod $reflectionMethod 65 | * 66 | * @throws InvalidArgumentException 67 | */ 68 | public function makeTestable(TestClass $class, ReflectionMethod $reflectionMethod): void; 69 | } 70 | -------------------------------------------------------------------------------- /src/Parsers/CodeParser.php: -------------------------------------------------------------------------------- 1 | 21 | * @author Killian Hascoët 22 | * @license MIT 23 | */ 24 | class CodeParser implements CodeParserContract 25 | { 26 | /** 27 | * @var Locator 28 | */ 29 | protected $astLocator; 30 | 31 | /** 32 | * CodeParser constructor. 33 | * 34 | * @param BetterReflection $betterReflection 35 | */ 36 | public function __construct(BetterReflection $betterReflection) 37 | { 38 | $this->astLocator = $betterReflection->astLocator(); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function parse(Source $source): ReflectionClass 45 | { 46 | $reflector = new DefaultReflector( 47 | new StringSourceLocator($source->toString(), $this->astLocator) 48 | ); 49 | 50 | try { 51 | $classes = $reflector->reflectAllClasses(); 52 | } catch (ParseToAstFailure $exception) { 53 | throw new InvalidArgumentException( 54 | 'code might have an invalid syntax because AST failed to parse it' 55 | ); 56 | } 57 | 58 | $classesCount = count($classes); 59 | if ($classesCount !== 1) { 60 | throw new InvalidArgumentException( 61 | 'code must contains exactly one class/interface/trait, found '.$classesCount 62 | ); 63 | } 64 | 65 | return $classes[0]; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Generators/Factories/ValueFactory.php: -------------------------------------------------------------------------------- 1 | 17 | * @author Killian Hascoët 18 | * @license MIT 19 | */ 20 | class ValueFactory implements ValueFactoryContract, MockGeneratorAware 21 | { 22 | use MockGeneratorAwareTrait; 23 | 24 | /** 25 | * Mapping between built in types and values. 26 | */ 27 | protected const BUILT_IN_VALUES = [ 28 | 'int' => '42', 29 | 'float' => '42.42', 30 | 'string' => '\'42\'', 31 | 'bool' => 'true', 32 | 'callable' => 'function () {}', 33 | 'array' => '[]', 34 | 'iterable' => '[]', 35 | 'object' => 'new \\stdClass()', 36 | ]; 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function make(TestClass $class, ?ReflectionType $reflectionType): string 42 | { 43 | if (! $reflectionType) { 44 | return 'null'; 45 | } 46 | 47 | $type = $reflectionType->getType(); 48 | 49 | if ($reflectionType->isBuiltin()) { 50 | return $this->createForBuiltIn($class, $type); 51 | } 52 | 53 | return $this->mockGenerator->generateMock($class, $type); 54 | } 55 | 56 | /** 57 | * Create a value for a built in type. 58 | * 59 | * @param TestClass $class 60 | * @param string $type 61 | * 62 | * @return string 63 | */ 64 | protected function createForBuiltIn(TestClass $class, string $type): string 65 | { 66 | // The built in type reference a class, so mock it. 67 | if ($type === 'self' || $type === 'parent') { 68 | return $this->mockGenerator->generateMock($class, $class->getReflectionClass()->getShortName()); 69 | } 70 | 71 | return static::BUILT_IN_VALUES[$type] ?? 'null'; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Generators/Tests/Laravel/Resource/ResourceMethodFactory.php: -------------------------------------------------------------------------------- 1 | 20 | * @author Killian Hascoët 21 | * @license MIT 22 | */ 23 | class ResourceMethodFactory extends BasicMethodFactory 24 | { 25 | use ChecksMethods; 26 | use MocksParameters; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function makeTestable(TestClass $class, ReflectionMethod $reflectionMethod): void 32 | { 33 | if ($this->isGetterOrSetter($reflectionMethod)) { 34 | parent::makeTestable($class, $reflectionMethod); 35 | 36 | return; 37 | } 38 | 39 | if (! $this->isMethod($reflectionMethod, 'toArray')) { 40 | throw new InvalidArgumentException( 41 | "cannot generate tests for method {$reflectionMethod->getShortName()}, not a \"toArray\" method" 42 | ); 43 | } 44 | 45 | $method = $this->makeEmpty($reflectionMethod); 46 | $class->addMethod($method); 47 | 48 | $parameters = Reflect::parameters($reflectionMethod); 49 | $this->mockParametersAndAddStatements($method, $parameters); 50 | 51 | $instanceName = $this->getPropertyName($class->getReflectionClass()); 52 | $parametersString = $parameters->map(function (ReflectionParameter $reflectionParameter) { 53 | return '$'.$reflectionParameter->getName(); 54 | })->join(', '); 55 | 56 | $method->addStatement($this->statementFactory->makeTodo('This test is incomplete.')); 57 | $method->addStatement( 58 | $this->statementFactory->makeAssert('same', '[]', "\$this->{$instanceName}->toArray({$parametersString})") 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Generators/Factories/ClassFactory.php: -------------------------------------------------------------------------------- 1 | 20 | * @author Killian Hascoët 21 | * @license MIT 22 | */ 23 | class ClassFactory implements ClassFactoryContract, ConfigAware, DocumentationFactoryAware 24 | { 25 | use ConfigAwareTrait; 26 | use DocumentationFactoryAwareTrait; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function make(ReflectionClass $reflectionClass): TestClass 32 | { 33 | $class = new TestClass( 34 | $reflectionClass, 35 | $this->makeName($reflectionClass) 36 | ); 37 | 38 | $class->setDocumentation( 39 | $this->documentationFactory->makeForClass($class) 40 | ); 41 | 42 | return $class; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function getTestBaseNamespace(): string 49 | { 50 | return trim($this->config->baseTestNamespace(), '\\'); 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function getTestSubNamespace(): string 57 | { 58 | return ''; 59 | } 60 | 61 | /** 62 | * Get the test class name. 63 | * 64 | * @param ReflectionClass $reflectionClass 65 | * 66 | * @return string 67 | */ 68 | public function makeName(ReflectionClass $reflectionClass): string 69 | { 70 | $name = $reflectionClass->getName(); 71 | 72 | $baseNamespace = trim($this->config->baseNamespace(), '\\'); 73 | if ($baseNamespace !== '') { 74 | $name = trim(Str::replaceFirst($baseNamespace, '', $name), '\\'); 75 | } 76 | 77 | return trim($this->getTestBaseNamespace().$this->getTestSubNamespace(), '\\').'\\'.$name.'Test'; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Generators/Tests/Laravel/Job/JobMethodFactory.php: -------------------------------------------------------------------------------- 1 | 21 | * @author Killian Hascoët 22 | * @license MIT 23 | */ 24 | class JobMethodFactory extends BasicMethodFactory 25 | { 26 | use ChecksMethods; 27 | use MocksParameters; 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function makeTestable(TestClass $class, ReflectionMethod $reflectionMethod): void 33 | { 34 | if ($this->isGetterOrSetter($reflectionMethod)) { 35 | parent::makeTestable($class, $reflectionMethod); 36 | 37 | return; 38 | } 39 | 40 | if (! $this->isMethod($reflectionMethod, 'handle')) { 41 | throw new InvalidArgumentException( 42 | "cannot generate tests for method {$reflectionMethod->getShortName()}, not a \"handle\" method" 43 | ); 44 | } 45 | 46 | $method = $this->makeEmpty($reflectionMethod); 47 | $class->addMethod($method); 48 | 49 | $instanceName = $this->getPropertyName($class->getReflectionClass()); 50 | $parameters = Reflect::parameters($reflectionMethod); 51 | 52 | $this->mockParametersAndAddStatements($method, $parameters); 53 | 54 | $method->addStatement($this->statementFactory->makeTodo('This test is incomplete.')); 55 | $method->addStatement( 56 | (new TestStatement('$this->')) 57 | ->append($instanceName) 58 | ->append('->handle(') 59 | ->append($parameters->map(function (ReflectionParameter $reflectionParameter) { 60 | return '$'.$reflectionParameter->getName(); 61 | })->join(', ')) 62 | ->append(')') 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Models/Concerns/HasLines.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | trait HasLines 17 | { 18 | /** 19 | * @var string[]|Collection The statement lines. 20 | */ 21 | protected $lines; 22 | 23 | /** 24 | * Initialize the lines collection with or without a first line. 25 | * 26 | * @param string|null $firstLine 27 | */ 28 | protected function initializeLines(?string $firstLine): void 29 | { 30 | $this->lines = new Collection(); 31 | 32 | if ($firstLine !== null) { 33 | $this->lines->add($firstLine); 34 | } 35 | } 36 | 37 | /** 38 | * @return Collection 39 | */ 40 | public function getLines(): Collection 41 | { 42 | return $this->lines; 43 | } 44 | 45 | /** 46 | * @param string $content 47 | * 48 | * @return static 49 | */ 50 | public function addLine(string $content = ''): self 51 | { 52 | $this->lines->add($content); 53 | 54 | return $this; 55 | } 56 | 57 | /** 58 | * @return static 59 | */ 60 | public function removeLine(): self 61 | { 62 | $this->lines->pop(); 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * Prepend content on the last line. Use the n line if $key is defined. 69 | * 70 | * @param string $content 71 | * @param int|null $key 72 | * 73 | * @return static 74 | */ 75 | public function prepend(string $content, ?int $key = null): self 76 | { 77 | if ($key === null) { 78 | $key = $this->lines->keys()->last(); 79 | } 80 | 81 | $this->lines->put($key, $content.$this->lines->get($key)); 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * Append content on the last line. Use the n line if $key is defined. 88 | * 89 | * @param string $content 90 | * @param int|null $key 91 | * 92 | * @return static 93 | */ 94 | public function append(string $content, ?int $key = null): self 95 | { 96 | if ($key === null) { 97 | $key = $this->lines->keys()->last(); 98 | } 99 | 100 | $this->lines->put($key, $this->lines->get($key).$content); 101 | 102 | return $this; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Generators/Tests/Basic/ManagesGetterAndSetter.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Killian Hascoët 17 | * @license MIT 18 | */ 19 | trait ManagesGetterAndSetter 20 | { 21 | /** 22 | * Check if the given method is a getter or a setter. 23 | * 24 | * @param ReflectionMethod $reflectionMethod 25 | * 26 | * @return bool 27 | */ 28 | protected function isGetterOrSetter(ReflectionMethod $reflectionMethod): bool 29 | { 30 | return $this->isGetter($reflectionMethod) || $this->isSetter($reflectionMethod); 31 | } 32 | 33 | /** 34 | * Check if the given method is a getter (begins with "get" and has a corresponding property). 35 | * 36 | * @param ReflectionMethod $reflectionMethod 37 | * 38 | * @return bool 39 | */ 40 | protected function isGetter(ReflectionMethod $reflectionMethod): bool 41 | { 42 | return $this->getPropertyFromMethod($reflectionMethod, 'get') !== null; 43 | } 44 | 45 | /** 46 | * Check if the given method is a setter (begins with "set" and has a corresponding property). 47 | * 48 | * @param ReflectionMethod $reflectionMethod 49 | * 50 | * @return bool 51 | */ 52 | protected function isSetter(ReflectionMethod $reflectionMethod): bool 53 | { 54 | return $this->getPropertyFromMethod($reflectionMethod, 'set') !== null; 55 | } 56 | 57 | /** 58 | * Get the property for the given method by removing prefix from the method name. 59 | * 60 | * @param ReflectionMethod $reflectionMethod 61 | * @param string $prefix 62 | * 63 | * @return ReflectionProperty|null 64 | */ 65 | private function getPropertyFromMethod(ReflectionMethod $reflectionMethod, string $prefix): ?ReflectionProperty 66 | { 67 | $methodName = $reflectionMethod->getShortName(); 68 | 69 | if (! Str::startsWith($prefix, $methodName)) { 70 | return null; 71 | } 72 | 73 | $property = Reflect::property( 74 | $reflectionMethod->getDeclaringClass(), 75 | lcfirst(Str::replaceFirst($prefix, '', $methodName)) 76 | ); 77 | 78 | if (! $property || $reflectionMethod->isStatic() !== $property->isStatic()) { 79 | return null; 80 | } 81 | 82 | return $property; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Generators/Tests/Laravel/Rule/RuleMethodFactory.php: -------------------------------------------------------------------------------- 1 | 18 | * @author Killian Hascoët 19 | * @license MIT 20 | */ 21 | class RuleMethodFactory extends BasicMethodFactory 22 | { 23 | use ChecksMethods; 24 | use MocksParameters; 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function makeTestable(TestClass $class, ReflectionMethod $reflectionMethod): void 30 | { 31 | if ($this->isGetterOrSetter($reflectionMethod)) { 32 | parent::makeTestable($class, $reflectionMethod); 33 | 34 | return; 35 | } 36 | 37 | if (! $this->isMethod($reflectionMethod, 'passes')) { 38 | throw new InvalidArgumentException( 39 | "cannot generate tests for method {$reflectionMethod->getShortName()}, not a \"passes\" method" 40 | ); 41 | } 42 | 43 | $this->makeRuleTestMethod($class, $reflectionMethod, 'WhenOk', 'True', 'valid value'); 44 | $this->makeRuleTestMethod($class, $reflectionMethod, 'WhenFailed', 'False', 'invalid value'); 45 | } 46 | 47 | /** 48 | * Make a test method which call the "passes" method. 49 | * 50 | * @param TestClass $class 51 | * @param ReflectionMethod $reflectionMethod 52 | * @param string $suffix 53 | * @param string $assert 54 | * @param string $attributeValue 55 | */ 56 | protected function makeRuleTestMethod( 57 | TestClass $class, 58 | ReflectionMethod $reflectionMethod, 59 | string $suffix, 60 | string $assert, 61 | string $attributeValue 62 | ): void { 63 | $method = $this->makeEmpty($reflectionMethod, $suffix); 64 | $class->addMethod($method); 65 | 66 | $instanceName = $this->getPropertyName($class->getReflectionClass()); 67 | $method->addStatement($this->statementFactory->makeTodo('This test is incomplete.')); 68 | $method->addStatement( 69 | $this->statementFactory->makeAssert( 70 | $assert, 71 | "\$this->{$instanceName}->passes('attribute', '{$attributeValue}')" 72 | ) 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/CoreApplication.php: -------------------------------------------------------------------------------- 1 | 22 | * @author Killian Hascoët 23 | * @license MIT 24 | */ 25 | class CoreApplication 26 | { 27 | /** 28 | * @var ContainerInterface 29 | */ 30 | protected $container; 31 | 32 | /** 33 | * CoreApplication constructor. 34 | * 35 | * @param ContainerInterface $container 36 | */ 37 | public function __construct(ContainerInterface $container) 38 | { 39 | $this->container = $container; 40 | } 41 | 42 | /** 43 | * Create an application using the given config array. 44 | * 45 | * @param array $config 46 | * 47 | * @return CoreApplication 48 | */ 49 | public static function make(array $config = []): self 50 | { 51 | return new static(CoreContainerFactory::make(Config::make($config))); 52 | } 53 | 54 | /** 55 | * Run PhpUnitGen on the given source and return the renderer result. 56 | * 57 | * @param Source $source 58 | * 59 | * @return Rendered 60 | */ 61 | public function run(Source $source): Rendered 62 | { 63 | $reflectionClass = $this->getCodeParser()->parse($source); 64 | $testClass = $this->getTestGenerator()->generate($reflectionClass); 65 | 66 | return $this->getRenderer()->visitTestClass($testClass)->getRendered(); 67 | } 68 | 69 | /** 70 | * @return ContainerInterface 71 | */ 72 | public function getContainer(): ContainerInterface 73 | { 74 | return $this->container; 75 | } 76 | 77 | /** 78 | * @return CodeParser 79 | */ 80 | public function getCodeParser(): CodeParser 81 | { 82 | return $this->getContainer()->get(CodeParser::class); 83 | } 84 | 85 | /** 86 | * @return TestGenerator 87 | */ 88 | public function getTestGenerator(): TestGenerator 89 | { 90 | return $this->getContainer()->get(TestGenerator::class); 91 | } 92 | 93 | /** 94 | * @return Renderer 95 | */ 96 | public function getRenderer(): Renderer 97 | { 98 | return $this->getContainer()->get(Renderer::class); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Generators/Tests/Laravel/Channel/ChannelMethodFactory.php: -------------------------------------------------------------------------------- 1 | 20 | * @author Killian Hascoët 21 | * @license MIT 22 | */ 23 | class ChannelMethodFactory extends BasicMethodFactory implements ConfigAware 24 | { 25 | use ChecksMethods; 26 | use UsesUserModel; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function makeSetUp(TestClass $class): TestMethod 32 | { 33 | $method = parent::makeSetUp($class); 34 | 35 | $this->makeUserAffectStatement($class, $method); 36 | 37 | return $method; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function makeTestable(TestClass $class, ReflectionMethod $reflectionMethod): void 44 | { 45 | if ($this->isGetterOrSetter($reflectionMethod)) { 46 | parent::makeTestable($class, $reflectionMethod); 47 | 48 | return; 49 | } 50 | 51 | if (! $this->isMethod($reflectionMethod, 'join')) { 52 | throw new InvalidArgumentException( 53 | "cannot generate tests for method {$reflectionMethod->getShortName()}, not a \"join\" method" 54 | ); 55 | } 56 | 57 | $this->addJoinTestMethod($class, $reflectionMethod, 'Unauthorized', 'false'); 58 | $this->addJoinTestMethod($class, $reflectionMethod, 'Authorized', 'true'); 59 | } 60 | 61 | /** 62 | * Make and add a method to test a join case (authorized or unauthorized). 63 | * 64 | * @param TestClass $class 65 | * @param ReflectionMethod $reflectionMethod 66 | * @param string $suffix 67 | * @param string $assert 68 | */ 69 | protected function addJoinTestMethod( 70 | TestClass $class, 71 | ReflectionMethod $reflectionMethod, 72 | string $suffix, 73 | string $assert 74 | ): void { 75 | $instanceName = $this->getPropertyName($class->getReflectionClass()); 76 | 77 | $method = $this->makeEmpty($reflectionMethod, 'When'.$suffix); 78 | $method->addStatement($this->statementFactory->makeTodo('This test is incomplete.')); 79 | $method->addStatement( 80 | $this->statementFactory->makeAssert($assert, "\$this->{$instanceName}->join(\$this->user)") 81 | ); 82 | 83 | $class->addMethod($method); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Generators/Factories/DocumentationFactory.php: -------------------------------------------------------------------------------- 1 | 24 | * @author Killian Hascoët 25 | * @license MIT 26 | */ 27 | class DocumentationFactory implements DocumentationFactoryContract, ConfigAware, TypeFactoryAware 28 | { 29 | use ConfigAwareTrait; 30 | use TypeFactoryAwareTrait; 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function makeForClass(TestClass $class): TestDocumentation 36 | { 37 | $documentation = new TestDocumentation("Class {$class->getShortName()}."); 38 | $documentation->addLine(); 39 | 40 | $hasDocumentation = $this->makeMergedDocLines($class) 41 | ->merge($this->config->phpDoc()) 42 | ->unique() 43 | ->each(function ($line) use ($documentation) { 44 | $documentation->addLine($line); 45 | }) 46 | ->isNotEmpty(); 47 | 48 | if ($hasDocumentation) { 49 | $documentation->addLine(); 50 | } 51 | 52 | return $documentation->addLine('@covers \\'.$class->getReflectionClass()->getName()); 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function makeForProperty(TestProperty $property, Collection $types): TestDocumentation 59 | { 60 | return new TestDocumentation('@var '.$this->typeFactory->formatTypes($types)); 61 | } 62 | 63 | /** 64 | * {@inheritdoc} 65 | */ 66 | public function makeForInheritedMethod(TestMethod $method): TestDocumentation 67 | { 68 | return new TestDocumentation('{@inheritdoc}'); 69 | } 70 | 71 | /** 72 | * Retrieve the merged doc lines from tested class. 73 | * 74 | * @param TestClass $class 75 | * 76 | * @return Collection 77 | */ 78 | protected function makeMergedDocLines(TestClass $class): Collection 79 | { 80 | return Reflect::docBlockTags($class->getReflectionClass()) 81 | ->reject(function (Tag $tag) { 82 | return ! in_array($tag->getName(), $this->config->mergedPhpDoc()); 83 | }) 84 | ->map(function (Tag $tag) { 85 | return $tag->render(); 86 | }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Contracts/Renderers/Renderer.php: -------------------------------------------------------------------------------- 1 | 23 | * @author Killian Hascoët 24 | * @license MIT 25 | */ 26 | interface Renderer 27 | { 28 | /** 29 | * Get the rendered content after visiting objects. 30 | * 31 | * @return Rendered 32 | */ 33 | public function getRendered(): Rendered; 34 | 35 | /** 36 | * Visit and render a test import. 37 | * 38 | * @param TestImport $import 39 | * 40 | * @return static 41 | */ 42 | public function visitTestImport(TestImport $import): self; 43 | 44 | /** 45 | * Visit and render a test class. 46 | * 47 | * @param TestClass $class 48 | * 49 | * @return static 50 | */ 51 | public function visitTestClass(TestClass $class): self; 52 | 53 | /** 54 | * Visit and render a test trait. 55 | * 56 | * @param TestTrait $trait 57 | * 58 | * @return static 59 | */ 60 | public function visitTestTrait(TestTrait $trait): self; 61 | 62 | /** 63 | * Visit and render a test property. 64 | * 65 | * @param TestProperty $property 66 | * 67 | * @return static 68 | */ 69 | public function visitTestProperty(TestProperty $property): self; 70 | 71 | /** 72 | * Visit and render a test method. 73 | * 74 | * @param TestMethod $method 75 | * 76 | * @return static 77 | */ 78 | public function visitTestMethod(TestMethod $method): self; 79 | 80 | /** 81 | * Visit and render a test parameter. 82 | * 83 | * @param TestParameter $parameter 84 | * 85 | * @return static 86 | */ 87 | public function visitTestParameter(TestParameter $parameter): self; 88 | 89 | /** 90 | * Visit and render a test provider. 91 | * 92 | * @param TestProvider $provider 93 | * 94 | * @return static 95 | */ 96 | public function visitTestProvider(TestProvider $provider): self; 97 | 98 | /** 99 | * Visit and render a test statement. 100 | * 101 | * @param TestStatement $statement 102 | * 103 | * @return static 104 | */ 105 | public function visitTestStatement(TestStatement $statement): self; 106 | 107 | /** 108 | * Visit and render a test documentation. 109 | * 110 | * @param TestDocumentation $documentation 111 | * 112 | * @return static 113 | */ 114 | public function visitTestDocumentation(TestDocumentation $documentation): self; 115 | } 116 | -------------------------------------------------------------------------------- /src/Generators/Tests/Laravel/Command/CommandMethodFactory.php: -------------------------------------------------------------------------------- 1 | 23 | * @author Killian Hascoët 24 | * @license MIT 25 | */ 26 | class CommandMethodFactory extends BasicMethodFactory 27 | { 28 | use ChecksMethods; 29 | use HasInstanceBinding; 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function makeSetUp(TestClass $class): TestMethod 35 | { 36 | $method = parent::makeSetUp($class); 37 | 38 | $method->addStatement( 39 | $this->makeInstanceBindingStatement($class->getReflectionClass()) 40 | ); 41 | 42 | return $method; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function makeTestable(TestClass $class, ReflectionMethod $reflectionMethod): void 49 | { 50 | if ($this->isGetterOrSetter($reflectionMethod)) { 51 | parent::makeTestable($class, $reflectionMethod); 52 | 53 | return; 54 | } 55 | 56 | if (! $this->isMethod($reflectionMethod, 'handle')) { 57 | throw new InvalidArgumentException( 58 | "cannot generate tests for method {$reflectionMethod->getShortName()}, not a \"handle\" method" 59 | ); 60 | } 61 | 62 | $method = $this->makeEmpty($reflectionMethod); 63 | 64 | $method->addStatement($this->statementFactory->makeTodo('This test is incomplete.')); 65 | $method->addStatement( 66 | (new TestStatement('$this->artisan(\'')) 67 | ->append($this->resolveCommandSignature($class->getReflectionClass())) 68 | ->append('\')') 69 | ->addLine('->expectsOutput(\'Some expected output\')') 70 | ->addLine('->assertExitCode(0)') 71 | ); 72 | 73 | $class->addMethod($method); 74 | } 75 | 76 | protected function resolveCommandSignature(ReflectionClass $reflectionClass): string 77 | { 78 | $signatureProperty = Reflect::property($reflectionClass, 'signature'); 79 | 80 | if (! $signatureProperty 81 | || $signatureProperty->isStatic() 82 | || ! $signatureProperty->isProtected() 83 | ) { 84 | return 'command:name'; 85 | } 86 | 87 | try { 88 | $signature = $signatureProperty->getDefaultValue(); 89 | } catch (Throwable $exception) { 90 | $signature = null; 91 | } 92 | 93 | return is_string($signature) ? $signature : 'command:name'; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Generators/Tests/Laravel/Controller/ControllerMethodFactory.php: -------------------------------------------------------------------------------- 1 | 17 | * @author Killian Hascoët 18 | * @license MIT 19 | */ 20 | class ControllerMethodFactory extends CommandMethodFactory 21 | { 22 | /** 23 | * The mapping between HTTP method to tests and strings controller methods should starts with. 24 | */ 25 | protected const HTTP_METHODS_MAP = [ 26 | 'post' => 'store', 27 | 'put' => 'update', 28 | 'delete' => ['delete', 'destroy'], 29 | ]; 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function makeTestable(TestClass $class, ReflectionMethod $reflectionMethod): void 35 | { 36 | if ($this->isGetterOrSetter($reflectionMethod)) { 37 | parent::makeTestable($class, $reflectionMethod); 38 | 39 | return; 40 | } 41 | 42 | $methodToUse = $this->resolveTestMethod($class, $reflectionMethod); 43 | 44 | $method = $this->makeEmpty($reflectionMethod); 45 | 46 | $method->addStatement($this->statementFactory->makeTodo('This test is incomplete.')); 47 | $method->addStatement( 48 | (new TestStatement('$this->')) 49 | ->append($methodToUse) 50 | ->append('(\'/path\'') 51 | ->append(Str::startsWith(['get', 'delete'], $methodToUse) ? '' : ', [ /* data */ ]') 52 | ->append(')') 53 | ->addLine('->assertStatus(200)') 54 | ); 55 | 56 | $class->addMethod($method); 57 | } 58 | 59 | /** 60 | * Resolve the test method to call in generated test, based on the class namespace and method name. 61 | * 62 | * @param TestClass $class 63 | * @param ReflectionMethod $reflectionMethod 64 | * 65 | * @return string 66 | */ 67 | protected function resolveTestMethod(TestClass $class, ReflectionMethod $reflectionMethod): string 68 | { 69 | $httpMethodToTest = $this->resolveHttpMethodToTest($reflectionMethod); 70 | 71 | if (Str::containsRegex('\\\\api\\\\', $class->getReflectionClass()->getName())) { 72 | return $httpMethodToTest.'Json'; 73 | } 74 | 75 | return $httpMethodToTest; 76 | } 77 | 78 | /** 79 | * Resolve the HTTP method to use in generated test from method name. 80 | * 81 | * @param ReflectionMethod $reflectionMethod 82 | * 83 | * @return string 84 | */ 85 | protected function resolveHttpMethodToTest(ReflectionMethod $reflectionMethod): string 86 | { 87 | $reflectionMethodName = $reflectionMethod->getShortName(); 88 | 89 | foreach (self::HTTP_METHODS_MAP as $httpMethod => $search) { 90 | if (Str::startsWith($search, $reflectionMethodName)) { 91 | return $httpMethod; 92 | } 93 | } 94 | 95 | return 'get'; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Container/ReflectionServiceProvider.php: -------------------------------------------------------------------------------- 1 | 22 | * @author Killian Hascoët 23 | * @license MIT 24 | */ 25 | abstract class ReflectionServiceProvider extends AbstractServiceProvider 26 | { 27 | /** 28 | * Add a contract's implementation to container with its arguments using reflection. 29 | * 30 | * @param string $contract 31 | * @param string $concrete 32 | * 33 | * @throws InvalidArgumentException 34 | */ 35 | protected function addDefinition(string $contract, string $concrete): void 36 | { 37 | $definition = $this->leagueContainer->add($contract, $concrete); 38 | 39 | $this->addDefinitionArguments($definition); 40 | } 41 | 42 | /** 43 | * Add the necessary arguments to the definition. 44 | * 45 | * @param DefinitionInterface $definition 46 | * 47 | * @throws InvalidArgumentException 48 | */ 49 | private function addDefinitionArguments(DefinitionInterface $definition): void 50 | { 51 | $constructor = $this->getClassConstructor($definition); 52 | 53 | if (! $constructor) { 54 | return; 55 | } 56 | 57 | foreach ($constructor->getParameters() as $parameter) { 58 | $this->addDefinitionArgument($definition, $parameter); 59 | } 60 | } 61 | 62 | /** 63 | * Add an argument to definition from the given parameter. 64 | * 65 | * @param DefinitionInterface $definition 66 | * @param ReflectionParameter $parameter 67 | * 68 | * @throws InvalidArgumentException 69 | */ 70 | private function addDefinitionArgument(DefinitionInterface $definition, ReflectionParameter $parameter): void 71 | { 72 | $type = $parameter->getType(); 73 | if (! $type || $type->isBuiltin()) { 74 | throw new InvalidArgumentException( 75 | "dependency {$parameter->getName()} for class {$definition->getConcrete()} has an unresolvable type" 76 | ); 77 | } 78 | 79 | $definition->addArgument($type->getName()); 80 | } 81 | 82 | /** 83 | * Get the constructor for a definition concrete class. 84 | * 85 | * @param DefinitionInterface $definition 86 | * 87 | * @return ReflectionMethod|null 88 | * 89 | * @throws InvalidArgumentException 90 | */ 91 | private function getClassConstructor(DefinitionInterface $definition): ?ReflectionMethod 92 | { 93 | try { 94 | return (new ReflectionClass($definition->getConcrete()))->getMethod('__construct'); 95 | } catch (ReflectionException $exception) { 96 | return null; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Generators/Factories/StatementFactory.php: -------------------------------------------------------------------------------- 1 | 20 | * @author Killian Hascoët 21 | * @license MIT 22 | */ 23 | class StatementFactory implements StatementFactoryContract, ImportFactoryAware 24 | { 25 | use ImportFactoryAwareTrait; 26 | use InstantiatesClass; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function makeTodo(string $todo): TestStatement 32 | { 33 | return new TestStatement("/** @todo {$todo} */"); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function makeAffect(string $name, string $value, bool $isProperty = true): TestStatement 40 | { 41 | $statement = new TestStatement('$'); 42 | 43 | if ($isProperty) { 44 | $statement->append('this->'); 45 | } 46 | 47 | return $statement->append($name) 48 | ->append(' = ') 49 | ->append($value); 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function makeAssert(string $assert, string ...$parameters): TestStatement 56 | { 57 | return (new TestStatement('self::assert')) 58 | ->append(ucfirst($assert)) 59 | ->append('(') 60 | ->append(implode(', ', $parameters)) 61 | ->append(')'); 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function makeInstantiation(TestClass $class, Collection $parameters): TestStatement 68 | { 69 | $reflectionClass = $class->getReflectionClass(); 70 | 71 | $className = $this->importFactory->make($class, $reflectionClass->getName()) 72 | ->getFinalName(); 73 | 74 | $parametersString = $parameters 75 | ->map(function (ReflectionParameter $reflectionParameter) { 76 | return '$this->'.$reflectionParameter->getName(); 77 | }) 78 | ->implode(', '); 79 | 80 | $statement = $this->makeAffect($this->getPropertyName($reflectionClass), ''); 81 | 82 | if (! $reflectionClass->isAbstract() && ! $reflectionClass->isTrait()) { 83 | return $statement->append('new ') 84 | ->append($className) 85 | ->append('(') 86 | ->append($parametersString) 87 | ->append(')'); 88 | } 89 | 90 | $statement->append('$this->getMockBuilder(') 91 | ->append($className) 92 | ->append('::class)') 93 | ->addLine('->setConstructorArgs([') 94 | ->append($parametersString) 95 | ->append('])'); 96 | 97 | if ($reflectionClass->isAbstract()) { 98 | return $statement->addLine('->getMockForAbstractClass()'); 99 | } 100 | 101 | return $statement->addLine('->getMockForTrait()'); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Contracts/Config/Config.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Killian Hascoët 14 | * @license MIT 15 | */ 16 | interface Config 17 | { 18 | /** 19 | * Tells if the generator should create advanced tests skeletons and class instantiation. 20 | * 21 | * @return bool 22 | */ 23 | public function automaticGeneration(): bool; 24 | 25 | /** 26 | * Get the contracts implementations mapping. 27 | * 28 | * @return array 29 | */ 30 | public function implementations(): array; 31 | 32 | /** 33 | * Get the base namespace of the source code. This will be removed from the test class namespace. 34 | * 35 | * @return string 36 | */ 37 | public function baseNamespace(): string; 38 | 39 | /** 40 | * Get the base namespace of the tests code. This will be prepended to the test class namespace. 41 | * 42 | * @return string 43 | */ 44 | public function baseTestNamespace(): string; 45 | 46 | /** 47 | * Get the test case absolute class name. 48 | * 49 | * @return string 50 | */ 51 | public function testCase(): string; 52 | 53 | /** 54 | * Tells if the test class should be final. 55 | * 56 | * @return bool 57 | */ 58 | public function testClassFinal(): bool; 59 | 60 | /** 61 | * Tells if the test class should declare strict types. 62 | * 63 | * @return bool 64 | */ 65 | public function testClassStrictTypes(): bool; 66 | 67 | /** 68 | * Tells if the test class properties should be typed or documented. 69 | * 70 | * @return bool 71 | */ 72 | public function testClassTypedProperties(): bool; 73 | 74 | /** 75 | * Get the case insensitive RegExp (without opening and closing "/") that tested methods shouldn't match. 76 | * 77 | * @return array 78 | */ 79 | public function excludedMethods(): array; 80 | 81 | /** 82 | * Get the PHP documentation tags that should be retrieved from tested class and append in its documentation. 83 | * 84 | * @return array 85 | */ 86 | public function mergedPhpDoc(): array; 87 | 88 | /** 89 | * Get the PHP documentation lines that should always be added to the tested class documentation. 90 | * 91 | * @return array 92 | */ 93 | public function phpDoc(): array; 94 | 95 | /** 96 | * Get the PHP header documentation lines that should always be added to the generated file documentation. 97 | * 98 | * @return string 99 | */ 100 | public function phpHeaderDoc(): string; 101 | 102 | /** 103 | * Get the additional options which might be used by specific test generators. 104 | * 105 | * @return array 106 | */ 107 | public function options(): array; 108 | 109 | /** 110 | * Get an additional option using its name. Returns $default if the option is not defined. 111 | * 112 | * @param string $name 113 | * @param null $default 114 | * 115 | * @return mixed 116 | */ 117 | public function getOption(string $name, $default = null); 118 | 119 | /** 120 | * Get the array version of the config. 121 | * 122 | * @return array 123 | */ 124 | public function toArray(): array; 125 | } 126 | -------------------------------------------------------------------------------- /src/Models/TestMethod.php: -------------------------------------------------------------------------------- 1 | 17 | * @author Killian Hascoët 18 | * @license MIT 19 | */ 20 | class TestMethod implements Renderable 21 | { 22 | use HasTestClassParent; 23 | use HasTestDocumentation; 24 | 25 | /** 26 | * @var string The name of the method. 27 | */ 28 | protected $name; 29 | 30 | /** 31 | * @var string The visibility of the method (public, protected or private). 32 | */ 33 | protected $visibility; 34 | 35 | /** 36 | * @var TestProvider|null The data provider. 37 | */ 38 | protected $provider; 39 | 40 | /** 41 | * @var TestParameter[]|Collection The list of parameters. 42 | */ 43 | protected $parameters; 44 | 45 | /** 46 | * @var TestStatement[]|Collection The list of statements. 47 | */ 48 | protected $statements; 49 | 50 | /** 51 | * TestMethod constructor. 52 | * 53 | * @param string $name 54 | * @param string $visibility 55 | */ 56 | public function __construct(string $name, string $visibility = 'public') 57 | { 58 | $this->name = $name; 59 | $this->visibility = $visibility; 60 | 61 | $this->parameters = new Collection(); 62 | $this->statements = new Collection(); 63 | } 64 | 65 | /** 66 | * {@inheritdoc} 67 | */ 68 | public function accept(Renderer $renderer): Renderer 69 | { 70 | return $renderer->visitTestMethod($this); 71 | } 72 | 73 | /** 74 | * @return string 75 | */ 76 | public function getName(): string 77 | { 78 | return $this->name; 79 | } 80 | 81 | /** 82 | * @return string 83 | */ 84 | public function getVisibility(): string 85 | { 86 | return $this->visibility; 87 | } 88 | 89 | /** 90 | * @return TestProvider|null 91 | */ 92 | public function getProvider(): ?TestProvider 93 | { 94 | return $this->provider; 95 | } 96 | 97 | /** 98 | * @param TestProvider|null $provider 99 | * 100 | * @return static 101 | */ 102 | public function setProvider(?TestProvider $provider): self 103 | { 104 | $this->provider = $provider ? $provider->setTestMethod($this) : null; 105 | 106 | return $this; 107 | } 108 | 109 | /** 110 | * @return TestParameter[]|Collection 111 | */ 112 | public function getParameters(): Collection 113 | { 114 | return $this->parameters; 115 | } 116 | 117 | /** 118 | * @param TestParameter $parameter 119 | * 120 | * @return static 121 | */ 122 | public function addParameter(TestParameter $parameter): self 123 | { 124 | $this->parameters->add($parameter->setTestMethod($this)); 125 | 126 | return $this; 127 | } 128 | 129 | /** 130 | * @return TestStatement[]|Collection 131 | */ 132 | public function getStatements(): Collection 133 | { 134 | return $this->statements; 135 | } 136 | 137 | /** 138 | * @param TestStatement $statement 139 | * 140 | * @return static 141 | */ 142 | public function addStatement(TestStatement $statement): self 143 | { 144 | $this->statements->add($statement->setTestMethod($this)); 145 | 146 | return $this; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/Generators/Factories/PropertyFactory.php: -------------------------------------------------------------------------------- 1 | 28 | * @author Killian Hascoët 29 | * @license MIT 30 | */ 31 | class PropertyFactory implements 32 | PropertyFactoryContract, 33 | ConfigAware, 34 | DocumentationFactoryAware, 35 | MockGeneratorAware, 36 | TypeFactoryAware 37 | { 38 | use ConfigAwareTrait; 39 | use DocumentationFactoryAwareTrait; 40 | use MockGeneratorAwareTrait; 41 | use TypeFactoryAwareTrait; 42 | use InstantiatesClass; 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function makeForClass(TestClass $class): TestProperty 48 | { 49 | $reflectionClass = $class->getReflectionClass(); 50 | 51 | return $this->makeCustom( 52 | $class, 53 | $this->getPropertyName($reflectionClass), 54 | $reflectionClass->getName(), 55 | false, 56 | false 57 | ); 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | public function makeForParameter(TestClass $class, ReflectionParameter $reflectionParameter): TestProperty 64 | { 65 | $stringType = 'mixed'; 66 | $isBuiltIn = true; 67 | 68 | $reflectionType = Reflect::parameterType($reflectionParameter); 69 | if ($reflectionType) { 70 | $stringType = $reflectionType->getType(); 71 | $isBuiltIn = $reflectionType->isBuiltin(); 72 | } 73 | 74 | return $this->makeCustom($class, $reflectionParameter->getName(), $stringType, $isBuiltIn); 75 | } 76 | 77 | /** 78 | * {@inheritdoc} 79 | */ 80 | public function makeCustom( 81 | TestClass $class, 82 | string $name, 83 | string $type, 84 | bool $isBuiltIn = false, 85 | bool $isMock = true 86 | ): TestProperty { 87 | $property = new TestProperty($name); 88 | 89 | $type = $this->typeFactory->makeFromString($class, $type, $isBuiltIn); 90 | $types = $isMock && $type instanceof TestImport 91 | ? new Collection([$type, $this->mockGenerator->getMockType($class)]) 92 | : new Collection([$type]); 93 | 94 | $this->typeOrDocumentProperty($property, $types); 95 | 96 | return $property; 97 | } 98 | 99 | /** 100 | * Add a type or a documentation to property depending on configuration. 101 | * 102 | * @param TestProperty $property 103 | * @param Collection $types 104 | */ 105 | protected function typeOrDocumentProperty(TestProperty $property, Collection $types): void 106 | { 107 | if ($this->config->testClassTypedProperties()) { 108 | $property->setType($this->typeFactory->formatTypes($types)); 109 | } else { 110 | $property->setDocumentation( 111 | $this->documentationFactory->makeForProperty($property, $types) 112 | ); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

PhpUnitGen

2 | 3 |

4 | Total Downloads 5 | Latest Stable Version 6 | Build Status 7 | StyleCI 8 | Quality Gate Status 9 | Coverage 10 |

11 | 12 | ## :warning: PhpUnitGen end of life 13 | 14 | After 7 years of existence, PhpUnitGen is not maintained anymore. 15 | [Read the post](https://github.com/paul-thebaud/phpunitgen-core/issues/32) 16 | 17 | ## Installation 18 | 19 | The CLI tool can be installed using: 20 | ```bash 21 | composer require --dev phpunitgen/console 22 | ``` 23 | 24 | Detailed information and webapp version are available at 25 | [https://phpunitgen.io](https://phpunitgen.io). 26 | 27 | ## About PhpUnitGen 28 | 29 | > **Note:** this repository contains the core code of PhpUnitGen. If you want 30 | > to use the tool on your browser, you can go on the 31 | > [webapp](https://phpunitgen.io). If you want to use the tool on your console, 32 | > you can install the 33 | > [command line package](https://github.com/paul-thebaud/phpunitgen-console). 34 | 35 | PhpUnitGen is an online and command line tool to generate your unit tests' 36 | skeletons on your projects. 37 | 38 | - [How does it work?](https://phpunitgen.io/docs#/en/how-does-it-work) 39 | - [Configuration](https://phpunitgen.io/docs#/en/configuration) 40 | - [Web application](https://phpunitgen.io/docs#/en/webapp) 41 | - [Command line](https://phpunitgen.io/docs#/en/command-line) 42 | - [Advanced usage](https://phpunitgen.io/docs#/en/advanced-usage) 43 | 44 | ### Key features 45 | 46 | - Generates tests skeletons for your PHP classes 47 | - Binds with Laravel "make" command 48 | - Generates class instantiation using dummy parameters or mocks 49 | - Adapts to PHPUnit or Mockery mocks generation 50 | 51 | PhpUnitGen is not meant to generate your tests content but only the skeleton (except for getters/setters). 52 | 53 | This is because inspecting your code to generate the appropriate test is 54 | way too complex, and might result in missing some of the code's features 55 | or marking them as "passed unit test" even if it contains errors. 56 | 57 | ## Roadmap 58 | 59 | You can track the tasks we plan to do on our 60 | [Taiga.io project](https://tree.taiga.io/project/paul-thebaud-phpunitgen/kanban). 61 | 62 | ## Contributing 63 | 64 | Please see [CONTRIBUTING](CONTRIBUTING.md) for more details. 65 | 66 | Informal discussion regarding bugs, new features, and implementation of 67 | existing features takes place in the 68 | [Github issue page of Core repository](https://github.com/paul-thebaud/phpunitgen-core/issues). 69 | 70 | ## Credits 71 | 72 | - [Paul Thébaud](https://github/paul-thebaud) 73 | - [Killian Hascoët](https://github.com/KillianH) 74 | - [Charles Corbel](https://dribbble.com/CorbelC) 75 | - [All Contributors](https://github.com/paul-thebaud/phpunitgen-core/graphs/contributors) 76 | 77 | ## License 78 | 79 | PhpUnitGen is an open-sourced software licensed under the 80 | [MIT license](https://opensource.org/licenses/MIT). 81 | -------------------------------------------------------------------------------- /src/Reflection/ReflectionType.php: -------------------------------------------------------------------------------- 1 | 17 | * @author Killian Hascoët 18 | * @license MIT 19 | */ 20 | class ReflectionType 21 | { 22 | /** 23 | * The built in types of PHP. 24 | */ 25 | protected const BUILT_IN_TYPES = [ 26 | 'int', 27 | 'float', 28 | 'string', 29 | 'bool', 30 | 'callable', 31 | 'self', 32 | 'parent', 33 | 'array', 34 | 'iterable', 35 | 'object', 36 | 'void', 37 | 'mixed', 38 | ]; 39 | 40 | /** 41 | * @var string 42 | */ 43 | protected $type; 44 | 45 | /** 46 | * @var bool 47 | */ 48 | protected $nullable; 49 | 50 | /** 51 | * ReflectionType constructor. 52 | * 53 | * @param string $type 54 | * @param bool $nullable 55 | */ 56 | public function __construct(string $type, bool $nullable) 57 | { 58 | $clearedType = ltrim($type, '\\'); 59 | if (Str::contains('|', $clearedType)) { 60 | $clearedType = explode('|', $clearedType)[0]; 61 | } 62 | 63 | $this->type = $clearedType; 64 | $this->nullable = $nullable; 65 | } 66 | 67 | /** 68 | * Make a type from the native or doc type. 69 | * 70 | * @param BetterReflectionType|null $reflectionType 71 | * @param array $docTypes 72 | * 73 | * @return static|null 74 | */ 75 | public static function make(?BetterReflectionType $reflectionType, array $docTypes): ?self 76 | { 77 | $type = null; 78 | if ($reflectionType) { 79 | $type = static::makeForBetterReflectionType($reflectionType); 80 | 81 | // When native type is not precisely defined (mixed or object), 82 | // try to retrieve the doc type instead. 83 | if ($type->getType() !== 'mixed' && $type->getType() !== 'object') { 84 | return $type; 85 | } 86 | } 87 | 88 | return static::makeForPhpDocumentorTypes($docTypes) ?? $type; 89 | } 90 | 91 | /** 92 | * Make an instance from a BetterReflection ReflectionType instance. 93 | * 94 | * @param BetterReflectionType $reflectionType 95 | * 96 | * @return static 97 | */ 98 | public static function makeForBetterReflectionType(BetterReflectionType $reflectionType): self 99 | { 100 | return new self(strval($reflectionType), $reflectionType->allowsNull()); 101 | } 102 | 103 | /** 104 | * Make an instance from a phpDocumentor string types. 105 | * 106 | * @param string[] $types 107 | * 108 | * @return static|null 109 | */ 110 | public static function makeForPhpDocumentorTypes(array $types): ?self 111 | { 112 | $stringTypes = new Collection(array_map('strval', $types)); 113 | 114 | // Check if its nullable. 115 | $nullable = $stringTypes->contains('null'); 116 | // Reject null type, since it is not a "real" types. 117 | $stringTypes = $stringTypes->reject('null'); 118 | 119 | if ($stringTypes->isEmpty()) { 120 | return null; 121 | } 122 | 123 | $stringType = $stringTypes->first(); 124 | if (Str::endsWith('[]', $stringType)) { 125 | $stringType = 'array'; 126 | } 127 | 128 | return new self($stringType, $nullable); 129 | } 130 | 131 | /** 132 | * Get the type as a string. 133 | * 134 | * @return string 135 | */ 136 | public function getType(): string 137 | { 138 | return $this->type; 139 | } 140 | 141 | /** 142 | * Tells if this type is nullable. 143 | * 144 | * @return bool 145 | */ 146 | public function isNullable(): bool 147 | { 148 | return $this->nullable; 149 | } 150 | 151 | /** 152 | * Tells if this type is built in. 153 | * 154 | * @return bool 155 | */ 156 | public function isBuiltIn(): bool 157 | { 158 | return in_array($this->type, self::BUILT_IN_TYPES); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Generators/Tests/Laravel/Policy/PolicyMethodFactory.php: -------------------------------------------------------------------------------- 1 | 25 | * @author Killian Hascoët 26 | * @license MIT 27 | */ 28 | class PolicyMethodFactory extends BasicMethodFactory implements ConfigAware 29 | { 30 | use HasInstanceBinding; 31 | use MocksParameters; 32 | use UsesUserModel { 33 | UsesUserModel::setStatementFactory insteadof MocksParameters; 34 | UsesUserModel::getStatementFactory insteadof MocksParameters; 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function makeSetUp(TestClass $class): TestMethod 41 | { 42 | $method = parent::makeSetUp($class); 43 | 44 | $this->makeUserAffectStatement($class, $method); 45 | $method->addStatement( 46 | $this->makeInstanceBindingStatement($class->getReflectionClass()) 47 | ); 48 | 49 | return $method; 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function makeTestable(TestClass $class, ReflectionMethod $reflectionMethod): void 56 | { 57 | if ($this->isGetterOrSetter($reflectionMethod)) { 58 | parent::makeTestable($class, $reflectionMethod); 59 | 60 | return; 61 | } 62 | 63 | if ($reflectionMethod->isStatic()) { 64 | throw new InvalidArgumentException( 65 | "cannot generate tests for method {$reflectionMethod->getShortName()}, policy method cannot be static" 66 | ); 67 | } 68 | 69 | $this->addPolicyMethod($class, $reflectionMethod, 'false', 'Unauthorized'); 70 | $this->addPolicyMethod($class, $reflectionMethod, 'true', 'Authorized'); 71 | } 72 | 73 | /** 74 | * Add a method to test a policy method for authorized or unauthorized user. 75 | * 76 | * @param TestClass $class 77 | * @param ReflectionMethod $reflectionMethod 78 | * @param string $expected 79 | * @param string $nameSuffix 80 | */ 81 | protected function addPolicyMethod( 82 | TestClass $class, 83 | ReflectionMethod $reflectionMethod, 84 | string $expected, 85 | string $nameSuffix 86 | ): void { 87 | $method = $this->makeEmpty($reflectionMethod, 'When'.$nameSuffix); 88 | $class->addMethod($method); 89 | $method->addStatement($this->statementFactory->makeTodo('This test is incomplete.')); 90 | 91 | $parameters = Reflect::parameters($reflectionMethod)->forget(0); 92 | 93 | $this->mockParametersAndAddStatements($method, $parameters); 94 | $parametersString = $this->getParametersString($class->getReflectionClass(), $parameters); 95 | 96 | $methodCall = '$this->user->can(\''.$reflectionMethod->getShortName().'\', '.$parametersString.')'; 97 | 98 | $method->addStatement( 99 | $this->statementFactory->makeAssert($expected, $methodCall) 100 | ); 101 | } 102 | 103 | /** 104 | * Retrieve the parameters as they will be added in method argument. 105 | * 106 | * @param ReflectionClass $reflectionClass 107 | * @param Collection $parameters 108 | * 109 | * @return string 110 | */ 111 | protected function getParametersString(ReflectionClass $reflectionClass, Collection $parameters): string 112 | { 113 | $parameters = $parameters 114 | ->map(function (ReflectionParameter $reflectionParameter) { 115 | return '$'.$reflectionParameter->getName(); 116 | }); 117 | 118 | if ($parameters->count() < 1) { 119 | return '['.$reflectionClass->getShortName().'::class]'; 120 | } 121 | 122 | $parametersString = $parameters->implode(', '); 123 | 124 | if ($parameters->count() > 1) { 125 | return '['.$parametersString.']'; 126 | } 127 | 128 | return $parametersString; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Models/TestClass.php: -------------------------------------------------------------------------------- 1 | 18 | * @author Killian Hascoët 19 | * @license MIT 20 | */ 21 | class TestClass implements Renderable 22 | { 23 | use HasTestDocumentation; 24 | 25 | /** 26 | * @var ReflectionClass The class for which this test class is created. 27 | */ 28 | protected $reflectionClass; 29 | 30 | /** 31 | * @var string The complete name of the class (including namespace). 32 | */ 33 | protected $name; 34 | 35 | /** 36 | * @var TestImport[]|Collection The list of test imports. 37 | */ 38 | protected $imports; 39 | 40 | /** 41 | * @var TestTrait[]|Collection The list of test traits. 42 | */ 43 | protected $traits; 44 | 45 | /** 46 | * @var TestProperty[]|Collection The list of test properties. 47 | */ 48 | protected $properties; 49 | 50 | /** 51 | * @var TestMethod[]|Collection The list of test methods. 52 | */ 53 | protected $methods; 54 | 55 | /** 56 | * TestClass constructor. 57 | * 58 | * @param ReflectionClass $reflectionClass 59 | * @param string $name 60 | */ 61 | public function __construct(ReflectionClass $reflectionClass, string $name) 62 | { 63 | $this->reflectionClass = $reflectionClass; 64 | $this->name = $name; 65 | 66 | $this->imports = new Collection(); 67 | $this->traits = new Collection(); 68 | $this->properties = new Collection(); 69 | $this->methods = new Collection(); 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function accept(Renderer $renderer): Renderer 76 | { 77 | return $renderer->visitTestClass($this); 78 | } 79 | 80 | /** 81 | * @return ReflectionClass 82 | */ 83 | public function getReflectionClass(): ReflectionClass 84 | { 85 | return $this->reflectionClass; 86 | } 87 | 88 | /** 89 | * @return string 90 | */ 91 | public function getName(): string 92 | { 93 | return $this->name; 94 | } 95 | 96 | /** 97 | * @return string|null 98 | */ 99 | public function getNamespace(): ?string 100 | { 101 | if (! Str::contains('\\', $this->name)) { 102 | return null; 103 | } 104 | 105 | return Str::beforeLast('\\', $this->name); 106 | } 107 | 108 | /** 109 | * @return string 110 | */ 111 | public function getShortName(): string 112 | { 113 | return Str::afterLast('\\', $this->name); 114 | } 115 | 116 | /** 117 | * @return TestImport[]|Collection 118 | */ 119 | public function getImports(): Collection 120 | { 121 | return $this->imports; 122 | } 123 | 124 | /** 125 | * @param TestImport $import 126 | * 127 | * @return static 128 | */ 129 | public function addImport(TestImport $import): self 130 | { 131 | $this->imports->add($import->setTestClass($this)); 132 | 133 | return $this; 134 | } 135 | 136 | /** 137 | * @return TestTrait[]|Collection 138 | */ 139 | public function getTraits(): Collection 140 | { 141 | return $this->traits; 142 | } 143 | 144 | /** 145 | * @param TestTrait $trait 146 | * 147 | * @return static 148 | */ 149 | public function addTrait(TestTrait $trait): self 150 | { 151 | $this->traits->add($trait->setTestClass($this)); 152 | 153 | return $this; 154 | } 155 | 156 | /** 157 | * @return TestProperty[]|Collection 158 | */ 159 | public function getProperties() 160 | { 161 | return $this->properties; 162 | } 163 | 164 | /** 165 | * @param TestProperty $property 166 | * 167 | * @return static 168 | */ 169 | public function addProperty(TestProperty $property): self 170 | { 171 | $this->properties->add($property->setTestClass($this)); 172 | 173 | return $this; 174 | } 175 | 176 | /** 177 | * @return TestMethod[]|Collection 178 | */ 179 | public function getMethods(): Collection 180 | { 181 | return $this->methods; 182 | } 183 | 184 | /** 185 | * @param TestMethod $method 186 | * 187 | * @return static 188 | */ 189 | public function addMethod(TestMethod $method): self 190 | { 191 | $this->methods->add($method->setTestClass($this)); 192 | 193 | return $this; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/Helpers/Str.php: -------------------------------------------------------------------------------- 1 | 15 | * @author Killian Hascoët 16 | * @license MIT 17 | */ 18 | class Str 19 | { 20 | /** 21 | * Get the substring after the last occurrence of search. 22 | * 23 | * @param string $search 24 | * @param string $subject 25 | * 26 | * @return string 27 | */ 28 | public static function beforeLast(string $search, string $subject): string 29 | { 30 | $lastPosition = strrpos($subject, $search); 31 | if ($lastPosition === false) { 32 | return $subject; 33 | } 34 | 35 | return substr($subject, 0, $lastPosition); 36 | } 37 | 38 | /** 39 | * Get the substring after the first occurrence of search. 40 | * 41 | * @param string $search 42 | * @param string $subject 43 | * 44 | * @return string 45 | */ 46 | public static function afterFirst(string $search, string $subject): string 47 | { 48 | $lastPosition = strpos($subject, $search); 49 | if ($lastPosition === false) { 50 | return $subject; 51 | } 52 | 53 | return substr($subject, $lastPosition + 1); 54 | } 55 | 56 | /** 57 | * Get the substring after the last occurrence of search. 58 | * 59 | * @param string $search 60 | * @param string $subject 61 | * 62 | * @return string 63 | */ 64 | public static function afterLast(string $search, string $subject): string 65 | { 66 | $lastPosition = strrpos($subject, $search); 67 | if ($lastPosition === false) { 68 | return $subject; 69 | } 70 | 71 | return substr($subject, $lastPosition + 1); 72 | } 73 | 74 | /** 75 | * Replace the first occurrence in the given string. 76 | * 77 | * @param string $search 78 | * @param string $replace 79 | * @param string $subject 80 | * 81 | * @return string 82 | */ 83 | public static function replaceFirst(string $search, string $replace, string $subject): string 84 | { 85 | if (! self::contains($search, $subject)) { 86 | return $subject; 87 | } 88 | 89 | return preg_replace('/'.preg_quote($search, '/').'/', $replace, $subject, 1); 90 | } 91 | 92 | /** 93 | * Replace the last occurrence in the given string. 94 | * 95 | * @param string $search 96 | * @param string $replace 97 | * @param string $subject 98 | * 99 | * @return string 100 | */ 101 | public static function replaceLast(string $search, string $replace, string $subject): string 102 | { 103 | $position = strrpos($subject, $search); 104 | if ($position === false) { 105 | return $subject; 106 | } 107 | 108 | return substr_replace($subject, $replace, $position, strlen($search)); 109 | } 110 | 111 | /** 112 | * Check if the given string contains with (one of) the given search. 113 | * 114 | * @param string|string[] $searches 115 | * @param string $subject 116 | * 117 | * @return bool 118 | */ 119 | public static function contains($searches, string $subject): bool 120 | { 121 | $searches = Arr::wrap($searches); 122 | 123 | foreach ($searches as $search) { 124 | if (strpos($subject, $search) !== false) { 125 | return true; 126 | } 127 | } 128 | 129 | return false; 130 | } 131 | 132 | /** 133 | * Check if the given string contains with (one of) the given regex. 134 | * 135 | * @param string|string[] $expressions 136 | * @param string $subject 137 | * 138 | * @return bool 139 | */ 140 | public static function containsRegex($expressions, string $subject): bool 141 | { 142 | $expressions = Arr::wrap($expressions); 143 | 144 | foreach ($expressions as $expression) { 145 | if (preg_match('/'.$expression.'/i', $subject)) { 146 | return true; 147 | } 148 | } 149 | 150 | return false; 151 | } 152 | 153 | /** 154 | * Check if the given string starts with (one of) the given search. 155 | * 156 | * @param string|string[] $searches 157 | * @param string $subject 158 | * 159 | * @return bool 160 | */ 161 | public static function startsWith($searches, string $subject): bool 162 | { 163 | $searches = Arr::wrap($searches); 164 | 165 | foreach ($searches as $search) { 166 | if (strpos($subject, $search) === 0) { 167 | return true; 168 | } 169 | } 170 | 171 | return false; 172 | } 173 | 174 | /** 175 | * Check if the given string ends with (one of) the given search. 176 | * 177 | * @param string|string[] $searches 178 | * @param string $subject 179 | * 180 | * @return bool 181 | */ 182 | public static function endsWith($searches, string $subject): bool 183 | { 184 | $searches = Arr::wrap($searches); 185 | 186 | foreach ($searches as $search) { 187 | if (substr($subject, -strlen($search)) === $search) { 188 | return true; 189 | } 190 | } 191 | 192 | return false; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/Generators/Tests/Basic/BasicMethodFactory.php: -------------------------------------------------------------------------------- 1 | 21 | * @author Killian Hascoët 22 | * @license MIT 23 | */ 24 | class BasicMethodFactory extends MethodFactory 25 | { 26 | use ManagesGetterAndSetter; 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function makeTestable(TestClass $class, ReflectionMethod $reflectionMethod): void 32 | { 33 | if ($this->isGetter($reflectionMethod)) { 34 | $this->handleGetterMethod($class, $reflectionMethod); 35 | 36 | return; 37 | } 38 | 39 | if ($this->isSetter($reflectionMethod)) { 40 | $this->handleSetterMethod($class, $reflectionMethod); 41 | 42 | return; 43 | } 44 | 45 | throw new InvalidArgumentException( 46 | "cannot generate tests for method {$reflectionMethod->getShortName()}, not a getter or a setter" 47 | ); 48 | } 49 | 50 | /** 51 | * Handle a getter method to generate tests for it. 52 | * 53 | * @param TestClass $class 54 | * @param ReflectionMethod $reflectionMethod 55 | */ 56 | protected function handleGetterMethod(TestClass $class, ReflectionMethod $reflectionMethod): void 57 | { 58 | $method = $this->handlePropertyMethod($class, $reflectionMethod, Reflect::returnType($reflectionMethod), 'get'); 59 | 60 | [$callTarget, $actualValueTarget] = $this->getCallTargetAndValueTarget($class, $reflectionMethod); 61 | 62 | $method->addStatement( 63 | new TestStatement('$property->setValue('.$actualValueTarget.', $expected)') 64 | ); 65 | $method->addStatement( 66 | $this->statementFactory->makeAssert('same', '$expected', $callTarget.$reflectionMethod->getShortName().'()') 67 | ); 68 | } 69 | 70 | /** 71 | * Handle a getter method to generate tests for it. 72 | * 73 | * @param TestClass $class 74 | * @param ReflectionMethod $reflectionMethod 75 | */ 76 | protected function handleSetterMethod(TestClass $class, ReflectionMethod $reflectionMethod): void 77 | { 78 | /** @var ReflectionParameter|null $reflectionParameter */ 79 | $reflectionParameter = Reflect::parameters($reflectionMethod)->first(); 80 | $type = $reflectionParameter ? Reflect::parameterType($reflectionParameter) : null; 81 | 82 | $method = $this->handlePropertyMethod($class, $reflectionMethod, $type, 'set'); 83 | 84 | [$callTarget, $actualValueTarget] = $this->getCallTargetAndValueTarget($class, $reflectionMethod); 85 | 86 | $method->addStatement( 87 | new TestStatement($callTarget.$reflectionMethod->getShortName().'($expected)') 88 | ); 89 | $method->addStatement( 90 | $this->statementFactory->makeAssert('same', '$expected', '$property->getValue('.$actualValueTarget.')') 91 | ); 92 | } 93 | 94 | /** 95 | * Get the target to call the tested method and the target to define or get the value to test. 96 | * 97 | * @param TestClass $class 98 | * @param ReflectionMethod $reflectionMethod 99 | * 100 | * @return array 101 | */ 102 | private function getCallTargetAndValueTarget(TestClass $class, ReflectionMethod $reflectionMethod): array 103 | { 104 | $className = $class->getReflectionClass()->getShortName(); 105 | 106 | if ($reflectionMethod->isStatic()) { 107 | $callTarget = $class->getReflectionClass()->getShortName().'::'; 108 | $actualValueTarget = 'null'; 109 | } else { 110 | $callTarget = '$this->'.lcfirst($className).'->'; 111 | $actualValueTarget = '$this->'.lcfirst($className); 112 | } 113 | 114 | return [$callTarget, $actualValueTarget]; 115 | } 116 | 117 | /** 118 | * Handle a method for a property to create expected value and reflection property instantiation. 119 | * 120 | * @param TestClass $class 121 | * @param ReflectionMethod $reflectionMethod 122 | * @param ReflectionType|null $reflectionType 123 | * @param string $prefix 124 | * 125 | * @return TestMethod 126 | */ 127 | private function handlePropertyMethod( 128 | TestClass $class, 129 | ReflectionMethod $reflectionMethod, 130 | ?ReflectionType $reflectionType, 131 | string $prefix 132 | ): TestMethod { 133 | $method = $this->makeEmpty($reflectionMethod); 134 | $class->addMethod($method); 135 | 136 | $reflectionProperty = $this->getPropertyFromMethod($reflectionMethod, $prefix); 137 | 138 | // Expected value variable creation. 139 | $expectedStatement = (new TestStatement('$expected = ')) 140 | ->append($this->valueFactory->make($method->getTestClass(), $reflectionType)); 141 | $method->addStatement($expectedStatement); 142 | 143 | // Reflected property variable creation. 144 | $reflectionClassImport = $this->importFactory->make($method->getTestClass(), 'ReflectionClass'); 145 | $propertyStatement = (new TestStatement('$property = (new ')) 146 | ->append($reflectionClassImport->getFinalName()) 147 | ->append('(') 148 | ->append($class->getReflectionClass()->getShortName()) 149 | ->append('::class))') 150 | ->addLine('->getProperty(\'') 151 | ->append($reflectionProperty->getName()) 152 | ->append('\')'); 153 | $method->addStatement($propertyStatement); 154 | 155 | return $method; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Config/Config.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Killian Hascoët 15 | * @license MIT 16 | */ 17 | class Config implements ConfigContract 18 | { 19 | /** 20 | * The type for string properties. 21 | */ 22 | protected const TYPE_STRING = 'string'; 23 | 24 | /** 25 | * The type for boolean properties. 26 | */ 27 | protected const TYPE_BOOL = 'bool'; 28 | 29 | /** 30 | * The type for array properties. 31 | */ 32 | protected const TYPE_ARRAY = 'array'; 33 | 34 | /** 35 | * The properties of the config with there type hint. 36 | */ 37 | protected const PROPERTIES = [ 38 | 'automaticGeneration' => self::TYPE_BOOL, 39 | 'implementations' => self::TYPE_ARRAY, 40 | 'baseNamespace' => self::TYPE_STRING, 41 | 'baseTestNamespace' => self::TYPE_STRING, 42 | 'testCase' => self::TYPE_STRING, 43 | 'testClassFinal' => self::TYPE_BOOL, 44 | 'testClassStrictTypes' => self::TYPE_BOOL, 45 | 'testClassTypedProperties' => self::TYPE_BOOL, 46 | 'excludedMethods' => self::TYPE_ARRAY, 47 | 'mergedPhpDoc' => self::TYPE_ARRAY, 48 | 'phpDoc' => self::TYPE_ARRAY, 49 | 'phpHeaderDoc' => self::TYPE_STRING, 50 | 'options' => self::TYPE_ARRAY, 51 | ]; 52 | 53 | /** 54 | * @var array The configuration, as an array. 55 | */ 56 | protected $config; 57 | 58 | /** 59 | * Validate the given config and create a new instance. 60 | * 61 | * @param array $config 62 | * 63 | * @return static 64 | */ 65 | public static function make(array $config = []) 66 | { 67 | $config = static::validate($config); 68 | 69 | return new static( 70 | array_merge( 71 | static::getDefaultConfig(), 72 | $config, 73 | ), 74 | ); 75 | } 76 | 77 | /** 78 | * Validate the given config properties and the cleaned config array. 79 | * 80 | * @param array $config 81 | * 82 | * @return array 83 | */ 84 | public static function validate(array $config): array 85 | { 86 | $validated = []; 87 | 88 | foreach (static::PROPERTIES as $property => $type) { 89 | $value = $config[$property] ?? null; 90 | 91 | if ($value === null) { 92 | continue; 93 | } 94 | 95 | if (! call_user_func('is_'.$type, $value)) { 96 | throw new InvalidArgumentException( 97 | "configuration property {$property} must be of type {$type}", 98 | ); 99 | } 100 | 101 | $validated[$property] = $value; 102 | } 103 | 104 | return $validated; 105 | } 106 | 107 | /** 108 | * Retrieve the default configuration as an array. 109 | * 110 | * @return array 111 | */ 112 | protected static function getDefaultConfig(): array 113 | { 114 | return require __DIR__.'/../../config/phpunitgen.php'; 115 | } 116 | 117 | /** 118 | * Config constructor. 119 | * 120 | * @param array $config 121 | */ 122 | protected function __construct(array $config = []) 123 | { 124 | $this->config = $config; 125 | } 126 | 127 | /** 128 | * {@inheritdoc} 129 | */ 130 | public function automaticGeneration(): bool 131 | { 132 | return $this->config['automaticGeneration']; 133 | } 134 | 135 | /** 136 | * {@inheritdoc} 137 | */ 138 | public function implementations(): array 139 | { 140 | return $this->config['implementations']; 141 | } 142 | 143 | /** 144 | * {@inheritdoc} 145 | */ 146 | public function baseNamespace(): string 147 | { 148 | return $this->config['baseNamespace']; 149 | } 150 | 151 | /** 152 | * {@inheritdoc} 153 | */ 154 | public function baseTestNamespace(): string 155 | { 156 | return $this->config['baseTestNamespace']; 157 | } 158 | 159 | /** 160 | * {@inheritdoc} 161 | */ 162 | public function testCase(): string 163 | { 164 | return $this->config['testCase']; 165 | } 166 | 167 | /** 168 | * {@inheritdoc} 169 | */ 170 | public function testClassFinal(): bool 171 | { 172 | return $this->config['testClassFinal']; 173 | } 174 | 175 | /** 176 | * {@inheritdoc} 177 | */ 178 | public function testClassStrictTypes(): bool 179 | { 180 | return $this->config['testClassStrictTypes']; 181 | } 182 | 183 | /** 184 | * {@inheritdoc} 185 | */ 186 | public function testClassTypedProperties(): bool 187 | { 188 | return $this->config['testClassTypedProperties']; 189 | } 190 | 191 | /** 192 | * {@inheritdoc} 193 | */ 194 | public function excludedMethods(): array 195 | { 196 | return $this->config['excludedMethods']; 197 | } 198 | 199 | /** 200 | * {@inheritdoc} 201 | */ 202 | public function mergedPhpDoc(): array 203 | { 204 | return $this->config['mergedPhpDoc']; 205 | } 206 | 207 | /** 208 | * {@inheritdoc} 209 | */ 210 | public function phpDoc(): array 211 | { 212 | return $this->config['phpDoc']; 213 | } 214 | 215 | /** 216 | * {@inheritdoc} 217 | */ 218 | public function phpHeaderDoc(): string 219 | { 220 | return $this->config['phpHeaderDoc']; 221 | } 222 | 223 | /** 224 | * {@inheritdoc} 225 | */ 226 | public function options(): array 227 | { 228 | return $this->config['options']; 229 | } 230 | 231 | /** 232 | * {@inheritdoc} 233 | */ 234 | public function getOption(string $name, $default = null) 235 | { 236 | return $this->options()[$name] ?? $default; 237 | } 238 | 239 | /** 240 | * {@inheritdoc} 241 | */ 242 | public function toArray(): array 243 | { 244 | return $this->config; 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /config/phpunitgen.php: -------------------------------------------------------------------------------- 1 | true, 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Contract implementations to use. 19 | | 20 | | Tells which implementation you want to use when PhpUnitGen requires a 21 | | specific contract. Please see 22 | | https://phpunitgen.io/docs#/en/configuration?id=implementations-to-use 23 | |-------------------------------------------------------------------------- 24 | */ 25 | 'implementations' => DelegateTestGenerator::implementations(), 26 | 27 | /* 28 | |-------------------------------------------------------------------------- 29 | | Base Namespace of source code. 30 | | 31 | | This string will be removed from the test class namespace. 32 | |-------------------------------------------------------------------------- 33 | */ 34 | 'baseNamespace' => 'App', 35 | 36 | /* 37 | |-------------------------------------------------------------------------- 38 | | Base Namespace of tests. 39 | | 40 | | This string will be prepend to the test class namespace. 41 | |-------------------------------------------------------------------------- 42 | */ 43 | 'baseTestNamespace' => 'Tests', 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | Test Case. 48 | | 49 | | The absolute class name to TestCase. 50 | |-------------------------------------------------------------------------- 51 | */ 52 | 'testCase' => 'Tests\\TestCase', 53 | 54 | /* 55 | |-------------------------------------------------------------------------- 56 | | Test class final. 57 | | 58 | | Tells if the test class should be final. 59 | |-------------------------------------------------------------------------- 60 | */ 61 | 'testClassFinal' => true, 62 | 63 | /* 64 | |-------------------------------------------------------------------------- 65 | | Test class strict types. 66 | | 67 | | Tells if the test class should declare strict types. 68 | |-------------------------------------------------------------------------- 69 | */ 70 | 'testClassStrictTypes' => false, 71 | 72 | /* 73 | |-------------------------------------------------------------------------- 74 | | Test class typed properties. 75 | | 76 | | Tells if the test class properties should be typed or documented. 77 | |-------------------------------------------------------------------------- 78 | */ 79 | 'testClassTypedProperties' => true, 80 | 81 | /* 82 | |-------------------------------------------------------------------------- 83 | | Excluded methods. 84 | | 85 | | Those methods will not have tests or skeleton generation. This must be an 86 | | array of RegExp compatible with "preg_match", but without the opening and 87 | | closing "/", as they will be added automatically. 88 | |-------------------------------------------------------------------------- 89 | */ 90 | 'excludedMethods' => [ 91 | '__construct', 92 | '__destruct', 93 | ], 94 | 95 | /* 96 | |-------------------------------------------------------------------------- 97 | | Merged PHP documentation tags. 98 | | 99 | | Those tags will be retrieved from tested class documentation, and appends 100 | | to the test class documentation. 101 | |-------------------------------------------------------------------------- 102 | */ 103 | 'mergedPhpDoc' => [ 104 | 'author', 105 | 'copyright', 106 | 'license', 107 | 'version', 108 | ], 109 | 110 | /* 111 | |-------------------------------------------------------------------------- 112 | | PHP documentation lines. 113 | | 114 | | Those complete documentation line (such as "@author John Doe") will be 115 | | added to the test class documentation. 116 | |-------------------------------------------------------------------------- 117 | */ 118 | 'phpDoc' => [], 119 | 120 | /* 121 | |-------------------------------------------------------------------------- 122 | | PHP header documentation lines. 123 | | 124 | | The documentation header to append to generated files. 125 | | Should be a full documentation content (with lines breaks, opening tags, 126 | | etc.) or an empty string to disable printing a documentation header. 127 | |-------------------------------------------------------------------------- 128 | */ 129 | 'phpHeaderDoc' => '', 130 | 131 | /* 132 | |-------------------------------------------------------------------------- 133 | | Options. 134 | | 135 | | This property is for generator's specific configurations. It might 136 | | contains any other useful information for test generation. 137 | |-------------------------------------------------------------------------- 138 | */ 139 | 'options' => [ 140 | /* 141 | |---------------------------------------------------------------------- 142 | | Context. 143 | | 144 | | Tells the DelegateTestGenerator (default one) that we are in a 145 | | specific project context. If defined to "null", it will used basic 146 | | generators. If set to "laravel", it will use the Laravel tests 147 | | generators. 148 | |---------------------------------------------------------------------- 149 | */ 150 | 'context' => 'laravel', 151 | 152 | /* 153 | |---------------------------------------------------------------------- 154 | | Laravel Options. 155 | | 156 | | Those options are used by Laravel Test Generators and are nested in 157 | | a "laravel." namespace. 158 | | - "user" is the class of User Eloquent model, since it will be used 159 | | in many tests. 160 | |---------------------------------------------------------------------- 161 | */ 162 | // 'laravel.user' => 'App\\User', 163 | ], 164 | ]; 165 | -------------------------------------------------------------------------------- /src/Generators/Factories/MethodFactory.php: -------------------------------------------------------------------------------- 1 | 30 | * @author Killian Hascoët 31 | * @license MIT 32 | */ 33 | class MethodFactory implements 34 | MethodFactoryContract, 35 | DocumentationFactoryAware, 36 | ImportFactoryAware, 37 | StatementFactoryAware, 38 | ValueFactoryAware 39 | { 40 | use DocumentationFactoryAwareTrait; 41 | use ImportFactoryAwareTrait; 42 | use StatementFactoryAwareTrait; 43 | use ValueFactoryAwareTrait; 44 | use InstantiatesClass; 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function makeSetUp(TestClass $class): TestMethod 50 | { 51 | $method = new TestMethod('setUp', 'protected'); 52 | 53 | $this->makeMethodInherited($method); 54 | 55 | $constructor = $this->getConstructor($class->getReflectionClass()); 56 | if (! $constructor) { 57 | $this->completeSetUpWithoutConstructor($class, $method); 58 | } else { 59 | $this->completeSetUpWithConstructor($class, $method, $constructor); 60 | } 61 | 62 | return $method; 63 | } 64 | 65 | /** 66 | * {@inheritdoc} 67 | */ 68 | public function makeTearDown(TestClass $class): TestMethod 69 | { 70 | $method = new TestMethod('tearDown', 'protected'); 71 | 72 | $this->makeMethodInherited($method); 73 | 74 | $properties = $class->getProperties(); 75 | if ($properties->isEmpty()) { 76 | $method->addStatement( 77 | $this->statementFactory->makeTodo('Complete the tearDown() method.') 78 | ); 79 | 80 | return $method; 81 | } 82 | 83 | $properties->each(function (TestProperty $property) use ($method) { 84 | $method->addStatement( 85 | new TestStatement("unset(\$this->{$property->getName()})") 86 | ); 87 | }); 88 | 89 | return $method; 90 | } 91 | 92 | /** 93 | * {@inheritdoc} 94 | */ 95 | public function makeEmpty(ReflectionMethod $reflectionMethod, string $suffix = ''): TestMethod 96 | { 97 | return new TestMethod( 98 | $this->makeTestMethodName($reflectionMethod).$suffix 99 | ); 100 | } 101 | 102 | /** 103 | * {@inheritdoc} 104 | */ 105 | public function makeIncomplete(ReflectionMethod $reflectionMethod): TestMethod 106 | { 107 | $method = new TestMethod( 108 | $this->makeTestMethodName($reflectionMethod) 109 | ); 110 | 111 | $method->addStatement($this->statementFactory->makeTodo('This test is incomplete.')); 112 | $method->addStatement(new TestStatement('self::markTestIncomplete()')); 113 | 114 | return $method; 115 | } 116 | 117 | /** 118 | * {@inheritdoc} 119 | */ 120 | public function makeTestable(TestClass $class, ReflectionMethod $reflectionMethod): void 121 | { 122 | $class->addMethod($this->makeIncomplete($reflectionMethod)); 123 | } 124 | 125 | /** 126 | * Get the test method short name. 127 | * 128 | * @param ReflectionMethod $reflectionMethod 129 | * 130 | * @return string 131 | */ 132 | protected function makeTestMethodName(ReflectionMethod $reflectionMethod): string 133 | { 134 | return 'test'.ucfirst($reflectionMethod->getShortName()); 135 | } 136 | 137 | /** 138 | * Add the call to parent method with a new empty line after it. 139 | * 140 | * @param TestMethod $method 141 | */ 142 | protected function makeMethodInherited(TestMethod $method): void 143 | { 144 | $method->setDocumentation( 145 | $this->documentationFactory->makeForInheritedMethod($method) 146 | ); 147 | 148 | $callStatement = (new TestStatement('parent::')) 149 | ->append($method->getName()) 150 | ->append('()'); 151 | 152 | $method->addStatement($callStatement) 153 | ->addStatement(new TestStatement('')); 154 | } 155 | 156 | /** 157 | * Complete the "setUp" method without knowing the constructor. 158 | * 159 | * @param TestClass $class 160 | * @param TestMethod $method 161 | */ 162 | protected function completeSetUpWithoutConstructor(TestClass $class, TestMethod $method): void 163 | { 164 | $method->addStatement( 165 | $this->statementFactory->makeTodo('Correctly instantiate tested object to use it.') 166 | ); 167 | $method->addStatement( 168 | $this->statementFactory->makeInstantiation($class, new Collection()) 169 | ); 170 | } 171 | 172 | /** 173 | * Complete the "setUp" method when knowing the constructor. 174 | * 175 | * @param TestClass $class 176 | * @param TestMethod $method 177 | * @param ReflectionMethod $constructor 178 | */ 179 | protected function completeSetUpWithConstructor( 180 | TestClass $class, 181 | TestMethod $method, 182 | ReflectionMethod $constructor 183 | ): void { 184 | $parameters = Reflect::parameters($constructor) 185 | ->each(function (ReflectionParameter $reflectionParameter) use ($class, $method) { 186 | $value = $this->valueFactory->make($class, Reflect::parameterType($reflectionParameter)); 187 | 188 | $method->addStatement( 189 | $this->statementFactory->makeAffect($reflectionParameter->getName(), $value) 190 | ); 191 | }); 192 | 193 | $method->addStatement( 194 | $this->statementFactory->makeInstantiation($class, $parameters) 195 | ); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/Generators/Tests/DelegateTestGenerator.php: -------------------------------------------------------------------------------- 1 | 35 | * @author Killian Hascoët 36 | * @license MIT 37 | */ 38 | class DelegateTestGenerator implements DelegateTestGeneratorContract, ConfigAware 39 | { 40 | use ConfigAwareTrait; 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public static function implementations(): array 46 | { 47 | return [ 48 | TestGenerator::class => static::class, 49 | ]; 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function generate(ReflectionClass $reflectionClass): TestClass 56 | { 57 | return $this->getDelegate($reflectionClass)->generate($reflectionClass); 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | public function canGenerateFor(ReflectionClass $reflectionClass): bool 64 | { 65 | return true; 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function getDelegate(ReflectionClass $reflectionClass): TestGenerator 72 | { 73 | $testGeneratorClass = $this->chooseTestGenerator($reflectionClass); 74 | $config = $this->makeNewConfiguration($testGeneratorClass); 75 | 76 | return $this->makeNewContainer($config)->get(TestGenerator::class); 77 | } 78 | 79 | /** 80 | * {@inheritdoc} 81 | */ 82 | public function getClassFactory(): ClassFactory 83 | { 84 | throw new RuntimeException( 85 | 'getClassFactory method should not be called on a DelegateTestGenerator' 86 | ); 87 | } 88 | 89 | /** 90 | * Choose TestGenerator which should be used for the given reflection class. 91 | * 92 | * @param ReflectionClass $reflectionClass 93 | * 94 | * @return string 95 | */ 96 | protected function chooseTestGenerator(ReflectionClass $reflectionClass): string 97 | { 98 | if ($this->isLaravelContext()) { 99 | return $this->chooseTestGeneratorForLaravel($reflectionClass); 100 | } 101 | 102 | return BasicTestGenerator::class; 103 | } 104 | 105 | /** 106 | * Check if the context of class is a Laravel project. 107 | * 108 | * @return bool 109 | */ 110 | protected function isLaravelContext(): bool 111 | { 112 | return $this->config->getOption('context') === 'laravel'; 113 | } 114 | 115 | /** 116 | * Choose TestGenerator which should be used for the given reflection class in a Laravel context. 117 | * 118 | * @param ReflectionClass $reflectionClass 119 | * 120 | * @return string 121 | */ 122 | protected function chooseTestGeneratorForLaravel(ReflectionClass $reflectionClass): string 123 | { 124 | $reflectionClassName = $reflectionClass->getName(); 125 | foreach ($this->getNamespaceGeneratorMappingForLaravel() as $namespace => $generator) { 126 | if (Str::contains($namespace, $reflectionClassName)) { 127 | return $generator; 128 | } 129 | } 130 | 131 | return LaravelTestGenerator::class; 132 | } 133 | 134 | /** 135 | * Get the mapping between the namespace the class should be in and the associated generator. 136 | * 137 | * @return array 138 | */ 139 | protected function getNamespaceGeneratorMappingForLaravel(): array 140 | { 141 | return [ 142 | '\\Broadcasting\\' => ChannelTestGenerator::class, 143 | '\\Console\\Commands\\' => CommandTestGenerator::class, 144 | '\\Http\\Controllers\\' => ControllerTestGenerator::class, 145 | '\\Jobs\\' => JobTestGenerator::class, 146 | '\\Listeners\\' => ListenerTestGenerator::class, 147 | '\\Policies\\' => PolicyTestGenerator::class, 148 | '\\Http\\Resources\\' => ResourceTestGenerator::class, 149 | '\\Rules\\' => RuleTestGenerator::class, 150 | ]; 151 | } 152 | 153 | /** 154 | * Make the new config with chosen generator. 155 | * 156 | * @param string $testGeneratorClass 157 | * 158 | * @return ConfigContract 159 | */ 160 | protected function makeNewConfiguration(string $testGeneratorClass): ConfigContract 161 | { 162 | $implementationsKey = 'implementations'; 163 | 164 | $configArray = $this->config->toArray(); 165 | $oldImplementations = $configArray[$implementationsKey]; 166 | 167 | $configArray[$implementationsKey] = call_user_func([ 168 | $testGeneratorClass, 169 | $implementationsKey, 170 | ]); 171 | unset($oldImplementations[TestGenerator::class]); 172 | $configArray[$implementationsKey] = array_merge( 173 | $configArray[$implementationsKey], 174 | $oldImplementations 175 | ); 176 | 177 | return Config::make($configArray); 178 | } 179 | 180 | /** 181 | * Make the new container from the new configuration. 182 | * 183 | * @param ConfigContract $config 184 | * 185 | * @return ContainerInterface 186 | */ 187 | protected function makeNewContainer(ConfigContract $config): ContainerInterface 188 | { 189 | return CoreContainerFactory::make($config); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/Helpers/Reflect.php: -------------------------------------------------------------------------------- 1 | 25 | * @author Killian Hascoët 26 | * @license MIT 27 | */ 28 | class Reflect 29 | { 30 | /** 31 | * @var DocBlockFactoryInterface The doc block factory to parse doc blocks. 32 | */ 33 | protected static $docBlockFactory; 34 | 35 | /** 36 | * @return DocBlockFactoryInterface 37 | */ 38 | protected static function getDocBlockFactory(): DocBlockFactoryInterface 39 | { 40 | return self::$docBlockFactory ?? DocBlockFactory::createInstance(); 41 | } 42 | 43 | /** 44 | * @param DocBlockFactoryInterface $docBlockFactory 45 | */ 46 | public static function setDocBlockFactory(?DocBlockFactoryInterface $docBlockFactory): void 47 | { 48 | self::$docBlockFactory = $docBlockFactory; 49 | } 50 | 51 | /** 52 | * Get the immediate methods for the given reflection class. 53 | * 54 | * @param ReflectionClass $reflectionClass 55 | * 56 | * @return ReflectionMethod[]|Collection 57 | */ 58 | public static function methods(ReflectionClass $reflectionClass): Collection 59 | { 60 | return new Collection($reflectionClass->getImmediateMethods()); 61 | } 62 | 63 | /** 64 | * Get the immediate method matching the given name. 65 | * 66 | * @param ReflectionClass $reflectionClass 67 | * @param string $name 68 | * 69 | * @return ReflectionMethod|null 70 | */ 71 | public static function method(ReflectionClass $reflectionClass, string $name): ?ReflectionMethod 72 | { 73 | return self::methods($reflectionClass) 74 | ->first(function (ReflectionMethod $reflectionMethod) use ($name) { 75 | return $reflectionMethod->getShortName() === $name; 76 | }); 77 | } 78 | 79 | /** 80 | * Get the properties for the given reflection class. 81 | * 82 | * @param ReflectionClass $reflectionClass 83 | * 84 | * @return ReflectionProperty[]|Collection 85 | */ 86 | public static function properties(ReflectionClass $reflectionClass): Collection 87 | { 88 | return new Collection($reflectionClass->getImmediateProperties()); 89 | } 90 | 91 | /** 92 | * Get the immediate property matching the given name. 93 | * 94 | * @param ReflectionClass $reflectionClass 95 | * @param string $name 96 | * 97 | * @return ReflectionProperty|null 98 | */ 99 | public static function property(ReflectionClass $reflectionClass, string $name): ?ReflectionProperty 100 | { 101 | return self::properties($reflectionClass) 102 | ->first(function (ReflectionProperty $reflectionProperty) use ($name) { 103 | return $reflectionProperty->getName() === $name; 104 | }); 105 | } 106 | 107 | /** 108 | * Get the parameters for the given reflection method. 109 | * 110 | * @param ReflectionMethod $reflectionMethod 111 | * 112 | * @return ReflectionParameter[]|Collection 113 | */ 114 | public static function parameters(ReflectionMethod $reflectionMethod): Collection 115 | { 116 | return new Collection($reflectionMethod->getParameters()); 117 | } 118 | 119 | /** 120 | * Get the parameter type using Reflection or DocBlock. 121 | * 122 | * @param ReflectionParameter $reflectionParameter 123 | * 124 | * @return ReflectionType|null 125 | */ 126 | public static function parameterType(ReflectionParameter $reflectionParameter): ?ReflectionType 127 | { 128 | return ReflectionType::make( 129 | $reflectionParameter->getType(), 130 | self::convertDocBlockTagToTypes( 131 | self::docBlockTags($reflectionParameter->getDeclaringFunction()) 132 | ->first(function ($paramTag) use ($reflectionParameter) { 133 | return $paramTag instanceof DocBlock\Tags\Param 134 | && $paramTag->getVariableName() === $reflectionParameter->getName(); 135 | }) 136 | ) 137 | ); 138 | } 139 | 140 | /** 141 | * Get the return type of a method using Reflection or DocBlock. 142 | * 143 | * @param ReflectionMethod $reflectionMethod 144 | * 145 | * @return ReflectionType|null 146 | */ 147 | public static function returnType(ReflectionMethod $reflectionMethod): ?ReflectionType 148 | { 149 | return ReflectionType::make( 150 | $reflectionMethod->getReturnType(), 151 | self::convertDocBlockTagToTypes( 152 | self::docBlockTags($reflectionMethod) 153 | ->first(function ($returnTag) { 154 | return $returnTag instanceof DocBlock\Tags\Return_; 155 | }) 156 | ) 157 | ); 158 | } 159 | 160 | /** 161 | * Get the doc block object from the given reflection object (might be a class, method...). 162 | * 163 | * @param ReflectionMethod|ReflectionClass $reflectionObject 164 | * 165 | * @return DocBlock|null 166 | */ 167 | public static function docBlock(ReflectionMethod|ReflectionClass $reflectionObject): ?DocBlock 168 | { 169 | $docComment = $reflectionObject->getDocComment() ?? ''; 170 | 171 | return $docComment !== '' 172 | ? self::getDocBlockFactory()->create($docComment, self::docBlockContext($reflectionObject)) 173 | : null; 174 | } 175 | 176 | /** 177 | * Get the doc block tags from the given reflection object (might be a class, method...). 178 | * 179 | * @param ReflectionMethod|ReflectionClass $reflectionObject 180 | * 181 | * @return Collection 182 | */ 183 | public static function docBlockTags(ReflectionMethod|ReflectionClass $reflectionObject): Collection 184 | { 185 | $docBlock = self::docBlock($reflectionObject); 186 | 187 | return new Collection($docBlock ? $docBlock->getTags() : []); 188 | } 189 | 190 | /** 191 | * Convert the doc block tag to an array of types. 192 | * 193 | * @param DocBlock\Tags\TagWithType|null $tag 194 | * 195 | * @return array 196 | */ 197 | private static function convertDocBlockTagToTypes(?DocBlock\Tags\TagWithType $tag): array 198 | { 199 | return $tag ? explode('|', (string) $tag->getType()) : []; 200 | } 201 | 202 | /** 203 | * Build the doc block parsing context from a reflection object. 204 | * 205 | * @param ReflectionMethod|ReflectionClass $reflectionObject 206 | * 207 | * @return Context 208 | */ 209 | private static function docBlockContext(ReflectionMethod|ReflectionClass $reflectionObject): Context 210 | { 211 | $reflectionClass = $reflectionObject instanceof ReflectionMethod 212 | ? $reflectionObject->getDeclaringClass() 213 | : $reflectionObject; 214 | 215 | $contextFactory = new ContextFactory(); 216 | 217 | return $contextFactory->createForNamespace( 218 | $reflectionClass->getNamespaceName() ?? '', 219 | $reflectionClass->getLocatedSource()->getSource(), 220 | ); 221 | } 222 | } 223 | --------------------------------------------------------------------------------