Fake Object
How can we verify logic independently when depended-on objects cannot be used?
How can we avoid Slow Tests?
We replace a component that the SUT depends on with a much lighter-weight implementation.
The SUT often depends on other components or systems. Although the interactions with these other components may be necessary, the side effects of these interactions as implemented by the real 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 on which the SUT depends 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 provide only the equivalent services to the SUT so that the SUT remains unaware it isn’t using the real DOC.
A Fake Object is a kind of Test Double that is similar to a Test Stub in many ways, including the need to install into the SUT a substitutable dependency. Whereas a Test Stub acts as a control point to inject indirect inputs into the SUT, however, the Fake Object does not: It merely provides a way for the interactions to occur in a self-consistent manner. These interactions (i.e., 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 behavior with that of Test Stubs and Mock Objects , 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 typically involve initializing the state of the DOC may also be done with the Fake Object directly using Back Door Manipulation. Techniques such as 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) because they simply bind 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 the SUT depends on other components that are unavailable or that make testing difficult or slow (e.g., Slow Tests) and the tests need more complex sequences of behavior than are worth implementing in a Test Stub or Mock Object. It must also be easier to create a lightweight implementation than to build and program suitable Mock Objects, at least in the long run, if building a Fake Object is to be worthwhile.
Using a Fake Object helps us avoid Overspecified Software because we do not encode the exact calling sequences expected of the DOC within the test. The SUT can vary how many times the methods of the DOC are called without causing tests to fail.
If we need to control the indirect inputs or verify the indirect outputs of the SUT, we should probably use a Mock Object or Test Stub instead.
Some specific situations where we replace the real component with a Fake Object are described next.
Variation: Fake Database
With the Fake Database pattern, the real database or persistence layer is replaced by a Fake Object that is functionally equivalent but that has much better performance characteristics. An approach we have often 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 kind of In-Memory Database 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 approach allows us to focus on testing the user interface 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
- 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 owing 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 needs of the SUT, at least for the purposes of specific tests, with relatively little effort.
A popular 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 DOC’s services. If this proves successful, we may consider expanding the Fake Object to handle additional tests. Over time, we may find that we can run all of our tests using the Fake Object. (See the sidebar “Faster Tests Without Shared Fixtures” 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 ; more traditional developers may favor Dependency Lookup. The latter technique is also more appropriate when we introduce a Fake Database (see Fake Object) in an effort to speed up execution of the customer tests; Dependency Injection doesn’t work so well with these kinds of tests.
Motivating Example
In this example, the SUT 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 then the test removes the records from the database (several deletes). All of this work takes time—several seconds per test. This very quickly adds up to minutes, and soon we find that our developers aren’t running the tests quite so frequently. Here is an example of one of these tests:
public void testReadWrite() throws Exception{ // Setup FlightMngtFacade facade = new FlightMgmtFacadeImpl(); BigDecimal yyc = facade.createAirport("YYC", "Calgary", "Calgary"); BigDecimal lax = facade.createAirport("LAX", "LAX Intl", "LA"); facade.createFlight(yyc, lax); // Exercise List flights = facade.getFlightsByOriginAirport(yyc); // Verify assertEquals( "# of flights", 1, flights.size()); Flight flight = (Flight) flights.get(0); assertEquals( "origin", yyc, flight.getOrigin().getCode()); }
The test calls createAirport on our Service Facade [CJ2EEP], which calls, among other things, our data access layer. Here is the actual implementation of several of the methods we are calling:
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; }
The calls to dataAccess.createAirport, dataAccess.createFlight, and TransactionManager.commitTransaction cause our test to slow down the most. The calls to dataAccess.getAirportByPrimaryKey and dataAccess.getFlightsByOriginAirport are a lesser factor but still contribute to the slow test.
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 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 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; we merely set up the fixture in the normal way.
Example: Fake Database
In this example, we’ve created a Fake Object that replaces the database—that is, 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()); BigDecimal yyc = facade.createAirport("YYC", "Calgary", "Calgary"); BigDecimal lax = facade.createAirport("LAX", "LAX Intl", "LA"); facade.createFlight(yyc, lax); // Exercise List flights = facade.getFlightsByOriginAirport(yyc); // Verify assertEquals( "# of flights", 1, flights.size()); Flight flight = (Flight) flights.get(0); assertEquals( "origin", yyc, flight.getOrigin().getCode()); }
Here’s the implementation of the Fake Database:
public class InMemoryDatabase implements FlightDao{ private List airports = new Vector(); public Airport createAirport(String airportCode, String name, String nearbyCity) throws DataException, InvalidArgumentException { 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); }
Now all we need is the implementation of the method that installs the Fake Database into the facade to make our developers more than happy to run all the tests after every code change.
public void setDao(FlightDao) { dataAccess = dao; }
Further Reading
The sidebar “Faster Tests Without Shared Fixtures” provides a more in-depth description of how we faked out the entire database with hash tables and made our tests run 50 times faster. Mocks, Fakes, Stubs, and Dummies (in Appendix B) contains a more thorough comparison of the terminology used in various books and articles.