├── .editorconfig ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── bin └── test-generator ├── box.json.dist ├── composer.json ├── composer.lock ├── doc ├── phpstorm-integration.png └── testgenerator-logo.png ├── infection.json.dist ├── phpstan.neon ├── phpunit.xml.dist ├── src ├── ClassAnalyser.php ├── Clazz.php ├── Dependency.php ├── DependencyContainer.php ├── InvalidFullyQualifiedNameException.php ├── NotAPhpFileException.php ├── Output │ ├── Exception │ │ ├── InvalidFileException.php │ │ ├── SubjectNotInSrcBaseException.php │ │ └── UnableToWriteTestFileException.php │ ├── FileWriter.php │ ├── OutputProcessor.php │ ├── OutputProcessorFactory.php │ └── StdoutWriter.php ├── PhpFile.php ├── TemplateConfiguration.php ├── TestGenerator.php └── TwigRenderer.php ├── templates └── phpunit.twig ├── tests ├── functional │ ├── FunctionalBaseTest.php │ ├── backlog │ │ ├── AliasFlag │ │ │ ├── arguments.txt │ │ │ ├── expected.php │ │ │ └── source.php │ │ ├── MultipleClassesPerFile │ │ │ ├── expected.php │ │ │ ├── expected2.php │ │ │ └── source.php │ │ └── Php5SupportWithMockery │ │ │ ├── expected.php │ │ │ └── source.php │ └── fixtures │ │ ├── AddsCoversAnnotation │ │ ├── arguments.txt │ │ ├── expected.php │ │ └── source.php │ │ ├── ClassWithScalarValues │ │ ├── expected.php │ │ └── source.php │ │ ├── DifferentBaseClass │ │ ├── arguments.txt │ │ ├── expected.php │ │ └── source.php │ │ ├── FormattingForFieldsAndTestSubject │ │ ├── arguments.txt │ │ ├── expected.php │ │ └── source.php │ │ ├── GeneratesSimplePhpUnitTestCase │ │ ├── expected.php │ │ └── source.php │ │ ├── MockeryMocking │ │ ├── arguments.txt │ │ ├── expected.php │ │ └── source.php │ │ └── Php5SupportWithPhpUnit5 │ │ ├── arguments.txt │ │ ├── expected.php │ │ └── source.php ├── integration │ ├── PhpBackportingTest.php │ └── TemplateTest.php ├── resources │ ├── php7-to-php55-example │ │ ├── example.php │ │ └── expected.php │ └── simple-example-project │ │ └── src │ │ └── Deep │ │ └── X.php └── unit │ ├── ClassAnalyserTest.php │ ├── ClazzTest.php │ ├── DependencyContainerTest.php │ ├── DependencyTest.php │ ├── Output │ ├── FileWriterTest.php │ ├── OutputProcessorFactoryTest.php │ └── StdoutWriterTest.php │ ├── PhpFileTest.php │ ├── TemplateConfigurationTest.php │ ├── TestGeneratorTest.php │ └── TwigRendererTest.php └── tools ├── ensure-docopt-readme-and-app-are-identical ├── githook └── remove-php7-features /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | # PHP PSR-2 Coding Standards 5 | # http://www.php-fig.org/psr/psr-2/ 6 | 7 | root = true 8 | 9 | [*.php] 10 | charset = utf-8 11 | end_of_line = lf 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | indent_style = space 15 | indent_size = 4 16 | 17 | [Makefile] 18 | indent_style = tab 19 | 20 | # Matches the exact files either package.json or .travis.yml 21 | [{package.json,.travis.yml}] 22 | indent_style = space 23 | indent_size = 2 24 | 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # PHPStorm settings, etc. 2 | .idea 3 | 4 | # composer dependencies 5 | /vendor 6 | /samples 7 | 8 | # PHP Coding Standards Fixer cache file 9 | .php_cs.cache 10 | 11 | # for now don't add any more .phar files 12 | # because they might bloat the repo 13 | *.phar 14 | 15 | # build artifacts 16 | /build 17 | humbuglog.txt 18 | 19 | tests/resources/simple-example-project/tests 20 | tests/resources/php7-to-php55-example/actual.php 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.1 5 | - 7.2 6 | - 7.3 7 | - nightly 8 | 9 | matrix: 10 | allow_failures: 11 | - php: nightly 12 | 13 | before_script: 14 | - travis_retry composer self-update 15 | - travis_retry composer update --no-interaction --prefer-source 16 | 17 | script: 18 | - vendor/bin/phpunit --coverage-clover=coverage.xml 19 | - vendor/bin/infection 20 | - tools/ensure-docopt-readme-and-app-are-identical 21 | 22 | after_success: 23 | - bash <(curl -s https://codecov.io/bash) 24 | 25 | env: 26 | global: 27 | secure: R3XOmZ+j5oCsqfYmJQcRQ+LY9x70Ht3MX3V8jd5Ze4ZqRAeod6ZdlAvk5ae8IcvhIUC/gfUZCEGDa+EWXhMRxNf4RPszK9D8Gcz/TxPT3lkZVg+zdz/Fa2vNtthCQGc+HqZU2dnWtK4RV6TH0yq0ra5ychG5fOXEKMfk28WdznPMqgQwdPthjKX6tihVtT6M5QgR40h2daN5sm7OGUPP4iorrwsAhceGzz2OZeMib30oWpblIFPBJ1Ibf7Q73+T8w8tdhA4zMYERQ4DLZUr5+V90GQNu6NZ4y2IJmDW1jKYKJS6VsfLuLXi8GoEik8SjIrgH0tzaFBQUCZej77HqphUdtasRAhRzG564gLjmWMAeuRQyiA0m8cltnCIGj+V5EEiY1D1weyTMrHa5+kEubi8RkzUHAKyxd2XTRTSPMIiJW/jzhyVEv5MpD9Ma44fu49zdbex8en3MgNvAMoH66YK79gULuU1+RDl7ulgAdlHehEZM2AQeYk37seKcAcZPWtFO47jVgR7K+dMohUxwLEGFLCkkvWSFh2nVKUiNMyeVe5Dcs6316W2dNTgGHfjWneUE3lK2gyEwAfrC3e9dxv+FytlxI+Tf8PIo5XSGYf0PkbipFT7BPe60fehFIIcxCYNteDXW93uG6g9fN+etstmWimKnKOEyRNYTCLCOsVE= 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [1.3.0] - 2018-08-25 10 | ### Added 11 | - Add `--src-base` (`-s`) and `--test-base` (`-t`) which can be used for saving the test file instead of printing to stdout 12 | 13 | ## [1.2.0] - 2017-08-28 14 | ### Added 15 | - Add formatting options via `--base-class`, `--subject-format` and `--field-format` 16 | 17 | ### Fixed 18 | - Fix style issue with classes without dependencies 19 | 20 | ## [1.1.0] - 2017-08-27 21 | ### Added 22 | - Support for namespaces, PHP5, PHPUnit5 and Mockery 23 | 24 | ## [1.0.0] - 2017-08-27 25 | ### Added 26 | - Generate PHPUnit 6 tests using PHPUnit for mocking 27 | - Backport to PHP5 via bin/remove-php7-features 28 | 29 | [Unreleased]: https://github.com/mihaeu/php-test-generator/compare/1.3.0...HEAD 30 | [1.3.0]: https://github.com/mihaeu/php-test-generator/compare/1.2.0...1.3.0 31 | [1.2.0]: https://github.com/mihaeu/php-test-generator/compare/1.1.0...1.2.0 32 | [1.1.0]: https://github.com/mihaeu/php-test-generator/compare/1.0.0...1.1.0 33 | [1.0.0]: https://github.com/mihaeu/php-test-generator/compare/0e8be99...1.0.0 34 | 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2018 Michael Haeuslmann 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NO_COLOR=\x1b[0m 2 | OK_COLOR=\x1b[32;01m 3 | ERROR_COLOR=\x1b[31;01m 4 | WARN_COLOR=\x1b[33;01m 5 | 6 | PHP=php 7 | PHP_NO_INI=php -n 8 | PHPUNIT=vendor/bin/phpunit 9 | 10 | all: check-reqs checks autoload test testdox cov humbug 11 | 12 | autoload: 13 | composer install 14 | 15 | checks: phpstan phpcs 16 | 17 | check-reqs: 18 | @echo "Verifying dev dependencies are installed ..." 19 | @test -f box.phar || { echo >&2 "Box is not installed locally"; exit 1; } 20 | @echo Ok. 21 | 22 | cov: 23 | @$(PHP) $(PHPUNIT) -c phpunit.xml.dist --coverage-text 24 | 25 | feature: 26 | @$(PHP_NO_INI) $(PHPUNIT) -c phpunit.xml.dist --testsuite=functional --testdox\ 27 | | sed 's/\[x\]/$(OK_COLOR)$\[x]$(NO_COLOR)/' \ 28 | | sed -r 's/(\[ \].+)/$(ERROR_COLOR)\1$(NO_COLOR)/' \ 29 | | sed -r 's/(^[^ ].+)/$(WARN_COLOR)\1$(NO_COLOR)/' 30 | 31 | humbug: 32 | @vendor/bin/humbug 33 | 34 | unit: 35 | $(PHP_NO_INI) $(PHPUNIT) -c phpunit.xml.dist --testsuite=unit 36 | 37 | integration: 38 | $(PHP_NO_INI) $(PHPUNIT) -c phpunit.xml.dist --testsuite=integration 39 | 40 | test: unit feature integration 41 | 42 | testdox: 43 | @$(PHP_NO_INI) $(PHPUNIT) -c phpunit.xml.dist --testdox \ 44 | | sed 's/\[x\]/$(OK_COLOR)$\[x]$(NO_COLOR)/' \ 45 | | sed -r 's/(\[ \].+)/$(ERROR_COLOR)\1$(NO_COLOR)/' \ 46 | | sed -r 's/(^[^ ].+)/$(WARN_COLOR)\1$(NO_COLOR)/' 47 | 48 | backport: 49 | find src -iname '*.php' -exec tools/remove-php7-features --write {} \; 50 | 51 | phar: 52 | @composer install --no-dev 53 | @mkdir -p build 54 | @$(PHP) box.phar build 55 | @chmod +x build/test-generator.phar 56 | @composer install 57 | 58 | phar55: backport phar 59 | git checkout -- src 60 | 61 | phpstan: 62 | @$(PHP_NO_INI) vendor/bin/phpstan analyse src tests/unit --level=4 -c phpstan.neon 63 | 64 | phpmd: 65 | @$(PHP_NO_INI) vendor/bin/phpmd src,tests/unit text cleancode,codesize,controversial,design,naming,unusedcode 66 | 67 | phpcs: 68 | @$(PHP_NO_INI) vendor/bin/phpcs --standard=PSR2 src tests/unit 69 | 70 | phpcbf: 71 | @$(PHP_NO_INI) vendor/bin/phpcbf --standard=PSR2 src tests/unit 72 | 73 | c: cov 74 | 75 | d: testdox 76 | 77 | s: style 78 | 79 | t: test 80 | 81 | f: feature 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | [![Travis branch](https://img.shields.io/travis/mihaeu/php-test-generator/develop.svg)](https://travis-ci.org/mihaeu/php-test-generator) 6 | [![Codecov branch](https://img.shields.io/codecov/c/github/mihaeu/php-test-generator/develop.svg)](https://codecov.io/gh/mihaeu/php-test-generator) 7 | [![Infection MSI](https://badge.stryker-mutator.io/github.com/mihaeu/php-test-generator/master)](https://infection.github.io) 8 | ![](https://img.shields.io/badge/PHP-7.3-brightgreen.svg) 9 | ![](https://img.shields.io/badge/PHP-7.2-brightgreen.svg) 10 | ![](https://img.shields.io/badge/PHP-7.1-yellow.svg) 11 | ![](https://img.shields.io/badge/PHP-7.0-yellow.svg) 12 | ![](https://img.shields.io/badge/PHP-5.6-yellow.svg) 13 | ![](https://img.shields.io/badge/PHP-5.5-yellow.svg) 14 | 15 | > Generate test cases for existing files 16 | 17 | ## Use Cases 18 | 19 | - PHPStorm has Apache Velocity support for file templates, but it is annoying to work with and limited 20 | - other IDEs or editors like Vim or Emacs don't have built-in code generation 21 | - somehow the test files never end up where they belong forcing you to rearrange code manually 22 | 23 | `test-generator` saves you all the tedious work of typing repetitive code when testing legacy applications. Next time you write a test for a class with too many dependencies and you start mocking away think of how much time you could've saved if you could automate this. 24 | 25 | This is where `test-generator` comes into play. Try it out, configure everything to your needs and create an alias for your shell or even better include it as an external tool in your editor/IDE ([like PHPStorm](https://www.jetbrains.com/help/phpstorm/external-tools.html)). 26 | 27 | ## Usage 28 | 29 | ### CLI 30 | 31 | ```bash 32 | bin/test-generator --help 33 | ``` 34 | 35 | ``` 36 | Test-Generator 37 | 38 | Usage: 39 | test-generator [options] [(--src-base --test-base)] 40 | 41 | Options: 42 | --php5 Generate PHP5 compatible code [default:false]. 43 | --phpunit5 Generate a test for PHPUnit 5 [default:false]. 44 | --mockery Generates mocks using Mockery [default:false]. 45 | --covers Adds the @covers annotation [default:false]. 46 | --base-class= Inherit from this base class e.g. "Example\TestCase". 47 | --subject-format= Format the field for the subject class. 48 | --field-format= Format the fields for dependencies. 49 | -s, --src-base= Base directory for source files; requires --test-base 50 | -t, --test-base= Base directory for test files; requires --src-base; writes output to that directory 51 | 52 | Format: 53 | %n Name starting with a lower-case letter. 54 | %N Name starting with an upper-case letter. 55 | %t Type starting with a lower-case letter. 56 | %T Type starting with a upper-case letter. 57 | 58 | Format Examples: 59 | "mock_%t" Customer => mock_customer 60 | "%NTest" arg => ArgTest 61 | "testClass" SomeName => TestClass 62 | ``` 63 | 64 | ### PHPStorm 65 | 66 | I recommend integrating `test-generator` as an external tool in PHPStorm. This works, because PHPStorm can pass the 67 | filename of the currently active file as an argument to `test-generator`, which will then generate and write the 68 | test to your preconfigured location. 69 | 70 | Navigate to Settings > Tools > External Tools and klick on **+**. Add the following information: 71 | 72 | | Field | Value | 73 | |-------------------|------------------------------------------------------------------------------| 74 | | Name | test-generator | 75 | | Description | Generate Test Stubs | 76 | | Program | `$PhpExecutable` | 77 | | Arguments | `vendor/bin/test-generator $FilePath$ -s base=src -t tests/unit` | 78 | | Working directory | `$ProjectFileDir$` | 79 | 80 | Remember to adjust *Program* and *Arguments* in case you are using the `.phar` file. 81 | 82 | In case you want to generate different tests with different settings and locations, simply create more external tool entries. 83 | 84 | ![How to integrate test-generator in PHPStorm](doc/phpstorm-integration.png) 85 | 86 | **Pro Tip**: Assign a shortcut to this tool, because you might end up using it a lot ;) 87 | 88 | ## Installation 89 | 90 | ### Composer (PHP 7.1+) 91 | 92 | ```bash 93 | # local install 94 | composer require "mihaeu/test-generator:^1.0" 95 | 96 | # global install 97 | composer global require "mihaeu/test-generator:^1.0" 98 | ``` 99 | 100 | ### Phar (PHP 5.5+) 101 | 102 | Since I actually need to use this on 5.5 legacy projects (should work with 5.4 as well, but didn't test for it), I also release a phar file which works for older versions: 103 | 104 | ```bash 105 | wget https://github.com/mihaeu/php-test-generator/releases/download/1.2.0/test-generator-1.2.0.phar 106 | chmod +x test-generator-1.2.0.phar 107 | ``` 108 | 109 | **Please note that by doing this we should be disgusted at ourselves for not upgrading to PHP 7.1 (soon 7.2).** 110 | 111 | ### Git 112 | 113 | ```bash 114 | git clone https://github.com/mihaeu/php-test-generator 115 | cd php-test-generator 116 | composer install 117 | bin/test-generator --help 118 | ``` 119 | 120 | If you don't have PHP 7.1 installed you can run `bin/remove-php7-features` to convert the source files. I won't however except pull requests without PHP 7.1 support. 121 | 122 | ## Example 123 | 124 | Given a PHP file like: 125 | 126 | ```php 127 | mockNodeTraverser = Mockery::mock(PhpParser\NodeTraverser::class); 180 | $this->mockDependencyInspectionVisitor = Mockery::mock(Mihaeu\PhpDependencies\Analyser\DependencyInspectionVisitor::class); 181 | $this->mockParser = Mockery::mock(Mihaeu\PhpDependencies\Analyser\Parser::class); 182 | $this->classUnderTest = new StaticAnalyser( 183 | $this->mockNodeTraverser, 184 | $this->mockDependencyInspectionVisitor, 185 | $this->mockParser 186 | ); 187 | } 188 | 189 | public function testMissing() 190 | { 191 | $this->fail('Test not yet implemented'); 192 | } 193 | } 194 | ``` 195 | 196 | ## Roadmap 197 | 198 | - avoid FQNs by default by including (`use`) all required namespaces 199 | - `--template=` for custom templates 200 | - and many more features are planned, just [check out the functional backlog](https://github.com/mihaeu/php-test-generator/tree/master/tests/functional/backlog) 201 | 202 | ## Contributing 203 | 204 | If you have any ideas for new features or are willing to contribute yourself you are more than welcome to do so. 205 | 206 | Make sure to keep the code coverage at 100% (and run humbug for mutation testing) and stick to PSR-2. The `Makefile` in the repo is making lots of assumptions and probably won't work on your machine, but it might help. 207 | 208 | ## LICENSE 209 | 210 | > Copyright (c) 2017-2019 Michael Haeuslmann 211 | > 212 | > Permission is hereby granted, free of charge, to any person obtaining a copy 213 | > of this software and associated documentation files (the "Software"), to deal 214 | > in the Software without restriction, including without limitation the rights 215 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 216 | > copies of the Software, and to permit persons to whom the Software is 217 | > furnished to do so, subject to the following conditions: 218 | > 219 | > The above copyright notice and this permission notice shall be included in all 220 | > copies or substantial portions of the Software. 221 | > 222 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 223 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 224 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 225 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 226 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 227 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 228 | > SOFTWARE. 229 | -------------------------------------------------------------------------------- /bin/test-generator: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 21 | 22 | Options: 23 | --php5 Generate PHP5 compatible code [default:false]. 24 | --phpunit5 Generate a test for PHPUnit 5 [default:false]. 25 | --mockery Generates mocks using Mockery [default:false]. 26 | --covers Adds the @covers annotation [default:false]. 27 | --base-class= Inherit from this base class e.g. "Example\TestCase". 28 | --subject-format= Format the field for the subject class. 29 | --field-format= Format the fields for dependencies. 30 | -s, --src-base= Base directory for source files; requires --test-base 31 | -t, --test-base= Base directory for test files; requires --src-base; writes output to that directory 32 | 33 | Format: 34 | %n Name starting with a lower-case letter. 35 | %N Name starting with an upper-case letter. 36 | %t Type starting with a lower-case letter. 37 | %T Type starting with a upper-case letter. 38 | 39 | Format Examples: 40 | "mock_%t" Customer => mock_customer 41 | "%NTest" arg => ArgTest 42 | "testClass" SomeName => TestClass 43 | EOT; 44 | $args = Docopt::handle($description, $argv); 45 | 46 | $container = new DependencyContainer($args); 47 | try { 48 | $container->testGenerator()->run(new PhpFile(new SplFileInfo($args['']))); 49 | } catch (Exception $exception) { 50 | echo $description 51 | . PHP_EOL 52 | . PHP_EOL 53 | . '! ' . $exception->getMessage() 54 | . PHP_EOL; 55 | exit(1); 56 | } 57 | -------------------------------------------------------------------------------- /box.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "directories": ["src/", "vendor/", "templates/"], 3 | "main": "bin/test-generator", 4 | "output": "build/test-generator.phar", 5 | "stub": true 6 | } 7 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mihaeu/test-generator", 3 | "description": "Generate test cases for existing files", 4 | "type": "library", 5 | "homepage": "https://github.com/mihaeu/php-test-generator", 6 | "keywords": [ 7 | "testing", 8 | "generation", 9 | "phpunit", 10 | "mockery" 11 | ], 12 | "bin": ["bin/test-generator"], 13 | "require": { 14 | "php": "^7.1", 15 | "nikic/php-parser": "^4.0", 16 | "docopt/docopt": "^1.0", 17 | "twig/twig": "^1.0" 18 | }, 19 | "require-dev": { 20 | "phpunit/phpunit": "^6.5", 21 | "infection/infection": "0.9.*", 22 | "squizlabs/php_codesniffer": "^3.2.3", 23 | "phpmd/phpmd": "^2.6", 24 | "phpstan/phpstan": "^0.11.0" 25 | }, 26 | "minimum-stability": "dev", 27 | "prefer-stable": true, 28 | "license": "MIT", 29 | "authors": [ 30 | { 31 | "name": "Michael Haeuslmann", 32 | "email": "haeuslmann@gmail.com" 33 | } 34 | ], 35 | "autoload": { 36 | "psr-4": { 37 | "Mihaeu\\TestGenerator\\": ["src/"] 38 | } 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "Mihaeu\\TestGenerator\\": [ 43 | "tests/unit/", 44 | "tests/functional/", 45 | "tests/integration/" 46 | ] 47 | }, 48 | "files": ["vendor/phpunit/phpunit/src/Framework/Assert/Functions.php"] 49 | }, 50 | "support": { 51 | "issues": "https://github.com/mihaeu/php-test-generator/issues" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /doc/phpstorm-integration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihaeu/php-test-generator/30b4630c996de683c19bb1d44e27fd7cb6fc74a1/doc/phpstorm-integration.png -------------------------------------------------------------------------------- /doc/testgenerator-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mihaeu/php-test-generator/30b4630c996de683c19bb1d44e27fd7cb6fc74a1/doc/testgenerator-logo.png -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "directories": [ 4 | "src" 5 | ] 6 | }, 7 | "timeout": 10, 8 | "logs": { 9 | "text": "build/infection.log", 10 | "summary": "build/summary.log", 11 | "debug": "build/debug.log", 12 | "perMutator": "build/per-mutator.md", 13 | "badge": { 14 | "branch": "master" 15 | } 16 | }, 17 | "testFramework":"phpunit" 18 | } 19 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - '#does not accept PHPUnit_Framework_MockObject_MockObject#' 4 | - '#Call to an undefined method [a-zA-Z0-9\\_]+::method\(\)#' 5 | - '#Access to an undefined property PHPUnit_Framework_MockObject_MockObject::\$[a-zA-Z0-9_]+#' 6 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | tests/unit 16 | 17 | 18 | tests/functional 19 | 20 | 21 | tests/integration 22 | 23 | 24 | 25 | 26 | 27 | src 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/ClassAnalyser.php: -------------------------------------------------------------------------------- 1 | name->name === self::CONSTRUCTOR_METHOD_NAME 30 | ) { 31 | $this->parameters = array_reduce($node->getParams(), function (array $parameters, Param $parameter) { 32 | $parameters[$parameter->var->name] = new Dependency( 33 | $parameter->var->name, 34 | $this->generateType($parameter), 35 | $this->generateDefault($parameter) 36 | ); 37 | return $parameters; 38 | }, $this->parameters); 39 | } elseif ($node instanceof Node\Stmt\Class_) { 40 | $this->class = Clazz::fromClassNode($node); 41 | } 42 | } 43 | 44 | public function getParameters() : array 45 | { 46 | return $this->parameters; 47 | } 48 | 49 | public function getClass() : ?Clazz 50 | { 51 | return $this->class; 52 | } 53 | 54 | private function generateDefault(Param $parameter) : ?string 55 | { 56 | if ($parameter->default instanceof Expr\Array_) { 57 | return self::TYPE_DEFAULT_ARRAY; 58 | } 59 | 60 | if ($parameter->default) { 61 | return $this->defaultToString($parameter->default); 62 | } 63 | 64 | if ($parameter->type === null) { 65 | return null; 66 | } 67 | 68 | $name = $parameter->type->toString(); 69 | if ($name === 'string') { 70 | return self::TYPE_DEFAULT_STRING; 71 | } 72 | 73 | if ($name === 'float') { 74 | return self::TYPE_DEFAULT_FLOAT; 75 | } 76 | 77 | if ($name === 'int') { 78 | return self::TYPE_DEFAULT_INT; 79 | } 80 | 81 | if ($name === 'bool') { 82 | return self::TYPE_DEFAULT_BOOL; 83 | } 84 | 85 | if ($name === 'array') { 86 | return self::TYPE_DEFAULT_ARRAY; 87 | } 88 | 89 | return null; 90 | } 91 | 92 | private function generateType(Param $parameter) : ?string 93 | { 94 | if ($parameter->type) { 95 | return $parameter->type->toString(); 96 | } 97 | 98 | return $this->guessTypeFromDefault($parameter); 99 | } 100 | 101 | private function defaultToString(Expr $default) : string 102 | { 103 | if ($default instanceof Expr\ConstFetch) { 104 | if (preg_match('/(false|true)/i', $default->name->toString())) { 105 | return strtolower($default->name->toString()); 106 | } 107 | return $default->name->toString(); 108 | } 109 | 110 | if (is_string($default->value)) { 111 | return "'" . $default->value . "'"; 112 | } 113 | 114 | return (string) $default->value; 115 | } 116 | 117 | private function guessTypeFromDefault(Param $parameter) : ?string 118 | { 119 | if ($parameter->default instanceof Expr\Array_) { 120 | return 'array'; 121 | } 122 | 123 | if ($parameter->default instanceof Node\Scalar\LNumber) { 124 | return 'int'; 125 | } 126 | 127 | if ($parameter->default instanceof Node\Scalar\DNumber) { 128 | return 'float'; 129 | } 130 | 131 | if ($parameter->default instanceof Node\Scalar\String_) { 132 | return 'string'; 133 | } 134 | 135 | if ($parameter->default instanceof Expr\ConstFetch 136 | && preg_match('/(true|false)/i', $parameter->default->name->toString()) 137 | ) { 138 | return 'bool'; 139 | } 140 | 141 | return null; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Clazz.php: -------------------------------------------------------------------------------- 1 | class = $class; 23 | $this->namespacedName = $namespacedName; 24 | $this->namespace = $namespace; 25 | } 26 | 27 | public function clazz(): string 28 | { 29 | return $this->class; 30 | } 31 | 32 | public static function fromClassNode(Class_ $classNode) : Clazz 33 | { 34 | $namespaceParts = $classNode->namespacedName->parts; 35 | $namespace = count($namespaceParts) 36 | ? implode('\\', array_slice($namespaceParts, 0, -1)) 37 | : ''; 38 | return new Clazz( 39 | (string) $classNode->name, 40 | implode('\\', $namespaceParts), 41 | $namespace 42 | ); 43 | } 44 | 45 | public static function fromFullyQualifiedNameString(string $fqn) : Clazz 46 | { 47 | self::assertNameIsValidPhpIdentifier($fqn); 48 | 49 | $parts = explode('\\', $fqn); 50 | $namespace = implode('\\', array_slice($parts, 0, -1)); 51 | return new Clazz($parts[count($parts) - 1], $fqn, $namespace); 52 | } 53 | 54 | private static function assertNameIsValidPhpIdentifier(string $fqn): void 55 | { 56 | if (!preg_match('/^[a-zA-Z_\x7f-\xff][\\a-zA-Z0-9_\x7f-\xff]*$/', $fqn)) { 57 | throw new InvalidFullyQualifiedNameException($fqn); 58 | } 59 | } 60 | 61 | public function toArray() : array 62 | { 63 | return [ 64 | 'class' => $this->class, 65 | 'namespacedName' => $this->namespacedName, 66 | 'namespace' => $this->namespace, 67 | ]; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Dependency.php: -------------------------------------------------------------------------------- 1 | name = $name; 23 | $this->type = $type; 24 | $this->value = $value; 25 | } 26 | 27 | public function name(): string 28 | { 29 | return $this->name; 30 | } 31 | 32 | public function type(): ?string 33 | { 34 | return $this->type; 35 | } 36 | 37 | public function value(): ?string 38 | { 39 | return $this->value; 40 | } 41 | 42 | public function isScalar() : bool 43 | { 44 | return $this->type === 'bool' 45 | || $this->type === 'int' 46 | || $this->type === 'float' 47 | || $this->type === 'array' 48 | || $this->type === 'string' 49 | || $this->type === null; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/DependencyContainer.php: -------------------------------------------------------------------------------- 1 | args = $args; 27 | } 28 | 29 | /** 30 | * @return TestGenerator 31 | * @throws InvalidFileException 32 | * @throws \Twig_Error_Loader 33 | * @throws \Twig_Error_Runtime 34 | * @throws \Twig_Error_Syntax 35 | */ 36 | public function testGenerator(): TestGenerator 37 | { 38 | return new TestGenerator( 39 | $this->parser(), 40 | new ClassAnalyser(), 41 | $this->nodeTraverser(), 42 | $this->twigRenderer(), 43 | $this->outputProcessor() 44 | ); 45 | } 46 | 47 | public function nodeTraverser() : NodeTraverser 48 | { 49 | $nodeTraverser = new NodeTraverser(); 50 | $nodeTraverser->addVisitor(new NameResolver()); 51 | return $nodeTraverser; 52 | } 53 | 54 | public function twigEnvironment() : Twig_Environment 55 | { 56 | $twig = new Twig_Environment( 57 | new Twig_Loader_Filesystem(__DIR__ . '/../templates'), 58 | ['autoescape' => false] 59 | ); 60 | $twig->addFilter($this->lcfirstFilter()); 61 | $twig->addFilter($this->isNullFilter()); 62 | $twig->addFilter($this->transformClazzFilter($this->args['--subject-format'] ?: '%t')); 63 | $twig->addFilter($this->transformDependencyFilter($this->args['--field-format'] ?: '%n')); 64 | return $twig; 65 | } 66 | 67 | public function templateConfiguration() : TemplateConfiguration 68 | { 69 | return new TemplateConfiguration( 70 | $this->baseClass(), 71 | $this->args['--php5'], 72 | $this->args['--phpunit5'], 73 | $this->args['--mockery'], 74 | $this->args['--covers'] 75 | ); 76 | } 77 | 78 | public function twigRenderer() : TwigRenderer 79 | { 80 | return new TwigRenderer($this->twigEnvironment(), $this->templateConfiguration()); 81 | } 82 | 83 | public function parser() : Parser 84 | { 85 | return (new ParserFactory())->create(ParserFactory::PREFER_PHP7); 86 | } 87 | 88 | public function lcfirstFilter(): Twig_SimpleFilter 89 | { 90 | return new Twig_SimpleFilter('lcfirst', 'lcfirst'); 91 | } 92 | 93 | public function isNullFilter(): Twig_SimpleFilter 94 | { 95 | return new Twig_SimpleFilter('isNull', function ($x) { 96 | return $x === null; 97 | }); 98 | } 99 | 100 | public function transformClazzFilter($format) : Twig_SimpleFilter 101 | { 102 | return new Twig_SimpleFilter('transformClazz', function ($x) use ($format) { 103 | return str_replace( 104 | ['%t', '%T', '%n', '%N'], 105 | [lcfirst($x), ucfirst($x), lcfirst($x), ucfirst($x)], 106 | $format 107 | ); 108 | }); 109 | } 110 | 111 | public function transformDependencyFilter($format) : Twig_SimpleFilter 112 | { 113 | return new Twig_SimpleFilter('transformDependency', function (Dependency $x) use ($format) { 114 | return str_replace( 115 | ['%t', '%T', '%n', '%N'], 116 | [lcfirst($x->type() ?: ''), ucfirst($x->type() ?: ''), lcfirst($x->name()), ucfirst($x->name())], 117 | $format 118 | ); 119 | }); 120 | } 121 | 122 | public function baseClass() : Clazz 123 | { 124 | return $this->args['--base-class'] 125 | ? Clazz::fromFullyQualifiedNameString($this->args['--base-class']) 126 | : $this->defaultBaseClass(); 127 | } 128 | 129 | /** 130 | * @throws InvalidFileException 131 | */ 132 | private function outputProcessor(): OutputProcessor 133 | { 134 | return OutputProcessorFactory::create( 135 | $this->args[''], 136 | $this->args['--src-base'] ?? null, 137 | $this->args['--test-base'] ?? null 138 | ); 139 | } 140 | 141 | private function defaultBaseClass() 142 | { 143 | if ($this->args['--php5']) { 144 | return Clazz::fromFullyQualifiedNameString('PHPUnit_Framework_TestCase'); 145 | } 146 | return Clazz::fromFullyQualifiedNameString('PHPUnit\Framework\TestCase'); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/InvalidFullyQualifiedNameException.php: -------------------------------------------------------------------------------- 1 | getPathname() . '" is not a PHP file.'); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Output/Exception/InvalidFileException.php: -------------------------------------------------------------------------------- 1 | getRealPath(), 15 | $srcBase->getRealPath() 16 | ) 17 | ); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Output/Exception/UnableToWriteTestFileException.php: -------------------------------------------------------------------------------- 1 | assertSubjectIsInSrcBase($subjectFile, $srcBase); 31 | 32 | $this->subjectFile = $subjectFile; 33 | $this->srcBase = $srcBase; 34 | $this->testBase = $testBase; 35 | } 36 | 37 | /** 38 | * @param string $output 39 | * @throws InvalidFileException 40 | * @throws UnableToWriteTestFileException 41 | */ 42 | public function write(string $output): void 43 | { 44 | $bytesWritten = @file_put_contents($this->pathToTestFile(), $output); 45 | if ($bytesWritten === false) { 46 | throw new UnableToWriteTestFileException; 47 | } 48 | } 49 | 50 | /** 51 | * @throws InvalidFileException 52 | */ 53 | private function pathToTestFile(): string 54 | { 55 | $projectPath = str_replace( 56 | $this->srcBase->getRealPath(), 57 | '', 58 | $this->subjectFile->getPathInfo()->getRealPath() 59 | ); 60 | $testDirectory = $this->testBase->getPathname() . DIRECTORY_SEPARATOR . $projectPath; 61 | if ((!mkdir($testDirectory, 0777, true) 62 | && !is_dir($testDirectory)) 63 | || !is_writable(dirname($testDirectory)) 64 | ) { 65 | throw InvalidFileException::becauseFileIsNotWritable($testDirectory); 66 | } 67 | 68 | return realpath($testDirectory) 69 | . DIRECTORY_SEPARATOR 70 | . $this->subjectFile->getBasename(self::PHP_EXTENSION) 71 | . self::TEST_FILE_EXTENSION 72 | . self::PHP_EXTENSION; 73 | } 74 | 75 | private function assertSubjectIsInSrcBase(\SplFileInfo $subjectFile, \SplFileInfo $srcBase): void 76 | { 77 | if (strpos($subjectFile->getRealPath(), $srcBase->getRealPath()) !== 0) { 78 | throw new SubjectNotInSrcBaseException($subjectFile, $srcBase); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Output/OutputProcessor.php: -------------------------------------------------------------------------------- 1 | isFile() || $file->getExtension() !== 'php') { 15 | throw new NotAPhpFileException($file); 16 | } 17 | 18 | $this->file = $file; 19 | } 20 | 21 | public function content() : string 22 | { 23 | return file_get_contents($this->file->getRealPath()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/TemplateConfiguration.php: -------------------------------------------------------------------------------- 1 | baseClass = $baseClass; 32 | $this->php5 = $php5; 33 | $this->phpunit5 = $phpunit5; 34 | $this->mockery = $mockery; 35 | $this->covers = $covers; 36 | } 37 | 38 | public function toArray() : array 39 | { 40 | return [ 41 | 'baseClass' => $this->baseClass->toArray(), 42 | 'php5' => $this->php5, 43 | 'phpunit5' => $this->phpunit5, 44 | 'mockery' => $this->mockery, 45 | 'covers' => $this->covers, 46 | ]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/TestGenerator.php: -------------------------------------------------------------------------------- 1 | parser = $parser; 34 | $this->classAnalyser = $classAnalyser; 35 | $this->nodeTraverser = $nodeTraverser; 36 | $this->nodeTraverser->addVisitor($this->classAnalyser); 37 | $this->twigRenderer = $twigRenderer; 38 | $this->outputProcessor = $outputProcessor; 39 | } 40 | 41 | public function run(PhpFile $file): void 42 | { 43 | $nodes = $this->parser->parse($file->content()); 44 | $this->nodeTraverser->traverse($nodes); 45 | 46 | if ($this->classAnalyser->getClass() === null) { 47 | return; 48 | } 49 | 50 | $this->outputProcessor->write( 51 | $this->twigRenderer->render( 52 | $this->classAnalyser->getClass(), 53 | $this->classAnalyser->getParameters() 54 | ) 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/TwigRenderer.php: -------------------------------------------------------------------------------- 1 | template = $twig->load(self::DEFAULT_TEMPLATE); 22 | $this->templateConfiguration = $templateConfiguration; 23 | } 24 | 25 | public function render(Clazz $class, array $dependencies) : string 26 | { 27 | return $this->template->render( 28 | ['dependencies' => $dependencies] 29 | + $this->templateConfiguration->toArray() 30 | + $class->toArray() 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /templates/phpunit.twig: -------------------------------------------------------------------------------- 1 | {{ dependency|transformDependency }} = {{ dependency.value }}; 34 | {% elseif not dependency.type %} 35 | $this->{{ dependency|transformDependency }} = null; 36 | {% elseif not mockery %} 37 | $this->{{ dependency|transformDependency }} = $this->createMock({{ dependency.type }}::class); 38 | {% else %} 39 | $this->{{ dependency|transformDependency }} = Mockery::mock({{ dependency.type }}::class); 40 | {% endif %} 41 | {% endfor %} 42 | {% if dependencies %} 43 | $this->{{ class|transformClazz }} = new {{ class }}( 44 | {% for dependency in dependencies %} 45 | $this->{{ dependency|transformDependency }}{% if not loop.last %},{% endif %} 46 | 47 | {% endfor %} 48 | ); 49 | {% else %} 50 | $this->{{ class|transformClazz }} = new {{ class }}(); 51 | {% endif %} 52 | } 53 | 54 | public function testMissing() 55 | { 56 | $this->fail('Test not yet implemented'); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/functional/FunctionalBaseTest.php: -------------------------------------------------------------------------------- 1 | currentTestFileFilename)) { 21 | unlink($this->currentTestFileFilename); 22 | } 23 | } 24 | 25 | /** 26 | * @dataProvider provideFixtures 27 | * 28 | * @param string $source 29 | * @param string $expected 30 | * @param string $arguments 31 | */ 32 | public function testGenerateSimplePhpUnitTestCase(string $source, string $expected, string $arguments) : void 33 | { 34 | $cmd = self::TEST_GENERATOR_BINARY . ' ' . $arguments . ' ' . $this->generateTestFile($source); 35 | $actual = shell_exec($cmd); 36 | assertEquals($expected, $actual); 37 | } 38 | 39 | public function provideFixtures() : array 40 | { 41 | $testArguments = []; 42 | foreach ($this->findFixtures() as $fixtureDir) { 43 | $fixtureDir = self::FIXTURES_DIR . DIRECTORY_SEPARATOR . $fixtureDir; 44 | $arguments = file_exists($fixtureDir . '/arguments.txt') 45 | ? str_replace(["\n", "\r"], ' ', file_get_contents($fixtureDir . '/arguments.txt')) 46 | : ''; 47 | $testArguments[$this->camelCaseToReadable($fixtureDir)] = [ 48 | file_get_contents($fixtureDir . '/source.php'), 49 | file_get_contents($fixtureDir . '/expected.php'), 50 | $arguments, 51 | ]; 52 | } 53 | return $testArguments; 54 | } 55 | 56 | private function generateTestFile(string $content) : string 57 | { 58 | $this->currentTestFileFilename = '/tmp/testfilefortestgenerator.php'; 59 | file_put_contents($this->currentTestFileFilename, $content); 60 | return $this->currentTestFileFilename; 61 | } 62 | 63 | private function camelCaseToReadable(string $camelCaseText) : string 64 | { 65 | return strtolower( 66 | trim( 67 | preg_replace('/([A-Z])/', ' $1', basename($camelCaseText)) 68 | ) 69 | ); 70 | } 71 | 72 | private function findFixtures(): array 73 | { 74 | return array_filter( 75 | scandir(self::FIXTURES_DIR, SCANDIR_SORT_ASCENDING), 76 | function (string $dirname) { 77 | return is_dir(self::FIXTURES_DIR . DIRECTORY_SEPARATOR . $dirname) 78 | && strpos($dirname, '.') !== 0; 79 | } 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tests/functional/backlog/AliasFlag/arguments.txt: -------------------------------------------------------------------------------- 1 | --alias='ClassA,B' --alias='PHPUnit_Framework_MockObject_MockObject,Mock' 2 | -------------------------------------------------------------------------------- /tests/functional/backlog/AliasFlag/expected.php: -------------------------------------------------------------------------------- 1 | classA = $this->createMock(B::class); 18 | $this->a = new A( 19 | $this->b 20 | ); 21 | } 22 | 23 | public function testMissing() 24 | { 25 | $this->fail('Test not yet implemented'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/functional/backlog/AliasFlag/source.php: -------------------------------------------------------------------------------- 1 | classA = $this->createMock(ClassA::class); 16 | $this->a = new A( 17 | $this->classA 18 | ); 19 | } 20 | 21 | public function testMissing() 22 | { 23 | $this->fail('Test not yet implemented'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/functional/backlog/MultipleClassesPerFile/expected2.php: -------------------------------------------------------------------------------- 1 | classB = $this->createMock(ClassB::class); 16 | $this->b = new B( 17 | $this->classB 18 | ); 19 | } 20 | 21 | public function testMissing() 22 | { 23 | $this->fail('Test not yet implemented'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/functional/backlog/MultipleClassesPerFile/source.php: -------------------------------------------------------------------------------- 1 | classA = Mockery::mock(ClassA::class); 18 | $this->a = new A( 19 | $this->classA 20 | ); 21 | } 22 | 23 | public function testMissing() 24 | { 25 | $this->fail('Test not yet implemented'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/functional/backlog/Php5SupportWithMockery/source.php: -------------------------------------------------------------------------------- 1 | classA = $this->createMock(Other\ClassA::class); 21 | $this->a = new A( 22 | $this->classA 23 | ); 24 | } 25 | 26 | public function testMissing() 27 | { 28 | $this->fail('Test not yet implemented'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/functional/fixtures/AddsCoversAnnotation/source.php: -------------------------------------------------------------------------------- 1 | x = null; 46 | $this->bool = false; 47 | $this->int = 0; 48 | $this->string = ''; 49 | $this->otherString = 'abc'; 50 | $this->otherInt = 999; 51 | $this->otherFloat = 3.1415; 52 | $this->xs = []; 53 | $this->array = []; 54 | $this->caseInsensitive = true; 55 | $this->fixed = PHP_EOL; 56 | $this->a = new A( 57 | $this->x, 58 | $this->bool, 59 | $this->int, 60 | $this->string, 61 | $this->otherString, 62 | $this->otherInt, 63 | $this->otherFloat, 64 | $this->xs, 65 | $this->array, 66 | $this->caseInsensitive, 67 | $this->fixed 68 | ); 69 | } 70 | 71 | public function testMissing() 72 | { 73 | $this->fail('Test not yet implemented'); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/functional/fixtures/ClassWithScalarValues/source.php: -------------------------------------------------------------------------------- 1 | classA = $this->createMock(ClassA::class); 18 | $this->a = new A( 19 | $this->classA 20 | ); 21 | } 22 | 23 | public function testMissing() 24 | { 25 | $this->fail('Test not yet implemented'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/functional/fixtures/DifferentBaseClass/source.php: -------------------------------------------------------------------------------- 1 | mockArg = $this->createMock(ClassA::class); 18 | $this->aUnderTest = new A( 19 | $this->mockArg 20 | ); 21 | } 22 | 23 | public function testMissing() 24 | { 25 | $this->fail('Test not yet implemented'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/functional/fixtures/FormattingForFieldsAndTestSubject/source.php: -------------------------------------------------------------------------------- 1 | classA = $this->createMock(ClassA::class); 18 | $this->a = new A( 19 | $this->classA 20 | ); 21 | } 22 | 23 | public function testMissing() 24 | { 25 | $this->fail('Test not yet implemented'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/functional/fixtures/GeneratesSimplePhpUnitTestCase/source.php: -------------------------------------------------------------------------------- 1 | classA = Mockery::mock(ClassA::class); 18 | $this->a = new A( 19 | $this->classA 20 | ); 21 | } 22 | 23 | public function testMissing() 24 | { 25 | $this->fail('Test not yet implemented'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/functional/fixtures/MockeryMocking/source.php: -------------------------------------------------------------------------------- 1 | classA = $this->createMock(ClassA::class); 16 | $this->a = new A( 17 | $this->classA 18 | ); 19 | } 20 | 21 | public function testMissing() 22 | { 23 | $this->fail('Test not yet implemented'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/functional/fixtures/Php5SupportWithPhpUnit5/source.php: -------------------------------------------------------------------------------- 1 | false, 21 | '--phpunit5' => false, 22 | '--mockery' => false, 23 | '--covers' => false, 24 | '--base-class' => 'PHPUnit\Framework\TestCase', 25 | '--subject-format' => false, 26 | '--field-format' => false, 27 | ]); 28 | $this->twigRenderer = (new DependencyContainer($args))->twigRenderer(); 29 | } 30 | 31 | public function testRendersTemplate() 32 | { 33 | $expected = <<<'EOT' 34 | customer = $this->createMock(Customer::class); 52 | $this->name = 'test'; 53 | $this->test = new Test( 54 | $this->customer, 55 | $this->name 56 | ); 57 | } 58 | 59 | public function testMissing() 60 | { 61 | $this->fail('Test not yet implemented'); 62 | } 63 | } 64 | 65 | EOT; 66 | $actual = $this->twigRenderer->render(new Clazz('Test', '', ''), [ 67 | new Dependency('customer', 'Customer'), 68 | new Dependency('name', 'string', '\'test\''), 69 | ]); 70 | assertEquals($expected, $actual); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /tests/resources/php7-to-php55-example/example.php: -------------------------------------------------------------------------------- 1 | object = $object; 21 | $this->dependency = $dependency; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/unit/ClassAnalyserTest.php: -------------------------------------------------------------------------------- 1 | classAnalyser = new ClassAnalyser(); 36 | } 37 | 38 | public function testOnlyRegistersOnConstructors() : void 39 | { 40 | $classNode = $this->createMock(Class_::class); 41 | $name = $this->createMock(Name::class); 42 | $name->name = 'Test'; 43 | $name->parts = ['Test']; 44 | $classNode->name = 'Test'; 45 | $classNode->namespacedName = $name; 46 | $this->classAnalyser->enterNode($classNode); 47 | assertEmpty($this->classAnalyser->getParameters()); 48 | } 49 | 50 | public function testFindsParametersInConstructors() : void 51 | { 52 | $param = new Param( 53 | new Variable('example'), 54 | null, 55 | new Identifier('A') 56 | ); 57 | $methodNode = new ClassMethod( 58 | new Identifier('__construct'), 59 | ['params' => [$param]] 60 | ); 61 | 62 | $this->classAnalyser->enterNode($methodNode); 63 | 64 | assertEquals( 65 | ['example' => new Dependency('example', 'A')], 66 | $this->classAnalyser->getParameters() 67 | ); 68 | } 69 | 70 | public function testAnalysesOnlyClassConstructors() : void 71 | { 72 | $functionNode = $this->createMock(Function_::class); 73 | $functionNode->name = '__construct'; 74 | 75 | $param = $this->createMock(Param::class); 76 | $param->name = 'Example'; 77 | $name = $this->createMock(Name::class); 78 | $name->method('toString')->willReturn('A'); 79 | $param->type = $name; 80 | $functionNode->method('getParams')->willReturn([$param]); 81 | 82 | $this->classAnalyser->enterNode($functionNode); 83 | assertEmpty($this->classAnalyser->getParameters()); 84 | } 85 | 86 | public function testFindsClass() : void 87 | { 88 | $classNode = $this->createMock(Class_::class); 89 | $classNode->name = 'b'; 90 | $name = $this->createMock(Name::class); 91 | $name->parts = ['a', 'b']; 92 | $classNode->namespacedName = $name; 93 | 94 | $this->classAnalyser->enterNode($classNode); 95 | $expected = new Clazz('b', 'a\b', 'a'); 96 | assertEquals($expected, $this->classAnalyser->getClass()); 97 | } 98 | 99 | /** 100 | * @dataProvider parameterProvider 101 | * @param string $message 102 | * @param $type 103 | * @param string|null $default 104 | * @param string|null $expected 105 | */ 106 | public function testGeneratesDefaults(string $message, $type, ?string $default, ?string $expected) : void 107 | { 108 | $this->classAnalyser->enterNode( 109 | $this->createConstructorWithOneParamenter($message, $type, $default) 110 | ); 111 | $parameters = $this->classAnalyser->getParameters(); 112 | assertEquals($expected, array_pop($parameters)->value(), $message); 113 | } 114 | 115 | public function parameterProvider() : array 116 | { 117 | return [ 118 | 'Object without default' => [ 119 | 'message' => 'Object without default', 120 | 'type' => 'ArrayObject', 121 | 'default' => null, 122 | 'expected' => '', 123 | ], 124 | 'No arguments' => [ 125 | 'message' => 'No arguments', 126 | 'type' => null, 127 | 'default' => null, 128 | 'expected' => null, 129 | ], 130 | 'Bool with no default' => [ 131 | 'message' => 'Bool with no default', 132 | 'type' => 'bool', 133 | 'default' => null, 134 | 'expected' => self::TYPE_BOOL_FALSE, 135 | ], 136 | 'No type with true default' => [ 137 | 'message' => 'No type with true default', 138 | 'type' => null, 139 | 'default' => self::TYPE_BOOL_TRUE, 140 | 'expected' => self::TYPE_BOOL_TRUE, 141 | ], 142 | 'No type with TRUE default' => [ 143 | 'message' => 'No type with TRUE default', 144 | 'type' => null, 145 | 'default' => 'TRUE', 146 | 'expected' => self::TYPE_BOOL_TRUE, 147 | ], 148 | 'No type with false default' => [ 149 | 'message' => 'No type with false default', 150 | 'type' => null, 151 | 'default' => self::TYPE_BOOL_FALSE, 152 | 'expected' => self::TYPE_BOOL_FALSE, 153 | ], 154 | 'Int with no default' => [ 155 | 'message' => 'Int with no default', 156 | 'type' => 'int', 157 | 'default' => null, 158 | 'expected' => '0', 159 | ], 160 | 'No type with int default' => [ 161 | 'message' => 'No type with int default', 162 | 'type' => null, 163 | 'default' => '123', 164 | 'expected' => '123', 165 | ], 166 | 'Float type with no default' => [ 167 | 'message' => 'Float type with no default', 168 | 'type' => 'float', 169 | 'default' => null, 170 | 'expected' => '0.0', 171 | ], 172 | 'No type with float default' => [ 173 | 'message' => 'No type with float default', 174 | 'type' => null, 175 | 'default' => '3.1415', 176 | 'expected' => '3.1415', 177 | ], 178 | 'String with no default' => [ 179 | 'message' => 'String with no default', 180 | 'type' => 'string', 181 | 'default' => null, 182 | 'expected' => "''", 183 | ], 184 | 'No type with string default' => [ 185 | 'message' => 'No type with string default', 186 | 'type' => null, 187 | 'default' => '"string"', 188 | 'expected' => "'string'", 189 | ], 190 | 'Array type with no default' => [ 191 | 'message' => 'Array type with no default', 192 | 'type' => 'array', 193 | 'default' => null, 194 | 'expected' => '[]', 195 | ], 196 | 'No type with array default' => [ 197 | 'message' => 'No type with array default', 198 | 'type' => null, 199 | 'default' => '[]', 200 | 'expected' => '[]', 201 | ], 202 | 'No type with defined global constant' => [ 203 | 'message' => 'No type with defined global constant', 204 | 'type' => null, 205 | 'default' => 'SOME_CONST', 206 | 'expected' => 'SOME_CONST', 207 | ], 208 | ]; 209 | } 210 | 211 | private function createConstructorWithOneParamenter(string $message, ?string $type, $default): ClassMethod 212 | { 213 | $param = new Param( 214 | new Variable(str_replace(' ', '_', $message)), 215 | $this->defaultToNode($default), 216 | $this->typeFromString($type) 217 | ); 218 | 219 | return new ClassMethod('__construct', ['params' => [$param]]); 220 | } 221 | 222 | private function typeFromString(?string $typeDefinition): ?Name 223 | { 224 | if ($typeDefinition === null) { 225 | return null; 226 | } 227 | 228 | if ($typeDefinition === 'bool' 229 | || $typeDefinition === 'float' 230 | || $typeDefinition === 'double' 231 | || $typeDefinition === 'int' 232 | || $typeDefinition === 'string' 233 | || $typeDefinition === 'array' 234 | ) { 235 | return new Name($typeDefinition); 236 | } 237 | 238 | return new Name($typeDefinition); 239 | } 240 | 241 | private function defaultToNode($default): ?Expr 242 | { 243 | if ($default === null) { 244 | return null; 245 | } 246 | 247 | if (preg_match(self::REGEX_FLOAT, $default)) { 248 | return new DNumber((float) $default); 249 | } 250 | 251 | if (preg_match(self::REGEX_INT, $default)) { 252 | return new LNumber((int) $default); 253 | } 254 | 255 | if ($default === self::TYPE_BOOL_TRUE 256 | || $default === self::TYPE_BOOL_FALSE 257 | || $default === 'TRUE' 258 | || $default === 'FALSE' 259 | ) { 260 | // $bool = $this->createMock(ConstFetch::class); 261 | // $bool->value = stripos($default, self::TYPE_BOOL_TRUE) !== false; 262 | // $bool->name = $this->createMock(Name::class); 263 | // $bool->name->method('toString')->willReturn($default); 264 | return new ConstFetch(new Name($default)); 265 | } 266 | 267 | if ($default === '[]') { 268 | return new Array_(); 269 | } 270 | 271 | if (preg_match('/^[\'"]/', $default)) { 272 | return new String_(trim($default, '\'""')); 273 | } 274 | 275 | return new ConstFetch(new Name($default)); 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /tests/unit/ClazzTest.php: -------------------------------------------------------------------------------- 1 | name = 'Test'; 30 | $this->namespacedName = 'Namespace\Test'; 31 | $this->namespace = 'Namespace'; 32 | $this->clazz = new Clazz( 33 | $this->name, 34 | $this->namespacedName, 35 | $this->namespace 36 | ); 37 | } 38 | 39 | public function testConvertsToArray() : void 40 | { 41 | assertEquals([ 42 | 'class' => 'Test', 43 | 'namespacedName' => 'Namespace\Test', 44 | 'namespace' => 'Namespace', 45 | ], $this->clazz->toArray()); 46 | } 47 | 48 | public function testGenerateFromClassNode() : void 49 | { 50 | $classNode = $this->createMock(Class_::class); 51 | $classNode->name = 'Test'; 52 | $name = $this->createMock(Name::class); 53 | $name->parts = ['Namespace', 'Test']; 54 | $classNode->namespacedName = $name; 55 | assertEquals([ 56 | 'class' => 'Test', 57 | 'namespacedName' => 'Namespace\Test', 58 | 'namespace' => 'Namespace', 59 | ], Clazz::fromClassNode($classNode)->toArray()); 60 | } 61 | 62 | public function testGeneratesFromStringWithoutNamespace() : void 63 | { 64 | assertEquals( 65 | new Clazz('Test', 'Test', ''), 66 | Clazz::fromFullyQualifiedNameString('Test') 67 | ); 68 | } 69 | 70 | 71 | public function testGeneratesFromStringWithNamespace() : void 72 | { 73 | assertEquals( 74 | new Clazz('Test', 'Vendor\Example\Test', 'Vendor\Example'), 75 | Clazz::fromFullyQualifiedNameString('Vendor\Example\Test') 76 | ); 77 | } 78 | 79 | public function testRejectsInvalidPhpIdentifier() : void 80 | { 81 | $this->expectException(InvalidFullyQualifiedNameException::class); 82 | Clazz::fromFullyQualifiedNameString('.'); 83 | } 84 | 85 | public function testHasClazz() : void 86 | { 87 | assertEquals('Test', (new Clazz('Test', 'Test', ''))->clazz()); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/unit/DependencyContainerTest.php: -------------------------------------------------------------------------------- 1 | args = $this->createMock(Response::class); 25 | $this->dependencyContainer = new DependencyContainer($this->args); 26 | } 27 | 28 | public function testGeneratesNodeTraverser(): void 29 | { 30 | assertInstanceOf(NodeTraverser::class, $this->dependencyContainer->nodeTraverser()); 31 | } 32 | 33 | public function testGeneratesTwig_Environment(): void 34 | { 35 | assertInstanceOf(Twig_Environment::class, $this->dependencyContainer->twigEnvironment()); 36 | } 37 | 38 | public function testGeneratesTwigRenderer(): void 39 | { 40 | assertInstanceOf(TwigRenderer::class, $this->dependencyContainer->twigRenderer()); 41 | } 42 | 43 | public function testGeneratesParser(): void 44 | { 45 | assertInstanceOf(Parser::class, $this->dependencyContainer->parser()); 46 | } 47 | 48 | public function testGeneratesTemplateConfiguration(): void 49 | { 50 | assertInstanceOf(TemplateConfiguration::class, $this->dependencyContainer->templateConfiguration()); 51 | } 52 | 53 | public function testGenerateBaseClassFromDefaultForPhpunit6(): void 54 | { 55 | $dependencyContainer = new DependencyContainer(new Response([ 56 | '--php5' => false, 57 | '--base-class' => false, 58 | ])); 59 | assertEquals( 60 | new Clazz('TestCase', 'PHPUnit\\Framework\\TestCase', 'PHPUnit\\Framework'), 61 | $dependencyContainer->baseClass() 62 | ); 63 | } 64 | 65 | public function testGenerateBaseClassFromDefaultForPhpunit5(): void 66 | { 67 | $dependencyContainer = new DependencyContainer(new Response([ 68 | '--php5' => true, 69 | '--base-class' => false, 70 | ])); 71 | assertEquals( 72 | new Clazz('PHPUnit_Framework_TestCase', 'PHPUnit_Framework_TestCase', ''), 73 | $dependencyContainer->baseClass() 74 | ); 75 | } 76 | 77 | public function testGenerateTestGenerator(): void 78 | { 79 | $dependencyContainer = new DependencyContainer(new Response([ 80 | '--subject-format' => '', 81 | '--field-format' => '', 82 | '--base-class' => '', 83 | '--php5' => '', 84 | '--phpunit5' => '', 85 | '--mockery' => '', 86 | '--covers' => '', 87 | '' => '', 88 | ])); 89 | assertInstanceOf( 90 | TestGenerator::class, 91 | $dependencyContainer->testGenerator() 92 | ); 93 | } 94 | 95 | public function testGenerateBaseClass(): void 96 | { 97 | $dependencyContainer = new DependencyContainer(new Response([ 98 | '--php5' => true, 99 | '--base-class' => 'Vendor\\Test', 100 | ])); 101 | assertEquals( 102 | new Clazz('Test', 'Vendor\\Test', 'Vendor'), 103 | $dependencyContainer->baseClass() 104 | ); 105 | } 106 | 107 | public function testLcfirstFilter(): void 108 | { 109 | $callable = $this->dependencyContainer->lcfirstFilter()->getCallable(); 110 | assertEquals('test', $callable('Test')); 111 | } 112 | 113 | public function testIsNullFilter(): void 114 | { 115 | $callable = $this->dependencyContainer->isNullFilter()->getCallable(); 116 | assertTrue($callable(null)); 117 | assertFalse($callable(false)); 118 | assertFalse($callable('')); 119 | assertFalse($callable(0)); 120 | } 121 | 122 | /** 123 | * @dataProvider clazzProvider 124 | */ 125 | public function testTransformClazzFilter($message, $format, $clazz, $expected): void 126 | { 127 | $callable = $this->dependencyContainer->transformClazzFilter($format)->getCallable(); 128 | assertEquals($expected, $callable($clazz), "$message with '$format'"); 129 | } 130 | 131 | public function clazzProvider(): array 132 | { 133 | return [ 134 | [ 135 | 'message' => 'Lowercase name', 136 | 'format' => '%n', 137 | 'clazz' => 'Test', 138 | 'expected' => 'test', 139 | ], 140 | [ 141 | 'message' => 'Uppercase name', 142 | 'format' => '%N', 143 | 'clazz' => 'test', 144 | 'expected' => 'Test', 145 | ], 146 | [ 147 | 'message' => 'Lowercase type with suffix', 148 | 'format' => '%tMock', 149 | 'clazz' => 'customer', 150 | 'expected' => 'customerMock', 151 | ], 152 | ]; 153 | } 154 | 155 | /** 156 | * @dataProvider dependencyProvider 157 | */ 158 | public function testTransformDependencyFilter($message, $format, $dependency, $expected): void 159 | { 160 | $callable = $this->dependencyContainer->transformDependencyFilter($format)->getCallable(); 161 | assertEquals($expected, $callable($dependency), "$message with '$format'"); 162 | } 163 | 164 | public function dependencyProvider(): array 165 | { 166 | return [ 167 | [ 168 | 'message' => 'Lowercase name', 169 | 'format' => '%n', 170 | 'dependency' => new Dependency('Test'), 171 | 'expected' => 'test', 172 | ], 173 | [ 174 | 'message' => 'Uppercase name', 175 | 'format' => '%N', 176 | 'dependency' => new Dependency('test'), 177 | 'expected' => 'Test', 178 | ], 179 | [ 180 | 'message' => 'Uppercase type', 181 | 'format' => '%T', 182 | 'dependency' => new Dependency('test', 'stdClass'), 183 | 'expected' => 'StdClass', 184 | ], 185 | [ 186 | 'message' => 'Lowercase type with suffix', 187 | 'format' => '%tMock', 188 | 'dependency' => new Dependency('test', 'Customer'), 189 | 'expected' => 'customerMock', 190 | ], 191 | ]; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /tests/unit/DependencyTest.php: -------------------------------------------------------------------------------- 1 | name()); 17 | } 18 | 19 | public function testHasType() : void 20 | { 21 | assertEquals('int', (new Dependency('test', 'int'))->type()); 22 | } 23 | 24 | public function testHasValue() : void 25 | { 26 | assertEquals('3.1415', (new Dependency('test', null, '3.1415'))->value()); 27 | } 28 | 29 | /** 30 | * @dataProvider typeProvider 31 | */ 32 | public function testDetectsIfDependencyTypeIsScalar(string $type, bool $expected) : void 33 | { 34 | assertEquals($expected, (new Dependency('test', $type))->isScalar()); 35 | } 36 | 37 | public function typeProvider() 38 | { 39 | return [ 40 | ['int', true], 41 | ['float', true], 42 | ['string', true], 43 | ['array', true], 44 | ['bool', true], 45 | ['ArrayObject', false], 46 | ['', false], 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/unit/Output/FileWriterTest.php: -------------------------------------------------------------------------------- 1 | setUp(); 34 | } 35 | 36 | public function testWritesOutputToFile(): void 37 | { 38 | $subjectFile = new \SplFileInfo(self::SUBJECT_FILE); 39 | $srcBase = new \SplFileInfo(self::SRC_BASE); 40 | $testBase = new \SplFileInfo(self::TEST_BASE); 41 | 42 | $fileWriter = new FileWriter($subjectFile, $srcBase, $testBase); 43 | $fileWriter->write('Test Output'); 44 | assertSame('Test Output', file_get_contents(self::TEST_FILE)); 45 | } 46 | 47 | public function testThrowsExceptionIfTestFileIsNotInSrcBase(): void 48 | { 49 | $subjectFile = new \SplFileInfo(self::SUBJECT_FILE); 50 | $srcBase = new \SplFileInfo(sys_get_temp_dir()); 51 | $testBase = new \SplFileInfo(self::TEST_BASE); 52 | 53 | $this->expectException(SubjectNotInSrcBaseException::class); 54 | (new FileWriter($subjectFile, $srcBase, $testBase))->write(''); 55 | } 56 | 57 | public function testThrowsExceptionIfDirectoryOfUnitTestIsNotWritable(): void 58 | { 59 | $subjectFile = new \SplFileInfo(self::SUBJECT_FILE); 60 | $srcBase = new \SplFileInfo(self::SRC_BASE); 61 | $testBase = new \SplFileInfo('/'); 62 | 63 | $this->expectException(InvalidFileException::class); 64 | $this->expectExceptionMessageRegExp('/is not writable/'); 65 | (new FileWriter($subjectFile, $srcBase, $testBase))->write(''); 66 | } 67 | 68 | public function testThrowsExceptionIfUnitFileCannotBeWritten(): void 69 | { 70 | $subjectFile = new \SplFileInfo(self::SUBJECT_FILE); 71 | $srcBase = new \SplFileInfo(self::SRC_BASE); 72 | $testBase = new \SplFileInfo(self::TEST_BASE); 73 | 74 | @mkdir(self::TEST_BASE); 75 | @touch(self::TEST_FILE); 76 | @chmod(self::TEST_FILE, 0000); 77 | $this->expectException(UnableToWriteTestFileException::class); 78 | (new FileWriter($subjectFile, $srcBase, $testBase))->write(''); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/unit/Output/OutputProcessorFactoryTest.php: -------------------------------------------------------------------------------- 1 | expectException(InvalidFileException::class); 28 | $this->expectExceptionMessageRegExp('/is not readable/'); 29 | OutputProcessorFactory::create( 30 | '/sfdsdf', 31 | __DIR__, 32 | '' 33 | ); 34 | } 35 | 36 | public function testThrowsExceptionIfSrcBaseDoesNotExist(): void 37 | { 38 | $this->expectException(InvalidFileException::class); 39 | $this->expectExceptionMessageRegExp('/does not exist/'); 40 | OutputProcessorFactory::create( 41 | __FILE__, 42 | '/sdfsdfs', 43 | '' 44 | ); 45 | } 46 | 47 | public function testThrowsExceptionIfSrcBaseIsNotADirectory(): void 48 | { 49 | $this->expectException(InvalidFileException::class); 50 | $this->expectExceptionMessageRegExp('/does not exist/'); 51 | OutputProcessorFactory::create( 52 | __FILE__, 53 | __FILE__, 54 | '' 55 | ); 56 | } 57 | 58 | public function testCreatesStdoutWriterByDefault(): void 59 | { 60 | assertInstanceOf(StdoutWriter::class, OutputProcessorFactory::create( 61 | __FILE__, 62 | null, 63 | null 64 | )); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/unit/Output/StdoutWriterTest.php: -------------------------------------------------------------------------------- 1 | write('Test'); 18 | assertSame('Test', ob_get_clean()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/unit/PhpFileTest.php: -------------------------------------------------------------------------------- 1 | content()); 20 | unlink($emptyFilename); 21 | } 22 | 23 | public function testReturnsFileContents() : void 24 | { 25 | $regularFilename = '/tmp/test-generator-regular-file.php'; 26 | file_put_contents($regularFilename, 'testdata'); 27 | assertEquals('testdata', (new PhpFile(new \SplFileInfo($regularFilename)))->content()); 28 | unlink($regularFilename); 29 | } 30 | 31 | public function testDoesNotAcceptDirectories() : void 32 | { 33 | $this->expectException(NotAPhpFileException::class); 34 | new PhpFile(new \SplFileInfo(sys_get_temp_dir())); 35 | } 36 | 37 | public function testDoesNotAcceptDirectoriesThatLookLikePhpFiles() : void 38 | { 39 | $this->expectException(NotAPhpFileException::class); 40 | new PhpFile(new \SplFileInfo(sys_get_temp_dir().'.php')); 41 | } 42 | 43 | public function testDoesNotAcceptFilesWithoutPhpExtension() : void 44 | { 45 | $this->expectException(NotAPhpFileException::class); 46 | new PhpFile(new \SplFileInfo('missing-php-extions.phtml')); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/unit/TemplateConfigurationTest.php: -------------------------------------------------------------------------------- 1 | clazz = new Clazz('Test', 'Test', ''); 33 | $this->php5 = false; 34 | $this->phpunit5 = false; 35 | $this->mockery = false; 36 | $this->templateConfiguration = new TemplateConfiguration( 37 | $this->clazz, 38 | $this->php5, 39 | $this->phpunit5, 40 | $this->mockery, 41 | $this->covers 42 | ); 43 | } 44 | 45 | public function testConvertsToArray() 46 | { 47 | assertEquals([ 48 | 'baseClass' => [ 49 | 'class' => 'Test', 50 | 'namespacedName' => 'Test', 51 | 'namespace' => '', 52 | ], 53 | 'php5' => false, 54 | 'phpunit5' => false, 55 | 'mockery' => false, 56 | 'covers' => false, 57 | ], $this->templateConfiguration->toArray()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/unit/TestGeneratorTest.php: -------------------------------------------------------------------------------- 1 | parser = $this->createMock(Parser::class); 43 | $this->classAnalyser = $this->createMock(ClassAnalyser::class); 44 | $this->nodeTraverser = $this->createMock(NodeTraverser::class); 45 | $this->twigRenderer = $this->createMock(TwigRenderer::class); 46 | $this->outputProcessor = $this->createMock(OutputProcessor::class); 47 | $this->testGenerator = new TestGenerator( 48 | $this->parser, 49 | $this->classAnalyser, 50 | $this->nodeTraverser, 51 | $this->twigRenderer, 52 | $this->outputProcessor 53 | ); 54 | } 55 | 56 | public function testPrintsEmptyTemplateIfFileDoesNotHaveAConstructor() : void 57 | { 58 | $this->parser->method('parse')->willReturn([]); 59 | $this->classAnalyser->method('getClass')->willReturn(null); 60 | $file = $this->createMock(PhpFile::class); 61 | $file->method('content')->willReturn(''); 62 | $this->twigRenderer->method('render')->willReturn(new Clazz('', '', '')); 63 | assertEquals('', $this->testGenerator->run($file)); 64 | } 65 | 66 | public function testReturnsEmptyStringForFileWithoutClass() : void 67 | { 68 | $emptyFile = $this->createMock(PhpFile::class); 69 | $emptyFile->method('content')->willReturn(''); 70 | 71 | $this->parser->method('parse')->willReturn([]); 72 | $this->classAnalyser->method('getClass')->willReturn(null); 73 | 74 | assertEmpty($this->testGenerator->run($emptyFile)); 75 | } 76 | 77 | public function testRendersClassAndDependencies() : void 78 | { 79 | $file = $this->createMock(PhpFile::class); 80 | $file->method('content')->willReturn(''); 81 | 82 | $this->parser->method('parse')->willReturn([]); 83 | $this->classAnalyser->method('getClass')->willReturn(new Clazz('', '', '')); 84 | 85 | $this->twigRenderer->expects($this->once())->method('render'); 86 | $this->testGenerator->run($file); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/unit/TwigRendererTest.php: -------------------------------------------------------------------------------- 1 | template = $this->createMock(Template::class); 26 | $this->twig = $this->createMock(Twig_Environment::class); 27 | $this->twig->expects($this->once())->method('load')->willReturn($this->template); 28 | $this->twigRenderer = new TwigRenderer($this->twig, new TemplateConfiguration(new Clazz('', '', ''))); 29 | } 30 | 31 | public function testRendersClassnameAndParameters() 32 | { 33 | $this->template->expects($this->once())->method('render')->willReturn('test'); 34 | assertEquals( 35 | 'test', 36 | $this->twigRenderer->render(new Clazz('', '', ''), [123]) 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tools/ensure-docopt-readme-and-app-are-identical: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | [] [--write] 29 | EOT; 30 | 31 | class Php7FeatureRemover extends NodeVisitorAbstract 32 | { 33 | private static $INVALID_PARAM_TYPES = [ 34 | 'bool', 35 | 'int', 36 | 'string', 37 | 'float', 38 | 'boolean', 39 | 'double', 40 | 'iterable', 41 | 'Throwable', 42 | ]; 43 | 44 | public function leaveNode(Node $node) { 45 | if ($node instanceof ClassMethod || $node instanceof Function_) { 46 | foreach ($node->params as &$param) { 47 | if (in_array($param->type->name ?? '', self::$INVALID_PARAM_TYPES, true)) { 48 | $param->type = null; 49 | } 50 | if ($param->variadic) { 51 | $param->variadic = false; 52 | } 53 | } 54 | $node->returnType = null; 55 | } else if ($node instanceof Declare_ 56 | && $node->declares[0]->key->name ?? '' === 'strict_types') { 57 | return NodeTraverser::REMOVE_NODE; 58 | } else if ($node instanceof ClassConst) { 59 | $node->flags = 0; 60 | } else if ($node instanceof CoalesceNode) { 61 | return $this->convertCoalesceToTernaryWithIsset($node); 62 | } else if ($node instanceof FuncCall) { 63 | return $this->replaceSplatOperator($node); 64 | } else if ($node instanceof StaticCall) { 65 | return $this->replaceSplatOperator($node); 66 | } else if ($node instanceof MethodCall) { 67 | return $this->replaceSplatOperator($node); 68 | } 69 | return null; 70 | } 71 | 72 | /** 73 | * @TODO 74 | */ 75 | private function replaceSplatOperator(Node $node): Node 76 | { 77 | return $node; 78 | $callable = []; 79 | $arguments = []; 80 | return new FuncCall( 81 | new Name('call_user_func_array'), 82 | [$callable, $arguments] 83 | ); 84 | 85 | } 86 | 87 | private function convertCoalesceToTernaryWithIsset(CoalesceNode $coalesceNode): TernaryNode 88 | { 89 | return new TernaryNode( 90 | new IssetNode([$coalesceNode->left]), 91 | $coalesceNode->left, 92 | $coalesceNode->right 93 | ); 94 | } 95 | } 96 | 97 | function main(\Docopt\Response $args) 98 | { 99 | $inputFile = $args['']; 100 | $outputFile = $args['']; 101 | 102 | if (!is_file($inputFile) 103 | || !is_readable($inputFile) 104 | ) { 105 | echo 'Input file is not a file or not readable' . PHP_EOL; 106 | exit(1); 107 | } 108 | 109 | if ($args['--write']) { 110 | $outputFile = $inputFile; 111 | } 112 | 113 | if ($outputFile !== null && !is_writable(dirname($outputFile))) 114 | { 115 | echo 'Output file is not writable' . PHP_EOL; 116 | exit(1); 117 | } 118 | 119 | $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7); 120 | $traverser = new NodeTraverser; 121 | $prettyPrinter = new PrettyPrinter\Standard; 122 | 123 | $traverser->addVisitor(new NameResolver); 124 | $traverser->addVisitor(new Php7FeatureRemover); 125 | 126 | try { 127 | $code = file_get_contents($inputFile); 128 | $statements = $traverser->traverse($parser->parse($code)); 129 | $code = $prettyPrinter->prettyPrintFile($statements); 130 | 131 | $outputFile 132 | ? file_put_contents($outputFile, $code) 133 | : print($code); 134 | } catch (PhpParser\Error $e) { 135 | echo 'Parse Error: ' . $e->getMessage() . PHP_EOL; 136 | exit(2); 137 | } catch (Exception $exception) { 138 | echo 'Something went wrong.' . PHP_EOL; 139 | exit(3); 140 | } 141 | } 142 | 143 | main(Docopt::handle($description)); 144 | --------------------------------------------------------------------------------