/** * Get the name of the test to create. * Method has to begin with 'test' * * @param string $methodName * @param bool $useDefaultTestMethodName * @param \Box\TestScribe\Spec\SpecsPerClass $specPerClass * * @return string */ public function getTestName($methodName, $useDefaultTestMethodName, SpecsPerClass $specPerClass) { $specsPerMethod = $specPerClass->getSpecsPerMethodByName($methodName); $specs = $specsPerMethod->getSpecs(); if ($specs) { $existingTestNames = array_keys($specs); $testMethodName = $this->testNameSelector->selectTestName($existingTestNames); if ($testMethodName !== '') { $msg = "Updating existing test ( {$testMethodName} )."; $this->output->writeln($msg); return $testMethodName; } } $testMethodNamePart = $methodName; if (!$useDefaultTestMethodName) { $message = "\nEnter the name of the test. It will be prefixed with 'test_'\n" . "Press enter to use the method name ( {$methodName} ) as the default."; $this->output->writeln($message); // rawInput is used instead of InputWithHelp so that // users don't have to quote the name as instructed by the help. $input = $this->rawInputWithPrompt->getString(); if ($input !== '') { $testMethodNamePart = $input; } } $testMethodName = "test_{$testMethodNamePart}"; return $testMethodName; }
/** * Create a mock object of the given class. * * Return null if users decide not to mock this object. * * @param $className * @param array $arguments * * @return null|object */ public function createMockInstance($className, array $arguments) { // Make sure to update the index if the caller hierarchy changes. // #1 Box\TestScribe\Mock\InjectedMockMgr->createMockInstance() // #2 Box\TestScribe\App::createMockedInstance() // #3 Box\TestScribe\_fixture\ServiceLocator::resolve_internal() // #4 Box\TestScribe\_fixture\ServiceLocator::resolve() // #5 Box\TestScribe\_fixture\_input\CalculatorViaLocator->calculateWithACalculator() $isTheCallFromTheClassBeingTested = $this->callOriginatorChecker->isCallFromTheClassBeingTested(5); if (!$isTheCallFromTheClassBeingTested) { return null; } if (array_key_exists($className, $this->injectedMockedObjects)) { // @TODO (ryang 1/27/15) : in Box webapp the diesel system will return // the same mock object if Diesel::Foo is called multiple times in // the same test and a mock for Foo is registered. // research if this behavior should be assumed for all // service locator systems. $msg = "Instantiating class ( {$className} ) which was mocked." . " Return the same mock object."; $this->output->writeln($msg); /** * @var MockClass $mockClass */ $mockClass = $this->injectedMockedObjects[$className]; return $mockClass->getMockedDynamicClassObj(); } $mockClass = $this->fullMockObjectFactory->createMockObject($className); $this->injectedMockedObjects[$className] = $mockClass; $mockedDynamicClassObj = $mockClass->getMockedDynamicClassObj(); return $mockedDynamicClassObj; }
/** * Get a string. * * @return string */ public function getString() { // Show a prompt symbol to make it easier for users to recognize // that an input is required. $this->output->writeln(">"); $str = $this->rawInput->getString(); return $str; }
/** * Handle intercepted calls made to the mock class instance. * * @param string $mockObjName * @param \Box\TestScribe\MethodInfo\Method $method * @param array $arguments * * @return void */ public function showCallInfo($mockObjName, Method $method, array $arguments) { $methodName = $method->getName(); $callerInfoString = $this->getCallerInfoString(); $msg = "\n{$callerInfoString} Calling {$mockObjName}->{$methodName}( "; $this->output->write($msg); $msg = $this->methodCallInfo->getCallParamInfo($method, $arguments); $this->output->writeln($msg); }
/** * Create and load the definition of a mock class. * * @param string $className * * @return \Box\TestScribe\Mock\MockClass * @throws \DI\NotFoundException */ public function createAndLoadStaticMockClass($className) { $mockClass = $this->mockClassFactory->create($className, true, ''); $mockObjectName = $mockClass->getMockObjectName(); $this->classBuilderStatic->create($mockClass); // @TODO (ryang 5/27/15) : display line number $msg = "Mocked ( {$className} ) id ( {$mockObjectName} ) for static methods invocation.\n"; $this->output->writeln($msg); return $mockClass; }
/** * Collect values of the arguments to the given method. * * @param \Box\TestScribe\MethodInfo\Method $method * * @return \Box\TestScribe\ArgumentInfo\Arguments */ public function collect(Method $method) { $reflectionMethod = $method->getReflectionMethod(); $args = $reflectionMethod->getParameters(); $argumentsCount = count($args); if (!$argumentsCount) { return new Arguments([]); } $methodName = $reflectionMethod->getName(); $className = $reflectionMethod->getDeclaringClass()->getName(); $message = "\nPrepare to get arguments to the method ( {$methodName} ) of the class ( {$className} ).\n"; $this->output->writeln($message); $methodParams = null; $testName = $this->globalComputedConfig->getTestMethodName(); $savedSpec = $this->savedSpecs->getSpecForTest($testName); if ($savedSpec) { if ($method->isConstructor()) { $methodParams = $savedSpec->getConstructorParameters(); } else { $methodParams = $savedSpec->getMethodParameters(); } } $argsArray = []; $index = 0; foreach ($args as $arg) { $argumentName = $arg->getName(); $argPromptSubject = "parameter ( {$argumentName} )"; $isOptional = $arg->isOptional(); if ($isOptional) { $argPromptSubject = "optional {$argPromptSubject}"; } if ($methodParams) { // @TODO (Ray Yang 9/30/15) : error checking $value = $methodParams[$index]; $this->output->writeln("Get ( {$value} ) from the saved test for {$argPromptSubject}"); $inputValue = $this->inputValueFactory->createPrimitiveValue($value); } else { $typeInfo = $method->getParamTypeString($argumentName); $inputValue = $this->inputValueGetter->get($typeInfo, $argPromptSubject, '', $methodName, $argumentName); } if ($inputValue->isVoid()) { // @TODO (ryang 1/9/15) : double check if the parameter is optional. // Assume the parameters after this one will all have to be void. break; } $argsArray[] = $inputValue; $index++; } // Add an empty line for improved readability. $this->output->writeln(''); $args = new Arguments($argsArray); return $args; }
/** * @param \Symfony\Component\Console\Input\InputInterface $input * * @return \Box\TestScribe\Config\ConfigParams * @throws \Box\TestScribe\Exception\TestScribeException */ public function getInputParams(InputInterface $input) { $originalInSourceFile = (string) $input->getArgument(CmdOption::SOURCE_FILE_NAME_KEY); // Always use the absolute path. This is needed when checking // if a call is from the class under test. $inSourceFile = $this->fileFunctionWrapper->realpath($originalInSourceFile); $inClassName = $this->classExtractor->getClassName($inSourceFile); $inPhpClassName = new PhpClassName($inClassName); $methodName = $this->methodNameGetter->getTestMethodName($input, $inClassName); $msg = "Testing the method ( {$methodName} ) of the class ( {$inClassName} )."; $this->output->writeln($msg); $inputParams = new ConfigParams($inSourceFile, $inPhpClassName, $methodName); return $inputParams; }
/** * Invoke a method on the target object regardless if the method is private, protected or public. * * @param object|null $targetObject null if the method is static * @param \Box\TestScribe\MethodInfo\Method $method * @param \Box\TestScribe\ArgumentInfo\Arguments $arguments * * @return mixed */ public function invokeMethodRegardlessOfProtectionLevel($targetObject, Method $method, Arguments $arguments) { $className = $method->getFullClassName(); $argumentValues = $arguments->getValues(); // @TODO (ryang 2/3/15) : warn against testing private methods directly. // @TODO (ryang 6/8/15) : only change accessibility when the method is not public $reflectionClass = new \ReflectionClass($className); $methodName = $method->getName(); $reflectionMethod = $reflectionClass->getMethod($methodName); $reflectionMethod->setAccessible(true); $this->output->writeln("\nStart executing method ( {$methodName} ).\n"); $executionResult = $reflectionMethod->invokeArgs($targetObject, $argumentValues); $this->output->writeln("\nFinish executing method ( {$methodName} ).\n"); return $executionResult; }
/** * @param \Box\TestScribe\Execution\ExecutionResult $result * * @return void */ public function showExecutionResult(ExecutionResult $result) { $exception = $result->getException(); if ($exception) { $exceptionType = get_class($exception); $exceptionMsg = $exception->getMessage(); $resultMsg = "An exception ( {$exceptionType} ) is thrown.\n" . "Exception message ( {$exceptionMsg} )."; } else { $value = $result->getResultValue(); // @TODO (ryang 6/4/15) : don't show the value if the return value of the method is void. $valueStr = $this->valueFormatter->getReadableFormat($value); $resultMsg = "Result from this method execution is :\n" . "{$valueStr}\n" . "End of the result."; } $msg = "{$resultMsg}\n\n" . "Please verify this result and the interactions with the mocks are what you expect."; $this->output->writeln($msg); }
/** * Generate method definition statements that overwrite methods * defined in the given base class. * This is done so that calls to these methods can be intercepted * by this tool. Otherwise calls to these methods will be routed to * the original class. * * @param string $baseClassName * @param string $nameOfTheMethodToPassThrough * * @return string */ private function genOriginalMethodsOverwriteStatements($baseClassName, $nameOfTheMethodToPassThrough) { $classBeingMocked = new \ReflectionClass($baseClassName); // We only need to overwrite public methods and protected methods // private methods can't be mocked by mocking frameworks anyway. $methodObjs = $classBeingMocked->getMethods(ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_PROTECTED); // The destructor is overwritten to prevent // the original versions to be called. // @TODO (ryang 2/17/15) : does it matter if the destructor is called or not? // Don't overwrite the original constructor. // We want the state of the mock object to resemble the original object // to support partial mocking. // @TODO (ryang 2/17/15) : re-evaluate when we use this class for all mock objects // not only the ones that represent the class under test. $overwriteStatement = "\npublic function __destruct(){}\n"; $constructorStatement = $this->genConstructorForPartialMocking($classBeingMocked, $nameOfTheMethodToPassThrough); $overwriteStatement .= $constructorStatement; foreach ($methodObjs as $method) { $methodName = $method->getName(); if ($method->isFinal()) { $msg = sprintf("Warning: Class ( %s ) being mocked has a final method ( %s ).\n" . "Mocking of this method is not supported.\n" . "If this method is called, the result is not defined. ", $classBeingMocked->getName(), $methodName); $this->output->writeln($msg); continue; } // If the method is static, no need to overwrite it // since static methods are not expected to be called in this context. if (!($method->isStatic() || in_array($methodName, self::$reservedMethodNames, true) || $methodName === $nameOfTheMethodToPassThrough)) { $overwriteStatement .= $this->genOneMethodOverwriteStatement($method); } } return $overwriteStatement; }
/** * @param string $className * * @return \Box\TestScribe\Mock\MockClass */ public function createMockObject($className) { $mockClass = $this->mockClassLoader->createAndLoadMockClass($className, ''); // The fully mocked objects are assumed to have the original // constructors overwritten to not call the original constructors. // Thus the arguments to the original constructors are ignored. $mockObj = $this->mockObjectFactory->createMockObjectFromMockClass($mockClass, []); $mockClass->setMockedDynamicClassObj($mockObj); // Only show the mock message when full mocking is requested. // Since in most cases when partial mocking is not needed // the class under test won't be mocked. // Showing this message actually can cause confusion. $fullName = $mockClass->getClassNameBeingMocked(); $mockObjectName = $mockClass->getMockObjectName(); // @TODO (ryang 5/27/15) : display line number $msg = "Mocked ( {$fullName} ) id ( {$mockObjectName} ).\n"; $this->output->writeln($msg); return $mockClass; }
/** * @param string[] $menu * * @param string $msg * * @return int * @throws \Box\TestScribe\Exception\TestScribeException */ public function selectFromMenu(array $menu, $msg) { $itemCount = count($menu); if ($itemCount < 2) { // There is no point of selecting from a menu with one or no item. throw new TestScribeException('Selecting from a menu with less than 2 items is not allowed.'); } $this->output->writeln($msg); for ($i = 0; $i < $itemCount; $i++) { /** @var string $name */ $name = $menu[$i]; $id = (string) $i; $msg = "{$id} : {$name}"; $this->output->writeln($msg); } $selectionString = $this->rawInputWithPrompt->getString(); // @TODO (Ray Yang 9/30/15) : error handling $selectionId = (int) $selectionString; return $selectionId; }
/** * Generate the test code in the output file. * * @param ExecutionResult $executionResult * * @return void */ public function render(ExecutionResult $executionResult) { $file = $this->globalComputedConfig->getOutSourceFile(); $this->deleteExistingDestinationFileWhenNeeded($file); $result = $this->methodRenderer->renderMethod($executionResult); if (!file_exists($file)) { $config = $this->globalComputedConfig; $header = $this->headerRenderer->renderClassHeader($config->getOutPhpClassName()); // @TODO (ryang 8/20/14) : move rendering logic to the render class. // @TODO (ryang 8/21/14) : make it configurable which mocking framework to use. $result = <<<TAG {$header}{ {$result} } TAG; } $this->insertIntoFile($file, '}', $result); $msg = "\nAdded a test to ( {$file} ).\n"; $this->output->writeln($msg); }
/** * @param \Box\TestScribe\Mock\MockClass $mockClassUnderTest * * @return \Box\TestScribe\Execution\ClassUnderTestMockCreationResultValue */ public function createMockObjectForTheClassUnderTest(MockClass $mockClassUnderTest) { $constructorMethodObj = $mockClassUnderTest->getConstructorOfTheMockedClass(); if ($constructorMethodObj) { $constructorArgs = $this->argumentsCollector->collect($constructorMethodObj); $constructorArgValues = $constructorArgs->getValues(); $this->output->writeln("\nStart executing the constructor.\n"); } else { // When the class under test doesn't have a constructor defined // don't display the constructor execution message. $constructorArgs = new Arguments([]); $constructorArgValues = []; } $result = $this->expectedExceptionCatcher->execute([$this->mockObjectFactory, 'createMockObjectFromMockClass'], [$mockClassUnderTest, $constructorArgValues]); $mockObj = $result->getResult(); $exception = $result->getException(); if ($constructorMethodObj) { $this->output->writeln("\nFinish executing the constructor.\n"); } $result = new ClassUnderTestMockCreationResultValue($constructorArgs, $mockObj, $exception); return $result; }
/** * The dependency management system used by the method under test should call this method * to return a mocked class name when a class mock is requested * to be used to invoke a static method. * * Return null if users decide not to mock this object and have the dependency management * system use the real class. * * @param string $className * * @return string|null */ public function createMockedClass($className) { // Make sure to update the index if the caller hierarchy changes. // #1 Box\TestScribe\Mock\InjectedMockClassMgr->createMockInstance() // #2 Box\TestScribe\App::createMockedClass() // #3 Box\TestScribe\_fixture\StaticServiceLocator::resolveInternal() // #4 Box\TestScribe\_fixture\StaticServiceLocator::resolve() // #5 Box\TestScribe\_fixture\_input\StaticCalculatorViaLocator->calculateWithACalculator() $isTheCallFromTheClassBeingTested = $this->callOriginatorChecker->isCallFromTheClassBeingTested(5); if (!$isTheCallFromTheClassBeingTested) { return null; } if (array_key_exists($className, self::$injectedMockedClass)) { $this->output->writeln("Requesting class ( {$className} ) for static method calls which was mocked." . " Return the same mock object."); /** * @var MockClass $mock */ $mock = self::$injectedMockedClass[$className]; return $mock->getMockObjectName(); } $mock = $this->staticMockClassFactory->createAndLoadStaticMockClass($className); self::$injectedMockedClass[$className] = $mock; return $mock->getMockObjectName(); }
/** * @return void */ private function showHelp() { $helpMsg = <<<'TAG' ------------------------------------------------------------- Input help: +++ Value input: Specify the input in PHP format. Use fully qualified class names in place of object variables. They will be mocked automatically. Use the word void to select the default value for a parameter, or a void return value. e.g. "ab", 'a', "a\n" true, false, 1, null, ["a", "b"], ["a" => 2], ["a" => ["b" => [ 1, 2]]] \ClassFoo , \Namespace1\ClassBar [\ClassFoo, \ClassBar] ['key' => \ClassFoo] void +++ Other commands: a : abort this test generation run. End of help ============================================================= TAG; $this->output->writeln($helpMsg); }
/** * Show a warning. * * @param string $variableName name without '$' prefix * @param mixed $value * * @return void */ private function showWarning($variableName, $value) { $typeString = gettype($value); $msg = "Assertion for a variable ( {$variableName} ) with type ( {$typeString} ) is not supported yet."; $this->output->writeln($msg); }