Completed
Push — master ( 66ec7e...c9ec0a )
by Matze
07:32
created

TestCreateCommand::addMock()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 2
Metric Value
c 5
b 0
f 2
dl 0
loc 18
rs 9.4285
cc 1
eloc 12
nc 1
nop 3
1
<?php
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 31 and the first side effect is on line 25.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
namespace BrainExe\Core\Console;
4
5
use BrainExe\Annotations\Annotations\Inject;
6
use BrainExe\Core\Console\TestGenerator\HandleExistingFile;
7
use BrainExe\Core\Console\TestGenerator\MethodCodeGenerator;
8
use BrainExe\Core\Console\TestGenerator\ProcessMethod;
9
use BrainExe\Core\Console\TestGenerator\TestData;
10
use BrainExe\Core\DependencyInjection\Rebuild;
11
use PHPUnit_Framework_MockObject_MockObject;
12
use PHPUnit_Framework_TestCase;
13
use ReflectionClass;
14
use ReflectionMethod;
15
use Symfony\Component\Console\Command\Command;
16
use Symfony\Component\Console\Input\InputArgument;
17
use Symfony\Component\Console\Input\InputInterface;
18
use Symfony\Component\Console\Input\InputOption;
19
use Symfony\Component\Console\Output\OutputInterface;
20
use Symfony\Component\DependencyInjection\ContainerBuilder;
21
use Symfony\Component\DependencyInjection\Definition;
22
use Symfony\Component\DependencyInjection\Reference;
23
use BrainExe\Core\Annotations\Command as CommandAnnotation;
24
25
require_once "TestGenerator/TestData.php";
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal TestGenerator/TestData.php does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
26
27
/**
28
 * @CommandAnnotation
29
 * @codeCoverageIgnore
30
 */
