Skip to content

Commit

Permalink
Added partial mock support for classes marked final, or classes with …
Browse files Browse the repository at this point in the history
…methods marked final
  • Loading branch information
Padraic Brady committed May 21, 2010
1 parent 6f8e89c commit 86bc056
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 40 deletions.
16 changes: 16 additions & 0 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,22 @@ identical, you can invoke the recorder's strict mode from the closure block, e.g
$user = new SubjectUser;
$user->use($subject);
});

Dealing with Final Classes/Methods
----------------------------------

One of the primary restrictions of mock objects in PHP, is that mocking classes
or methods marked final is hard. The final keyword prevents methods so marked
from being replaced in subclasses (subclassing is how mock objects can inherit
the type of the class or object being mocked.

The simplest solution is not to mark classes or methods as final!

However, in a compromise between mocking functionality and type safety, Mockery
does allow creating partial mocks from classes marked final, or from classes with
methods marked final. This offers all the usual mock object goodness but the
resulting mock will not inherit the class type of the object being mocked, i.e.
it will not pass any instanceof comparison.

Quick Examples
--------------
Expand Down
8 changes: 5 additions & 3 deletions library/Mockery/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,17 @@ public function mock()
}
}
if (!is_null($name)) {
$mock = new \Mockery\Mock($name, $this);
$mock = new \Mockery\Mock();
$mock->mockery_init($name, $this);
} elseif(!is_null($class)) {
$mock = \Mockery\Generator::createClassMock($class);
$mock->mockery_init($class, $this);
} elseif(!is_null($partial)) {
$mock = \Mockery\Generator::createClassMock(get_class($partial));
$mock = \Mockery\Generator::createClassMock(get_class($partial), null, true);
$mock->mockery_init($class, $this, $partial);
} else {
$mock = new \Mockery\Mock('unknown', $this);
$mock = new \Mockery\Mock();
$mock->mockery_init('unknown', $this);
}
if (!empty($quickdefs)) {
$mock->shouldReceive($quickdefs);
Expand Down
53 changes: 35 additions & 18 deletions library/Mockery/Generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,26 +29,53 @@ class Generator
* class type hierarchy as a typical instance of the class being
* mocked.
*
*
* @param string $className
* @param string $mockName
* @param string $allowFinal
*/
public static function createClassMock($className, $mockName = null)
public static function createClassMock($className, $mockName = null, $allowFinal = false)
{
if (is_null($mockName)) $mockName = uniqid('Mockery_');
$class = new \ReflectionClass($className);
$definition = '';
if ($class->isFinal()) {
if ($class->isFinal() && !$allowFinal) {
throw new \Mockery\Exception(
'The class ' . $className . ' is marked final and it is not '
. 'possible to generate a mock object with its type'
'The class ' . $className . ' is marked final and its methods'
. ' cannot be replaced. Classes marked final can be passed in'
. 'to \Mockery::mock() as instantiated objects to create a'
. ' partial mock, but only if the mock is not subject to type'
. ' hinting checks.'
);
} elseif ($class->isFinal()) {
$className = '\\Mockery\\Mock';
}
$hasFinalMethods = false;
$methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC);
foreach ($methods as $method) {
if ($method->isFinal() && !$allowFinal) {
throw new \Mockery\Exception(
'The method ' . $method->getName()
. ' is marked final and it is not possible to generate a '
. 'mock object with such a method defined. You should instead '
. 'pass an instance of this object to Mockery to create a '
. 'partial mock.'
);
} elseif ($method->isFinal()) {
$className = '\\Mockery\\Mock';
$hasFinalMethods = true;
}
}
if ($class->isInterface()) {
$inheritance = ' implements ' . $className . ', \Mockery\MockInterface';
} elseif ($class->isFinal() || $hasFinalMethods) {
$inheritance = ' extends ' . $className;
} else {
$inheritance = ' extends ' . $className . ' implements \Mockery\MockInterface';
}
$definition .= 'class ' . $mockName . $inheritance . PHP_EOL . '{' . PHP_EOL;
$definition .= self::applyMockeryTo($class);
if (!$class->isFinal() && !$hasFinalMethods) {
$definition .= self::applyMockeryTo($class, $methods);
}
$definition .= PHP_EOL . '}';
eval($definition);
$mock = new $mockName();
Expand All @@ -60,19 +87,9 @@ public static function createClassMock($className, $mockName = null)
*
*
*/
public static function applyMockeryTo(\ReflectionClass $class)
public static function applyMockeryTo(\ReflectionClass $class, array $methods)
{
$definition = '';
$methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC);
foreach ($methods as $method) {
if ($method->isFinal()) {
throw new \Mockery\Exception(
'The method ' . $method->getName()
. ' is marked final and it is not possible to generate a '
. 'mock object with such a method defined'
);
}
}
/**
* TODO: Worry about all these other method types later.
*/
Expand Down Expand Up @@ -167,7 +184,7 @@ public static function _getStandardMethods()
protected \$_mockery_partial = null;
protected \$_mockery_disableExpectationMatching = false;
public function mockery_init(\$name, \Mockery\Container \$container = null, \$partialObject = null)
{
\$this->_mockery_name = \$name;
Expand Down
21 changes: 2 additions & 19 deletions library/Mockery/Mock.php
Original file line number Diff line number Diff line change
Expand Up @@ -104,27 +104,10 @@ class Mock implements MockInterface
* @var bool
*/
protected $_mockery_disableExpectationMatching = false;

/**
* Constructor
*
* @param string $name
* @param \Mockery\Container $container
*/
public function __construct($name, \Mockery\Container $container = null, $partialObject = null)
{
$this->_mockery_name = $name;
if(is_null($container)) {
$container = new \Mockery\Container;
}
$this->_mockery_container = $container;
if (!is_null($partialObject)) {
$this->_mockery_partial = $partialObject;
}
}

/**
* Alternative setup method to constructor
* We want to avoid constructors since class is copied to Generator.php
* for inclusion on extending class definitions.
*
* @param string $name
* @param \Mockery\Container $container
Expand Down
42 changes: 42 additions & 0 deletions tests/Mockery/ContainerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,39 @@ public function testPassingClosureAsFinalParameterUsedToDefineExpectations()
$this->assertEquals('bar', $m->foo());
}

/**
* @expectedException \Mockery\Exception
*/
public function testMockingAKnownConcreteFinalClassThrowsErrors_OnlyPartialMocksCanMockFinalElements()
{
$m = $this->container->mock('MockeryFoo3');
}

/**
* @expectedException \Mockery\Exception
*/
public function testMockingAKnownConcreteClassWithFinalMethodsThrowsErrors_OnlyPartialMocksCanMockFinalElements()
{
$m = $this->container->mock('MockeryFoo4');
}

public function testFinalClassesCanBePartialMocks()
{
$m = $this->container->mock(new MockeryFoo3);
$m->shouldReceive('foo')->andReturn('baz');
$this->assertEquals('baz', $m->foo());
$this->assertFalse($m instanceof MockeryFoo3);
}

public function testClassesWithFinalMethodsCanBePartialMocks()
{
$m = $this->container->mock(new MockeryFoo4);
$m->shouldReceive('foo')->andReturn('baz');
$this->assertEquals('baz', $m->foo());
$this->assertEquals('bar', $m->bar());
$this->assertFalse($m instanceof MockeryFoo4);
}

}

class MockeryTestFoo {
Expand All @@ -121,3 +154,12 @@ class MockeryTestFoo2 {
public function foo() { return 'foo'; }
public function bar() { return 'bar'; }
}

final class MockeryFoo3 {
public function foo() { return 'baz'; }
}

class MockeryFoo4 {
final public function foo() { return 'baz'; }
public function bar() { return 'bar'; }
}

0 comments on commit 86bc056

Please sign in to comment.