Fake Object
The book has now been published and the content of this chapter has likely changed substanstially.Please see page 551 of xUnit Test Patterns for the latest information.
Also known as: Dummy
Variation of: Test Double
How can we verify logic independently when depended-on objects cannot be used?
How can we avoid Slow Tests?
Replace a component that the system under test (SUT) depends on with a much lighter-weight implementation.
Sketch Fake Object embedded from Fake Object.gif
The SUT often depend on other components or systems. The interactions with these other components may be necessary but the side-effects of these interactions as implemented by the real depended-on component (DOC), may be unnecessary or even detrimental.
A Fake Object is a much simpler and lighter weight implementation of the functionality provided by the DOC without the side effects we choose to do without.
How It Works
We acquire or build a very lightweight implementation of the same functionality as provided by a component that the SUT depends on and instruct the SUT to use it instead of the real DOC. This implementation need not have any of the "-ilities" that the real DOC needs to have (such as scalability); it need only provide the equivalent services to the SUT so that the SUT isn't aware it isn't using the real DOC.
A Fake Object is a kind of Test Double (page X) that is similar to a Test Stub (page X) in many ways including the need to install into the SUT a substitutable dependency but while a Test Stub acts as a control point to inject indirect inputs into the SUT the Fake Object does not. It merely provides a way for the interactions to occur in a self-consistent manner. These interactions (between the SUT and the Fake Object) will typically be many and the values passed in as arguments of earlier method calls will often be returned as results of later method calls. (Contrast this with Test Stubs and Mock Objects (page X) where the responses are either hard-coded or configured by the test.)
While the test does not normally configure a Fake Object, complex fixture setup that would normally involve initializing the state of the DOC may also be done with the Fake Object directly using Back Door Manipulation (page X). Techniques like Data Loader (see Back Door Manipulation) and Back Door Setup (see Back Door Manipulation) can be used quite successfully with less fear of Overspecified Software (see Fragile Test on page X) because it only binds us to the interface between the SUT and the Fake Object; the interface used to configure the Fake Object is a test-only concern.
When To Use It
We should use a Fake Object whenever our SUT depends on other components that are unavailable or which make testing difficult or slow (e.g. Slow Tests (page X)) and the tests need more complex sequences of behavior than is worth implementing in a Test Stub or Mock Object. It must also be easier to create a lightweight implementation than it would be to build and program suitable Mock Objects, at least in the long run, to make building a Fake Object worthwhile.
Using a Fake Object helps avoid Overspecified Software by not encoding within the test the exact calling sequences expected of the DOC. The SUT can vary how many times the methods of the DOC are called without causing tests to fail.
If we need to control either the indirect inputs or indirect outputs of the SUT, we should probably be using a Mock Object or Test Stub instead.
Some specific situations where we replace the real component with a Fake Object are:
Variation: Fake Database
The real database or persistence layer is replaced by a fake one that is functionally equivalent but which has much better performance characteristics. An approach we have commonly used involves replacing the database with a set of in-memory HashTables that act as a very lightweight way of retrieving objects that have been "persisted" earlier in the test.
Variation: In-Memory Database
Another example of a Fake Object is the use of a small-footprint, diskless database instead of a full-featured disk-based database. This will improve the speed of tests by at least an order of magnitude while giving up less functionality than a Fake Database.
Variation: Fake Web Service
When testing software that depends on other components that are accessed as Web Services, we can build a small hard-coded or data-driven implementation that can be used instead of the real Web Service to make our tests more robust and to avoid having to create a test instance of the real Web Service in our development environment.
Variation: Fake Service Layer
When testing user interfaces, we can avoid Data Sensitivity (see Fragile Test) and Behavior Sensitivity (see Fragile Test) of the tests by replacing the component that implements the Service Layer[PEAA] (including the domain layer) of our application with a Fake Object that returns remembered or data-driven results. This allows us to focus on testing the UI without having to worry about the data being returned changing over time.
Implementation Notes
Introducing a Fake Object involves two basic concerns:
- Building the Fake Object implementation, and
- Installing the Fake Object.
Building the Fake Object
Most Fake Objects are hand-built. Often, the Fake Object is used to replace a real implementation that suffers from latency issues due to real messaging or disk i/o with a much lighter in memory implementation. With the rich class libraries available in most object-oriented programming languages, it is usually possible to build a fake implementation that is sufficient to satisfy the need of the SUT, at least for the purposes of specific tests, with relatively little effort.
A commonly used strategy is to start by building a Fake Object to support a specific set of tests where the SUT requires only a subset of the services of the DOC. If this proves successful, consider expanding the Fake Object to handle additional tests. Over time, we may find that we can run all our tests using the Fake Object. (See the sidebar Faster Tests Without Shared Fixtures (page X) for a description of how we faked out the entire database with hash tables and made our tests run 50 times faster.)
Installing the Fake Object
Of course, we must have a way of installing the Fake Object into the SUT to be able to take advantage of it. We can use whichever substitutable dependency pattern the SUT supports. A common approach in the test-driven development community is Dependency Injection (page X) while more traditional development may favor Dependency Lookup (page X). The latter is also more appropriate when introducing a Fake Database (see Fake Object on page X) to speed up customer tests execution; Dependency Injection doesn't work so well with customer tests.
Motivating Example
In this example, we have a SUT that needs to read and write records from a database. The test must set up the fixture in the database (several writes), the SUT interacts (reads and writes) with the database several more times and and these test then removes the records from the database (several deletes). All this takes time. Several seconds per test. This very quickly adds up to minutes and we find our developers aren't running the tests very often any more. Here is an example of one of these tests:
public void testReadWrite() throws Exception{ // Setup: FlightMngtFacade facade = new FlightMgmtFacadeImpl(); // Exercise: BigDecimal yyc = facade.createAirport("YYC", "Calgary", "Calgary"); BigDecimal lax = facade.createAirport("LAX", "LAX Intl", "LA"); facade.createFlight(yyc, lax); List flights = facade.getFlightsByOriginAirport(yyc); // Verify: assertEquals( "# of flights", 1, flights.size()); Flight flight = (Flight) flights.get(0); assertEquals( "origin", yyc, flight.getOrigin().getCode()); } Example RealDbTest embedded from java/com/clrstream/ex6/domain/test/FlightServiceTest.java
Note that the test calls createAirport on our Service Facade[CJ2EEP] which calls, amongst others, our data access layer. Here is the actual implementation of the method we are testing:
public BigDecimal createAirport( String airportCode, String name, String nearbyCity) throws FlightBookingException{ TransactionManager.beginTransaction(); Airport airport = dataAccess.createAirport(airportCode, name, nearbyCity); logMessage("Wrong Action Code", airport.getCode());//bug TransactionManager.commitTransaction(); return airport.getId(); } public List getFlightsByOriginAirport( BigDecimal originAirportId) throws FlightBookingException { if (originAirportId == null) throw new InvalidArgumentException( "Origin Airport Id has not been provided", "originAirportId", null); Airport origin = dataAccess.getAirportByPrimaryKey(originAirportId); List flights = dataAccess.getFlightsByOriginAirport(origin); return flights; } Example DaoClient embedded from java/com/clrstream/ex6/domain/FlightMgmtFacadeImpl.java
The calls to dataAccess.createAirport and TransactionManager.commitTransaction are what cause our test to slow down the most; calls to dataAccess.getAirportByPrimaryKey and dataAccess.getFlightsByOriginAirport are a lessor but still contributing factor.
Refactoring Notes
The steps for introducing a Fake Object are very similar to those for adding a Mock Object. If one doesn't already exist, we use a Replace Dependency with Test Double (page X) refactoring to introduce a way to substitute the Fake Object for the DOC, usually a field (attribute) to hold the reference to it. In statically typed languages, we may have to do an Extract Interface[Fowler] refactoring before we can introduce the fake implementation. Then, we use this interface as the type of the variable that holds the reference to the substitutable dependency.
One notable difference is that we do not need to configure the Fake Object with expectations or return values.
Example: Fake Object
In this example, we've created a Fake Object that replaces the database; a "fake database" implemented entirely in memory using hash tables. The test doesn't change a lot, but the test execution occurs much, much faster.
public void testReadWrite_inMemory() throws Exception{ // Setup: FlightMgmtFacadeImpl facade = new FlightMgmtFacadeImpl(); facade.setDao(new InMemoryDatabase()); // Exercise: BigDecimal yyc = facade.createAirport("YYC", "Calgary", "Calgary"); BigDecimal lax = facade.createAirport("LAX", "LAX Intl", "LA"); facade.createFlight(yyc, lax); List flights = facade.getFlightsByOriginAirport(yyc); // Verify: assertEquals( "# of flights", 1, flights.size()); Flight flight = (Flight) flights.get(0); assertEquals( "origin", yyc, flight.getOrigin().getCode()); } Example FakeDbTest embedded from java/com/clrstream/ex6/domain/test/FlightServiceTest.java
And here's the implementation of the Fake Object:
public class InMemoryDatabase implements FlightDao{ private List airports = new Vector(); public Airport createAirport(String airportCode, String name, String nearbyCity) throws DataException, InvalidArgumentException {|-------10--------20--------30--------40--------50--------60-----|
assertParamtersAreValid( airportCode, name, nearbyCity); assertAirportDoesntExist( airportCode); Airport result = new Airport(getNextAirportId(), airportCode, name, createCity(nearbyCity)); airports.add(result); return result; } public Airport getAirportByPrimaryKey(BigDecimal airportId) throws DataException, InvalidArgumentException { assertAirportNotNull(airportId); Airport result = null; Iterator i = airports.iterator(); while (i.hasNext()) { Airport airport = (Airport) i.next(); if (airport.getId().equals(airportId)) { return airport; } } throw new DataException("Airport not found:"+airportId); } Example FakeDatabase embedded from java/com/clrstream/ex6/persistence/InMemoryDatabase.java
Now all we need is the method to install the implementation of the method to install the Fake Object to make our developers are more than happy to run all the tests after every code change.
public void setDao(InMemoryDatabase dao) { dataAccess = dao; } Example DaoInstallationMethod embedded from java/com/clrstream/ex6/domain/FlightMgmtFacadeImpl.java
Further Reading
See the sidebar Faster Tests Without Shared Fixtures for a more in-depth description of how we faked out the entire database with hash tables and made our tests run 50 times faster. The appendix Mocks, Fakes, Stubs and Dummies contains a more thorough comparison of the terminology used in various books and articles.
Copyright © 2003-2008 Gerard Meszaros all rights reserved