31
class TestCreateCommand extends Command
32
{
33
34
    /**
35
     * Cached container builder
36
     * @var ContainerBuilder|null
37
     */
38
    public $container = null;
39
40
    /**
41
     * @var Rebuild
42
     */
43
    private $rebuild;
44
45
    /**
46
     * {@inheritdoc}
47
     */
48
    protected function configure()
49
    {
50
        $this->setName('test:create')
51
            ->addArgument('service', InputArgument::REQUIRED, 'service id, e.g. IndexController')
52
            ->addOption('dry', 'd', InputOption::VALUE_NONE, 'only display the generated test');
53
    }
54
55
    /**
56
     * @Inject("@Core.Rebuild")
57
     * @param Rebuild $rebuild
58
     */
59
    public function __construct(Rebuild $rebuild)
60
    {
61
        $this->rebuild = $rebuild;
62
63
        parent::__construct();
64
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69
    protected function execute(InputInterface $input, OutputInterface $output)
70
    {
71
        $this->initContainerBuilder();
72
73
        $serviceId            = $input->getArgument('service');
74
        $serviceObject        = $this->getService($serviceId);
75
        $serviceDefinition    = $this->getServiceDefinition($serviceId);
76
        $serviceReflection    = new ReflectionClass($serviceObject);
77
        $serviceFullClassName = $serviceReflection->getName();
78
        $serviceClassName     = $this->getShortClassName($serviceFullClassName);
79
80
        $testData = new TestData();
81
82
        $testData->addUse(PHPUnit_Framework_TestCase::class, 'TestCase');
83
        $testData->addUse(PHPUnit_Framework_MockObject_MockObject::class, 'MockObject');
84
        $testData->addUse($serviceFullClassName);
85
86
        $this->generateMethods($serviceReflection, $testData, $serviceFullClassName);
87
        $this->setupConstructor($serviceDefinition, $testData);
88
89
        $methodProcessor = new ProcessMethod($this);
90
        foreach ($serviceDefinition->getMethodCalls() as $methodCall) {
91
            $methodProcessor->processMethod($methodCall, $testData);
92
        }
93
94
        $template = file_get_contents(__DIR__ . '/../../scripts/phpunit_template.php.tpl');
95
        $template = str_replace(
96
            '%test_namespace%',
97
            $this->getTestNamespace($serviceReflection->getNamespaceName()),
98
            $template
99
        );
100
        $template = str_replace('%service_namespace%', $serviceFullClassName, $template);
101
        $template = str_replace('%class_name%', $serviceClassName, $template);
102
        $template = str_replace('%setters%', implode("\n", $testData->setterCalls), $template);
103
        $template = str_replace('%default_tests%', implode("\n", $testData->defaultTests), $template);
104
        $template = str_replace('%mock_properties%', implode("\n", $testData->mockProperties), $template);
105
        $template = str_replace('%use_statements%', $testData->renderUse(), $template);
106
        $template = str_replace('%local_mocks%', implode("\n", $testData->localMocks), $template);
107
        $template = str_replace('%constructor_arguments%', implode(', ', $testData->constructorArguments), $template);
108
        $template = str_replace("\t", '    ', $template);
109
110
        $testFileName = $this->getTestFileName($serviceFullClassName);
111
112
        if ($input->getOption('dry')) {
113
            $output->writeln($template);
114
            return;
115
        } elseif (file_exists($testFileName)) {
116
            // handle already existing test file
117
            $fileHandler = new HandleExistingFile();
118
            $handler = $this->getHelper('question');
119
            $template = $fileHandler->handleExistingFile(
120
                $input,
121
                $output,
122
                $handler,
0 ignored issues
show
Compatibility introduced by
$handler of type object<Symfony\Component...Helper\HelperInterface> is not a sub-type of object<Symfony\Component...\Helper\QuestionHelper>. It seems like you assume a concrete implementation of the interface Symfony\Component\Console\Helper\HelperInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
123
                $serviceId,
124
                $testFileName,
125
                $template
126
            );
127
128
            if ($template === false) {
129
                return;
130
            }
131
        }
132
133
        $testDir = dirname($testFileName);
134
135
        if (!is_dir($testDir)) {
136
            mkdir($testDir, 0777, true);
137
        }
138
139
        file_put_contents($testFileName, $template);
140
141
        $output->writeln(
142
            sprintf("Created Test for '<info>%s</info>' in <info>%s</info>", $serviceId, $testFileName)
143
        );
144
    }
145
146
    /**
147
     * @param string $serviceNamespace
148
     * @return string
149
     */
150
    private function getTestFileName($serviceNamespace)
151
    {
152
        $path = str_replace('\\', DIRECTORY_SEPARATOR, $serviceNamespace);
153
154
        return sprintf('%sTests/%sTest.php', ROOT, $path);
155
    }
156
157
    /**
158
     * @param string $serviceId
159
     * @return Definition
160
     */
161
    public function getServiceDefinition($serviceId)
162
    {
163
        return $this->container->getDefinition($serviceId);
164
    }
165
166
    /**
167
     * @param string $serviceId
168
     * @return object
169
     */
170
    public function getService($serviceId)
171
    {
172
        return $this->container->get($serviceId);
173
    }
174
175
    /**
176
     * @param string $serviceNamespace
177
     * @return string
178
     */
179
    private function getTestNamespace($serviceNamespace)
180
    {
181
        return "Tests\\" . $serviceNamespace;
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal Tests\\ does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
182
    }
183
184
    /**
185
     * @param ReflectionClass $serviceReflection
186
     * @param TestData $testData
187
     * @param string $serviceFullClassName
188
     */
189
    protected function generateMethods(ReflectionClass $serviceReflection, TestData $testData, $serviceFullClassName)
190
    {
191
        $blacklistedMethods = $this->getBlacklistedMethods($serviceReflection);
192
193
        $methodCodeGenerator = new MethodCodeGenerator();
194
        $methods             = $serviceReflection->getMethods(ReflectionMethod::IS_PUBLIC);
195
        foreach ($methods as $method) {
196
            $methodName = $method->getName();
197
198
            if (in_array($methodName, $blacklistedMethods)) {
199
                continue;
200
            }
201
202
            if ($method->getDeclaringClass() == $serviceReflection) {
203
                $testData->defaultTests[] = $methodCodeGenerator->getDummyTestCode(
204
                    $testData,
205
                    $method,
206
                    $serviceFullClassName
207
                );
208
            }
209
        }
210
    }
211
212
    private function initContainerBuilder()
213
    {
214
        if ($this->container !== null) {
215
            return;
216
        }
217
218
        $this->container = $this->rebuild->rebuildDIC(false);
219
    }
220
221
    /**
222
     * @param string $fullClassName
223
     * @return string
224
     */
225
    public function getShortClassName($fullClassName)
226
    {
227
        // Strip off namespace
228
        $lastBackslashPos = strrpos($fullClassName, '\\');
229
230
        if (!$lastBackslashPos) {
231
            return $fullClassName;
232
        }
233
234
        return substr($fullClassName, $lastBackslashPos + 1);
235
    }
236
237
    /**
238
     * @param ReflectionClass $serviceReflection
239
     * @return string[]
240
     */
241
    private function getBlacklistedMethods(ReflectionClass $serviceReflection)
242
    {
243
        $blacklistedMethods = [];
244
245
        foreach ($serviceReflection->getTraitNames() as $trait) {
246
            $reflection = new ReflectionClass($trait);
247
            foreach ($reflection->getMethods() as $method) {
248
                $blacklistedMethods[] = $method->getName();
249
            }
250
        }
251
252
        $blacklistedMethods[] = '__construct';
253
254
        return $blacklistedMethods;
255
    }
256
257
    /**
258
     * @param Definition $referenceService
259
     * @param TestData $testData
260
     * @param string $mockName
261
     */
262
    public function addMock(Definition $referenceService, TestData $testData, $mockName)
263
    {
264
        $class = $referenceService->getClass();
265
        $testData->addUse($class);
266
267
        $mock = sprintf(
268
            "\t\t\$this->%s = \$this->createMock(%s::class);",
269
            lcfirst($mockName),
270
            $mockName
271
        );
272
273
        $testData->localMocks[]     = $mock;
274
        $testData->mockProperties[] = sprintf(
275
            "\t/**\n\t * @var %s|MockObject\n\t */\n\tprivate \$%s;\n",
276
            $mockName,
277
            lcfirst($mockName)
278
        );
279
    }
280
281
    /**
282
     * @param Definition $serviceDefinition
283
     * @param TestData $data
284
     */
285
    private function setupConstructor(Definition $serviceDefinition, TestData $data)
286
    {
287
        foreach ($serviceDefinition->getArguments() as $reference) {
288
            if ($reference instanceof Definition) {
289
                $definition = $reference;
290
                $mockName = $this->getShortClassName($definition->getClass());
291
            } elseif ($reference instanceof Reference) {
292
                // add setter for model mock
293
                $definition = $this->getServiceDefinition((string)$reference);
294
                $mockName = $this->getShortClassName($definition->getClass());
295
            } else {
296
                $data->constructorArguments[] = var_export($reference, true);
297
                continue;
298
            }
299
300
            $data->constructorArguments[] = sprintf('$this->%s', lcfirst($mockName));
301
302
            $this->addMock($definition, $data, $mockName);
303
        }
304
    }
305
}
306