Skip to content

Instantly share code, notes, and snippets.

@farfromunique
Created July 4, 2018 20:35
Show Gist options
  • Save farfromunique/b3375615b1f73de5a0d80ae76b49faaf to your computer and use it in GitHub Desktop.
Save farfromunique/b3375615b1f73de5a0d80ae76b49faaf to your computer and use it in GitHub Desktop.
a messy script to generate project tests.
<?php
// this must be placed inside /test/ directory
require '../vendor/autoload.php';
require 'helpers.php';
require 'FileClassReflection.php';
require 'TestCreator.php';
if(!class_exists('Symfony\Component\Finder\Finder')) {
die('symfony/finder is required');
}
if(!class_exists('Symfony\Component\Filesystem\Filesystem')) {
die('symfony/filesystem is required');
}
$config = [
'project' => [
'namespace' => 'MyProject\\',
'dir' => realpath(__DIR__ . '/../src/MyProject'),
],
'tests' => [
'namespace' => 'MyProject\\Tests\\',
'dir' => realpath(__DIR__ . '/../tests') . DIRECTORY_SEPARATOR . 'MyProject',
],
'stubs' => [
'class' => "<?php declare(strict_types=1);
namespace {{namespace}};
use PHPUnit\Framework\TestCase;
class {{class}} extends TestCase
{
{{methods}}
}",
'method' => "
public function {{method}}()
{
// TODO: write tests for {{oClassNamespace}}\{{oClass}}::{{oMethod}}
}"
]
];
$creator = new TestCreator($config);
$creator->run();
<?php
declare(strict_types=1);
use Symfony\Component\Finder\SplFileInfo as SfFileInfo;
class FileClassReflection extends ReflectionClass
{
private $fileInfo;
public function __construct(SfFileInfo $info)
{
$this->fileInfo = $info;
parent::__construct($this->getClass($info));
}
/**
* Returns class reflection for the first class in the file.
*
* @param SfFileInfo $file A Symfony Finder SplFileInfo representing the file
*
* @return string|false Full class name if found, false otherwise
*/
private function getClass(SfFileInfo $file)
{
$class = false;
$namespace = false;
$tokens = token_get_all($file->getContents());
if (1 === count($tokens) && T_INLINE_HTML === $tokens[0][0]) {
throw new \InvalidArgumentException(sprintf('The file "%s" does not contain PHP code. Did you forgot to add the "<?php" start tag at the beginning of the file?', $file));
}
for ($i = 0; isset($tokens[$i]); ++$i) {
$token = $tokens[$i];
if (!isset($token[1])) {
continue;
}
if (true === $class && T_STRING === $token[0]) {
return $namespace . '\\' . $token[1];
}
if (true === $namespace && T_STRING === $token[0]) {
$namespace = $token[1];
while (isset($tokens[++$i][1]) && in_array($tokens[$i][0], array(T_NS_SEPARATOR, T_STRING))) {
$namespace .= $tokens[$i][1];
}
$token = $tokens[$i];
}
if (T_CLASS === $token[0]) {
// Skip usage of ::class constant and anonymous classes
$skipClassToken = false;
for ($j = $i - 1; $j > 0; --$j) {
if (!isset($tokens[$j][1])) {
break;
}
if (T_DOUBLE_COLON === $tokens[$j][0] || T_NEW === $tokens[$j][0]) {
$skipClassToken = true;
break;
} elseif (!in_array($tokens[$j][0], array(T_WHITESPACE, T_DOC_COMMENT, T_COMMENT))) {
break;
}
}
if (!$skipClassToken) {
$class = true;
}
}
if (T_NAMESPACE === $token[0]) {
$namespace = true;
}
}
throw new \InvalidArgumentException('No class was found in ' . $file->getRealPath());
}
public function getFileInfo(): SfFileInfo
{
return $this->fileInfo;
}
}
<?php
function str_after($subject, $search){
return $search === '' ? $subject : \array_reverse(\explode($search, $subject, 2))[0];
}
<?php
declare(strict_types=1);
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo as SfFileInfo;
class TestCreator
{
protected $config;
private $fs;
public function __construct(array $config)
{
$this->config = $config;
$this->fs = new Filesystem();
}
public function run()
{
$this->createTestDirs();
$classes = $this->getProjectClasses();
/** @var FileClassReflection $class */
foreach ($classes as $class) {
$this->createTestCase($class);
}
}
protected function createTestDirs()
{
$dir = $this->config['tests']['dir'];
if (!$this->fs->exists($dir)) {
$this->fs->mkdir($dir);
}
$directories = Finder::create()
->in($this->config['project']['dir'])
->directories()->getIterator();
foreach ($directories as $directory) {
$dir = $this->getTestPath($directory);
if (!$this->fs->exists($dir)) {
$this->fs->mkdir($dir);
}
}
}
protected function getTestPath(SfFileInfo $file)
{
return $this->config['tests']['dir'] . str_after(
$file->getRealPath(),
$this->config['project']['dir']
);
}
protected function getProjectClasses()
{
$files = $this->getProjectFiles();
$classes = [];
foreach ($files as $file) {
try {
$classes[] = new FileClassReflection($file);
} catch (Exception $e) {
}
}
return $classes;
}
/**
* @return sfFileInfo[]|Finder
*/
protected function getProjectFiles()
{
return Finder::create()
->in($this->config['project']['dir'])
->name('*.php')
->files();
}
private function createTestCase(FileClassReflection $class)
{
echo PHP_EOL;
$reflectionMethods = $class->getMethods(ReflectionMethod::IS_PUBLIC);
$methods = [];
foreach ($reflectionMethods as $method) {
if ($method->isPublic() && $method->name[0] !== '_') {
$methods[] = $method;
}
}
if (count($methods) === 0) {
echo ' > Skipping test for ' . $class->getName() . ' - ( no public methods ) ' . PHP_EOL;
return;
}
$testCase = [
'oNamespace' => $class->getNamespaceName(),
'oClass' => $class->getShortName(),
'namespace' => $this->getTestNamespace($class),
'class' => $class->getShortName() . 'Test',
'file' => $this->getTestFileForClass($class)
];
if ($this->fs->exists($testCase['file'])) {
echo ' > Skipping TestCase for ' . $class->getName() . ' - ( test exists )' . PHP_EOL;
return;
}
$methodTest = $testCase;
$testCase['methods'] = '';
/** @var ReflectionMethod $method */
foreach ($methods as $method) {
$methodTest['oMethod'] = $method->getShortName();
$methodTest['method'] = 'test' . ucfirst($method->getShortName());
$body = str_replace(
['{{oNamespace}}', '{{oClass}}', '{{namespace}}', '{{class}}', '{{file}}', '{{oMethod}}', '{{method}}'],
array_values($methodTest),
$this->config['stubs']['method']
);
$testCase['methods'] .= $body . PHP_EOL . PHP_EOL;
}
$this->fs->touch($testCase['file']);
$stub = $this->config['stubs']['class'];
$content = str_replace(
['{{oNamespace}}', '{{oClass}}', '{{namespace}}', '{{class}}', '{{file}}', '{{methods}}'],
array_values($testCase),
$stub
);
$this->fs->dumpFile($testCase['file'], $content);
echo ' > TestCase created ' . $testCase['namespace'] . "\\" . $testCase['class'] . PHP_EOL;
}
private function getTestNamespace(FileClassReflection $class)
{
$namespace = $class->getNamespaceName();
$vendor = $this->config['project']['namespace'];
$classNamespace = substr($namespace, strlen($vendor));
$testNamespace = $this->config['tests']['namespace'] . $classNamespace;
return $testNamespace;
}
private function getTestFileForClass(FileClassReflection $class)
{
$fileInfo = $class->getFileInfo();
return str_replace('.php', 'Test.php', $this->getTestPath($fileInfo));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment