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