Practical PHP Testing Patterns: Fake Object
Join the DZone community and get the full member experience.
Join For FreeThe purpose of a Fake Object, a kind of Test Double, is to replace a collaborator with a functional copy. While Mocks prefer a specification of the behavior to check, Fake Objects are really a simplified version of the production object they substitute.
A good Fake Object, however, will be very lightweight, easy to create and throw away to ensure test isolation. Usually it won't satisfy some other functional or non-functional requirements, otherwise we would use him instead of the real object. For example a UserRepository, which normally stores users in a database, may be substitued by a Fake that:
- does not send mails when an user is added.
- Doesn't really save anything in the database, but keeps a list in an array.
- When some complex method is called, just throws a NonImplementedException.
Utility
Of course performance and less brittleness of tests are the main selling points of a Fake Objects: think about testing with a real database and without one; however this is true for all Test Doubles and I don't need to repeat it.
Often the Fake is used when the contract between the SUT and the collaborator is too complex to effectively build a Stub or Mock:
- many methods, or many calls to the same method are made.
- Return parameters are difficult to setup.
A Fake avoids overspecifying a test for a very invasive contract, like the order of calls and precise parameters; a real implementation is more robust to changes in the contract.
For example if the Fake is a collection, you can store and retrieve objects of another type, and still not change the Fake; expectations and configured return value instead would change (together).
As an example, an embryonal Fake is PHPUnit $this->returnArgument() for the will() Mock expectation. It's a piece of functionality which substitutes an hard-coded expectation, in order to return one of the arguments of the call instead of adding the argument to the expectation itself.
Note that many times we don't have to create new code to leverage Fakes: we can use the existing one with a different configuration (like a Cache maximumCachedItems=0, or with an adapter that use an in-memory cache instead of APC or Memcache) or something provided for us, like an sqlite in-memory database created by PDO. NullObjects are sometimes Fake, but are used in production more than in testing.
Implementation
Several steps are necessary for building an hand-rolled Fake:- create the Fake class by hand, and define the simplest implementation that could do the job for this test. Remember, you're not testing the Fake, you're testing the SUT, so the Fake doesn't have to be perfect, but just passable for the test case at end.
- Instance an object from the Fake class: the constructor may be different from the real collaborator's one as it's not part of the contract.
- Install the Fake into the SUT, and proceed as normal.
- create the "mock" as always via getMock() or getMockBuilder().
- Define expectations for its methods with will($this->returnCallback()) and some anonymous functions (or via self-shunting like we did for "Stubs"). Often objects passed in this closures (such as ArrayObject instances) can aid in making the Fake methods communicate with each other.
- Install the Fake and proceed with the test.
Variations
- Fake Database: an alternative implementation of the Database Facade that lets you test classes that depend on the database without actually using it. For exaple, a DAO which internally use an SplObjectStorage instead of the connection.
- In-Memory Database: sqlite3 in-memory database used with ORMs for tests that isolate you from the real database, but not from using PDO. High Return On Investment.
- Fake Web Service: mimics the interface of some web service like Google Analytics, when you have to interact also in write and not only in read.
- Fake Service Layer: simplified implementations of Service Layer classes, which avoid checking concurrency, authorization, authentication and so on in order to simplify functional testing.
Example
In this code sample, the test target a ForumManager class which needs to manipulate Posts. It's not a simple Facade: it moves Post around, merges threads and so on. So we inject in it a FakePostDao: ForumManager will call it instead of the database.
When you I have many tests of this type, the time for writing the Fake implementation is well spent.
<?php
/**
* The System Under Test should use this database-related class to move Posts around.
* However we inject a FakePostDao in order to avoid using a real database in this test,
* and at the same time better define the interface between the two.
*/
class FakeObjectTest extends PHPUnit_Framework_TestCase
{
public function testMergesTwoThreads()
{
$dao = new FakePostDao(array(
1 => array(
new Post('Hello'),
new Post('Hello!'),
new Post('')
),
2 => array(
new Post('Hi'),
new Post('Hi!')
),
3 => array(
new Post('Good morning.')
)
));
$forumManager = new ForumManager($dao);
$forumManager->mergeThreadsByIds(1, 2);
$thread = $dao->getThread(1);
$this->assertEquals(5, count($thread));
}
}
/**
* The SUT.
*/
class ForumManager
{
private $dao;
public function __construct(PostsDao $dao)
{
$this->dao = $dao;
}
public function mergeThreadsByIds($originalId, $toBeMergedId)
{
$original = $this->dao->getThread($originalId);
$toBeMerged = $this->dao->getThread($toBeMergedId);
$newOne = array_merge($original, $toBeMerged);
$this->dao->removeThread($originalId);
$this->dao->removeThread($toBeMergedId);
$this->dao->addThread($originalId, $newOne);
}
}
/**
* Interface for the collaborator to substitute with the Test Double.
*/
interface PostsDao
{
public function getThread($id);
public function removeThread($id);
public function addThread($id, array $thread);
}
/**
* Fake implementation.
*/
class FakePostDao implements PostsDao
{
private $threads;
public function __construct(array $initialState)
{
$this->threads = $initialState;
}
public function getThread($id)
{
return $this->threads[$id];
}
public function removeThread($id)
{
unset($this->threads[$id]);
}
/**
* We model Thread as array of Posts for simplicity.
*/
public function addThread($id, array $thread)
{
$this->threads[$id] = $thread;
}
}
/**
* Again a Dummy object: minimal implementation, to make this test pass.
*/
class Post
{
}
Opinions expressed by DZone contributors are their own.
Comments