770 lines
20 KiB
PHP
770 lines
20 KiB
PHP
<?php
|
|
/**
|
|
* Mockery
|
|
*
|
|
* LICENSE
|
|
*
|
|
* This source file is subject to the new BSD license that is bundled
|
|
* with this package in the file LICENSE.txt.
|
|
* It is also available through the world-wide-web at this URL:
|
|
* http://github.com/padraic/mockery/blob/master/LICENSE
|
|
* If you did not receive a copy of the license and are unable to
|
|
* obtain it through the world-wide-web, please send an email
|
|
* to padraic@php.net so we can send you a copy immediately.
|
|
*
|
|
* @category Mockery
|
|
* @package Mockery
|
|
* @copyright Copyright (c) 2010-2014 Pádraic Brady (http://blog.astrumfutura.com)
|
|
* @license http://github.com/padraic/mockery/blob/master/LICENSE New BSD License
|
|
*/
|
|
|
|
use Mockery\ExpectationInterface;
|
|
use Mockery\Generator\CachingGenerator;
|
|
use Mockery\Generator\Generator;
|
|
use Mockery\Generator\MockConfigurationBuilder;
|
|
use Mockery\Generator\StringManipulationGenerator;
|
|
use Mockery\Generator\StringManipulation\Pass\CallTypeHintPass;
|
|
use Mockery\Generator\StringManipulation\Pass\ClassNamePass;
|
|
use Mockery\Generator\StringManipulation\Pass\ClassPass;
|
|
use Mockery\Generator\StringManipulation\Pass\InstanceMockPass;
|
|
use Mockery\Generator\StringManipulation\Pass\InterfacePass;
|
|
use Mockery\Generator\StringManipulation\Pass\MethodDefinitionPass;
|
|
use Mockery\Generator\StringManipulation\Pass\RemoveBuiltinMethodsThatAreFinalPass;
|
|
use Mockery\Generator\StringManipulation\Pass\RemoveUnserializeForInternalSerializableClassesPass;
|
|
use Mockery\Loader\EvalLoader;
|
|
use Mockery\Loader\Loader;
|
|
|
|
class Mockery
|
|
{
|
|
const BLOCKS = 'Mockery_Forward_Blocks';
|
|
|
|
/**
|
|
* Global container to hold all mocks for the current unit test running.
|
|
*
|
|
* @var \Mockery\Container
|
|
*/
|
|
protected static $_container = null;
|
|
|
|
/**
|
|
* Global configuration handler containing configuration options.
|
|
*
|
|
* @var \Mockery\Configuration
|
|
*/
|
|
protected static $_config = null;
|
|
|
|
/**
|
|
* @var \Mockery\Generator\Generator
|
|
*/
|
|
protected static $_generator;
|
|
|
|
/**
|
|
* @var \Mockery\Loader\Loader
|
|
*/
|
|
protected static $_loader;
|
|
|
|
/**
|
|
* @var array
|
|
*/
|
|
private static $_filesToCleanUp = array();
|
|
|
|
/**
|
|
* Defines the global helper functions
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function globalHelpers()
|
|
{
|
|
require_once __DIR__.'/helpers.php';
|
|
}
|
|
|
|
/**
|
|
* Static shortcut to \Mockery\Container::mock().
|
|
*
|
|
* @return \Mockery\MockInterface
|
|
*/
|
|
public static function mock()
|
|
{
|
|
$args = func_get_args();
|
|
|
|
return call_user_func_array(array(self::getContainer(), 'mock'), $args);
|
|
}
|
|
|
|
/**
|
|
* @return \Mockery\MockInterface
|
|
*/
|
|
public static function spy()
|
|
{
|
|
$args = func_get_args();
|
|
return call_user_func_array(array(self::getContainer(), 'mock'), $args)->shouldIgnoreMissing();
|
|
}
|
|
|
|
/**
|
|
* @return \Mockery\MockInterface
|
|
*/
|
|
public static function instanceMock()
|
|
{
|
|
$args = func_get_args();
|
|
|
|
return call_user_func_array(array(self::getContainer(), 'mock'), $args);
|
|
}
|
|
|
|
/**
|
|
* Static shortcut to \Mockery\Container::mock(), first argument names the mock.
|
|
*
|
|
* @return \Mockery\MockInterface
|
|
*/
|
|
public static function namedMock()
|
|
{
|
|
$args = func_get_args();
|
|
$name = array_shift($args);
|
|
|
|
$builder = new MockConfigurationBuilder();
|
|
$builder->setName($name);
|
|
|
|
array_unshift($args, $builder);
|
|
|
|
return call_user_func_array(array(self::getContainer(), 'mock'), $args);
|
|
}
|
|
|
|
/**
|
|
* Static shortcut to \Mockery\Container::self().
|
|
*
|
|
* @throws LogicException
|
|
*
|
|
* @return \Mockery\MockInterface
|
|
*/
|
|
public static function self()
|
|
{
|
|
if (is_null(self::$_container)) {
|
|
throw new \LogicException('You have not declared any mocks yet');
|
|
}
|
|
|
|
return self::$_container->self();
|
|
}
|
|
|
|
/**
|
|
* Static shortcut to closing up and verifying all mocks in the global
|
|
* container, and resetting the container static variable to null.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function close()
|
|
{
|
|
foreach (self::$_filesToCleanUp as $fileName) {
|
|
@unlink($fileName);
|
|
}
|
|
self::$_filesToCleanUp = array();
|
|
|
|
if (is_null(self::$_container)) {
|
|
return;
|
|
}
|
|
|
|
self::$_container->mockery_teardown();
|
|
self::$_container->mockery_close();
|
|
self::$_container = null;
|
|
}
|
|
|
|
/**
|
|
* Static fetching of a mock associated with a name or explicit class poser.
|
|
*
|
|
* @param $name
|
|
*
|
|
* @return \Mockery\Mock
|
|
*/
|
|
public static function fetchMock($name)
|
|
{
|
|
return self::$_container->fetchMock($name);
|
|
}
|
|
|
|
/**
|
|
* Get the container.
|
|
*/
|
|
public static function getContainer()
|
|
{
|
|
if (is_null(self::$_container)) {
|
|
self::$_container = new Mockery\Container(self::getGenerator(), self::getLoader());
|
|
}
|
|
|
|
return self::$_container;
|
|
}
|
|
|
|
/**
|
|
* @param \Mockery\Generator\Generator $generator
|
|
*/
|
|
public static function setGenerator(Generator $generator)
|
|
{
|
|
self::$_generator = $generator;
|
|
}
|
|
|
|
public static function getGenerator()
|
|
{
|
|
if (is_null(self::$_generator)) {
|
|
self::$_generator = self::getDefaultGenerator();
|
|
}
|
|
|
|
return self::$_generator;
|
|
}
|
|
|
|
public static function getDefaultGenerator()
|
|
{
|
|
$generator = new StringManipulationGenerator(array(
|
|
new CallTypeHintPass(),
|
|
new ClassPass(),
|
|
new ClassNamePass(),
|
|
new InstanceMockPass(),
|
|
new InterfacePass(),
|
|
new MethodDefinitionPass(),
|
|
new RemoveUnserializeForInternalSerializableClassesPass(),
|
|
new RemoveBuiltinMethodsThatAreFinalPass(),
|
|
));
|
|
|
|
return new CachingGenerator($generator);
|
|
}
|
|
|
|
/**
|
|
* @param Loader $loader
|
|
*/
|
|
public static function setLoader(Loader $loader)
|
|
{
|
|
self::$_loader = $loader;
|
|
}
|
|
|
|
/**
|
|
* @return Loader
|
|
*/
|
|
public static function getLoader()
|
|
{
|
|
if (is_null(self::$_loader)) {
|
|
self::$_loader = self::getDefaultLoader();
|
|
}
|
|
|
|
return self::$_loader;
|
|
}
|
|
|
|
/**
|
|
* @return EvalLoader
|
|
*/
|
|
public static function getDefaultLoader()
|
|
{
|
|
return new EvalLoader();
|
|
}
|
|
|
|
/**
|
|
* Set the container.
|
|
*
|
|
* @param \Mockery\Container $container
|
|
*
|
|
* @return \Mockery\Container
|
|
*/
|
|
public static function setContainer(Mockery\Container $container)
|
|
{
|
|
return self::$_container = $container;
|
|
}
|
|
|
|
/**
|
|
* Reset the container to null.
|
|
*/
|
|
public static function resetContainer()
|
|
{
|
|
self::$_container = null;
|
|
}
|
|
|
|
/**
|
|
* Return instance of ANY matcher.
|
|
*
|
|
* @return \Mockery\Matcher\Any
|
|
*/
|
|
public static function any()
|
|
{
|
|
return new \Mockery\Matcher\Any();
|
|
}
|
|
|
|
/**
|
|
* Return instance of TYPE matcher.
|
|
*
|
|
* @param $expected
|
|
*
|
|
* @return \Mockery\Matcher\Type
|
|
*/
|
|
public static function type($expected)
|
|
{
|
|
return new \Mockery\Matcher\Type($expected);
|
|
}
|
|
|
|
/**
|
|
* Return instance of DUCKTYPE matcher.
|
|
*
|
|
* @return \Mockery\Matcher\Ducktype
|
|
*/
|
|
public static function ducktype()
|
|
{
|
|
return new \Mockery\Matcher\Ducktype(func_get_args());
|
|
}
|
|
|
|
/**
|
|
* Return instance of SUBSET matcher.
|
|
*
|
|
* @param array $part
|
|
*
|
|
* @return \Mockery\Matcher\Subset
|
|
*/
|
|
public static function subset(array $part)
|
|
{
|
|
return new \Mockery\Matcher\Subset($part);
|
|
}
|
|
|
|
/**
|
|
* Return instance of CONTAINS matcher.
|
|
*
|
|
* @return \Mockery\Matcher\Contains
|
|
*/
|
|
public static function contains()
|
|
{
|
|
return new \Mockery\Matcher\Contains(func_get_args());
|
|
}
|
|
|
|
/**
|
|
* Return instance of HASKEY matcher.
|
|
*
|
|
* @param $key
|
|
*
|
|
* @return \Mockery\Matcher\HasKey
|
|
*/
|
|
public static function hasKey($key)
|
|
{
|
|
return new \Mockery\Matcher\HasKey($key);
|
|
}
|
|
|
|
/**
|
|
* Return instance of HASVALUE matcher.
|
|
*
|
|
* @param $val
|
|
*
|
|
* @return \Mockery\Matcher\HasValue
|
|
*/
|
|
public static function hasValue($val)
|
|
{
|
|
return new \Mockery\Matcher\HasValue($val);
|
|
}
|
|
|
|
/**
|
|
* Return instance of CLOSURE matcher.
|
|
*
|
|
* @param $closure
|
|
*
|
|
* @return \Mockery\Matcher\Closure
|
|
*/
|
|
public static function on($closure)
|
|
{
|
|
return new \Mockery\Matcher\Closure($closure);
|
|
}
|
|
|
|
/**
|
|
* Return instance of MUSTBE matcher.
|
|
*
|
|
* @param $expected
|
|
*
|
|
* @return \Mockery\Matcher\MustBe
|
|
*/
|
|
public static function mustBe($expected)
|
|
{
|
|
return new \Mockery\Matcher\MustBe($expected);
|
|
}
|
|
|
|
/**
|
|
* Return instance of NOT matcher.
|
|
*
|
|
* @param $expected
|
|
*
|
|
* @return \Mockery\Matcher\Not
|
|
*/
|
|
public static function not($expected)
|
|
{
|
|
return new \Mockery\Matcher\Not($expected);
|
|
}
|
|
|
|
/**
|
|
* Return instance of ANYOF matcher.
|
|
*
|
|
* @return \Mockery\Matcher\AnyOf
|
|
*/
|
|
public static function anyOf()
|
|
{
|
|
return new \Mockery\Matcher\AnyOf(func_get_args());
|
|
}
|
|
|
|
/**
|
|
* Return instance of NOTANYOF matcher.
|
|
*
|
|
* @return \Mockery\Matcher\NotAnyOf
|
|
*/
|
|
public static function notAnyOf()
|
|
{
|
|
return new \Mockery\Matcher\NotAnyOf(func_get_args());
|
|
}
|
|
|
|
/**
|
|
* Get the global configuration container.
|
|
*/
|
|
public static function getConfiguration()
|
|
{
|
|
if (is_null(self::$_config)) {
|
|
self::$_config = new \Mockery\Configuration();
|
|
}
|
|
|
|
return self::$_config;
|
|
}
|
|
|
|
/**
|
|
* Utility method to format method name and arguments into a string.
|
|
*
|
|
* @param string $method
|
|
* @param array $arguments
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function formatArgs($method, array $arguments = null)
|
|
{
|
|
if (is_null($arguments)) {
|
|
return $method . '()';
|
|
}
|
|
|
|
$formattedArguments = array();
|
|
foreach ($arguments as $argument) {
|
|
$formattedArguments[] = self::formatArgument($argument);
|
|
}
|
|
|
|
return $method . '(' . implode(', ', $formattedArguments) . ')';
|
|
}
|
|
|
|
private static function formatArgument($argument, $depth = 0)
|
|
{
|
|
if (is_object($argument)) {
|
|
return 'object(' . get_class($argument) . ')';
|
|
}
|
|
|
|
if (is_int($argument) || is_float($argument)) {
|
|
return $argument;
|
|
}
|
|
|
|
if (is_array($argument)) {
|
|
if ($depth === 1) {
|
|
$argument = 'array(...)';
|
|
} else {
|
|
$sample = array();
|
|
foreach ($argument as $key => $value) {
|
|
$sample[$key] = self::formatArgument($value, $depth + 1);
|
|
}
|
|
$argument = preg_replace("{\s}", '', var_export($sample, true));
|
|
}
|
|
|
|
return ((strlen($argument) > 1000) ? substr($argument, 0, 1000).'...)' : $argument);
|
|
}
|
|
|
|
if (is_bool($argument)) {
|
|
return $argument ? 'true' : 'false';
|
|
}
|
|
|
|
if (is_resource($argument)) {
|
|
return 'resource(...)';
|
|
}
|
|
|
|
if (is_null($argument)) {
|
|
return 'NULL';
|
|
}
|
|
|
|
$argument = (string) $argument;
|
|
|
|
return $depth === 0 ? '"' . $argument . '"' : $argument;
|
|
}
|
|
|
|
/**
|
|
* Utility function to format objects to printable arrays.
|
|
*
|
|
* @param array $objects
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function formatObjects(array $objects = null)
|
|
{
|
|
static $formatting;
|
|
|
|
if ($formatting) {
|
|
return '[Recursion]';
|
|
}
|
|
|
|
if (is_null($objects)) {
|
|
return '';
|
|
}
|
|
|
|
$objects = array_filter($objects, 'is_object');
|
|
if (empty($objects)) {
|
|
return '';
|
|
}
|
|
|
|
$formatting = true;
|
|
$parts = array();
|
|
|
|
foreach ($objects as $object) {
|
|
$parts[get_class($object)] = self::objectToArray($object);
|
|
}
|
|
|
|
$formatting = false;
|
|
|
|
return 'Objects: ( ' . var_export($parts, true) . ')';
|
|
}
|
|
|
|
/**
|
|
* Utility function to turn public properties and public get* and is* method values into an array.
|
|
*
|
|
* @param $object
|
|
* @param int $nesting
|
|
*
|
|
* @return array
|
|
*/
|
|
private static function objectToArray($object, $nesting = 3)
|
|
{
|
|
if ($nesting == 0) {
|
|
return array('...');
|
|
}
|
|
|
|
return array(
|
|
'class' => get_class($object),
|
|
'properties' => self::extractInstancePublicProperties($object, $nesting),
|
|
'getters' => self::extractGetters($object, $nesting)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns all public instance properties.
|
|
*
|
|
* @param $object
|
|
* @param $nesting
|
|
*
|
|
* @return array
|
|
*/
|
|
private static function extractInstancePublicProperties($object, $nesting)
|
|
{
|
|
$reflection = new \ReflectionClass(get_class($object));
|
|
$properties = $reflection->getProperties(\ReflectionProperty::IS_PUBLIC);
|
|
$cleanedProperties = array();
|
|
|
|
foreach ($properties as $publicProperty) {
|
|
if (!$publicProperty->isStatic()) {
|
|
$name = $publicProperty->getName();
|
|
$cleanedProperties[$name] = self::cleanupNesting($object->$name, $nesting);
|
|
}
|
|
}
|
|
|
|
return $cleanedProperties;
|
|
}
|
|
|
|
/**
|
|
* Returns all object getters.
|
|
*
|
|
* @param $object
|
|
* @param $nesting
|
|
*
|
|
* @return array
|
|
*/
|
|
private static function extractGetters($object, $nesting)
|
|
{
|
|
$reflection = new \ReflectionClass(get_class($object));
|
|
$publicMethods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);
|
|
$getters = array();
|
|
|
|
foreach ($publicMethods as $publicMethod) {
|
|
$name = $publicMethod->getName();
|
|
$irrelevantName = (substr($name, 0, 3) !== 'get' && substr($name, 0, 2) !== 'is');
|
|
$isStatic = $publicMethod->isStatic();
|
|
$numberOfParameters = $publicMethod->getNumberOfParameters();
|
|
|
|
if ($irrelevantName || $numberOfParameters != 0 || $isStatic) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$getters[$name] = self::cleanupNesting($object->$name(), $nesting);
|
|
} catch (\Exception $e) {
|
|
$getters[$name] = '!! ' . get_class($e) . ': ' . $e->getMessage() . ' !!';
|
|
}
|
|
}
|
|
|
|
return $getters;
|
|
}
|
|
|
|
private static function cleanupNesting($argument, $nesting)
|
|
{
|
|
if (is_object($argument)) {
|
|
$object = self::objectToArray($argument, $nesting - 1);
|
|
$object['class'] = get_class($argument);
|
|
|
|
return $object;
|
|
}
|
|
|
|
if (is_array($argument)) {
|
|
return self::cleanupArray($argument, $nesting - 1);
|
|
}
|
|
|
|
return $argument;
|
|
}
|
|
|
|
private static function cleanupArray($argument, $nesting = 3)
|
|
{
|
|
if ($nesting == 0) {
|
|
return '...';
|
|
}
|
|
|
|
foreach ($argument as $key => $value) {
|
|
if (is_array($value)) {
|
|
$argument[$key] = self::cleanupArray($value, $nesting - 1);
|
|
} elseif (is_object($value)) {
|
|
$argument[$key] = self::objectToArray($value, $nesting - 1);
|
|
}
|
|
}
|
|
|
|
return $argument;
|
|
}
|
|
|
|
/**
|
|
* Utility function to parse shouldReceive() arguments and generate
|
|
* expectations from such as needed.
|
|
*
|
|
* @param Mockery\MockInterface $mock
|
|
* @param array $args
|
|
* @param callable $add
|
|
* @return \Mockery\CompositeExpectation
|
|
*/
|
|
public static function parseShouldReturnArgs(\Mockery\MockInterface $mock, $args, $add)
|
|
{
|
|
$composite = new \Mockery\CompositeExpectation();
|
|
|
|
foreach ($args as $arg) {
|
|
if (is_array($arg)) {
|
|
foreach ($arg as $k => $v) {
|
|
$expectation = self::buildDemeterChain($mock, $k, $add)->andReturn($v);
|
|
$composite->add($expectation);
|
|
}
|
|
} elseif (is_string($arg)) {
|
|
$expectation = self::buildDemeterChain($mock, $arg, $add);
|
|
$composite->add($expectation);
|
|
}
|
|
}
|
|
|
|
return $composite;
|
|
}
|
|
|
|
/**
|
|
* Sets up expectations on the members of the CompositeExpectation and
|
|
* builds up any demeter chain that was passed to shouldReceive.
|
|
*
|
|
* @param \Mockery\MockInterface $mock
|
|
* @param string $arg
|
|
* @param callable $add
|
|
* @throws Mockery\Exception
|
|
* @return \Mockery\ExpectationDirector
|
|
*/
|
|
protected static function buildDemeterChain(\Mockery\MockInterface $mock, $arg, $add)
|
|
{
|
|
/** @var Mockery\Container $container */
|
|
$container = $mock->mockery_getContainer();
|
|
$methodNames = explode('->', $arg);
|
|
reset($methodNames);
|
|
|
|
if (!\Mockery::getConfiguration()->mockingNonExistentMethodsAllowed()
|
|
&& !$mock->mockery_isAnonymous()
|
|
&& !in_array(current($methodNames), $mock->mockery_getMockableMethods())
|
|
) {
|
|
throw new \Mockery\Exception(
|
|
'Mockery\'s configuration currently forbids mocking the method '
|
|
. current($methodNames) . ' as it does not exist on the class or object '
|
|
. 'being mocked'
|
|
);
|
|
}
|
|
|
|
/** @var ExpectationInterface|null $expectations */
|
|
$expectations = null;
|
|
|
|
/** @var Callable $nextExp */
|
|
$nextExp = function ($method) use ($add) {
|
|
return $add($method);
|
|
};
|
|
|
|
while (true) {
|
|
$method = array_shift($methodNames);
|
|
$expectations = $mock->mockery_getExpectationsFor($method);
|
|
|
|
if (is_null($expectations) || self::noMoreElementsInChain($methodNames)) {
|
|
$expectations = $nextExp($method);
|
|
if (self::noMoreElementsInChain($methodNames)) {
|
|
break;
|
|
}
|
|
|
|
$mock = self::getNewDemeterMock($container, $method, $expectations);
|
|
} else {
|
|
$demeterMockKey = $container->getKeyOfDemeterMockFor($method);
|
|
if ($demeterMockKey) {
|
|
$mock = self::getExistingDemeterMock($container, $demeterMockKey);
|
|
}
|
|
}
|
|
|
|
$nextExp = function ($n) use ($mock) {
|
|
return $mock->shouldReceive($n);
|
|
};
|
|
}
|
|
|
|
return $expectations;
|
|
}
|
|
|
|
/**
|
|
* @param \Mockery\Container $container
|
|
* @param string $method
|
|
* @param Mockery\ExpectationInterface $exp
|
|
*
|
|
* @return \Mockery\Mock
|
|
*/
|
|
private static function getNewDemeterMock(Mockery\Container $container,
|
|
$method,
|
|
Mockery\ExpectationInterface $exp
|
|
) {
|
|
$mock = $container->mock('demeter_' . $method);
|
|
$exp->andReturn($mock);
|
|
|
|
return $mock;
|
|
}
|
|
|
|
/**
|
|
* @param \Mockery\Container $container
|
|
* @param string $demeterMockKey
|
|
*
|
|
* @return mixed
|
|
*/
|
|
private static function getExistingDemeterMock(Mockery\Container $container, $demeterMockKey)
|
|
{
|
|
$mocks = $container->getMocks();
|
|
$mock = $mocks[$demeterMockKey];
|
|
|
|
return $mock;
|
|
}
|
|
|
|
/**
|
|
* @param array $methodNames
|
|
*
|
|
* @return bool
|
|
*/
|
|
private static function noMoreElementsInChain(array $methodNames)
|
|
{
|
|
return empty($methodNames);
|
|
}
|
|
|
|
/**
|
|
* Register a file to be deleted on tearDown.
|
|
*
|
|
* @param string $fileName
|
|
*/
|
|
public static function registerFileForCleanUp($fileName)
|
|
{
|
|
self::$_filesToCleanUp[] = $fileName;
|
|
}
|
|
}
|