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.
SUT往往取决于其他组件或系统ms. Although the interactions with these other components may be necessary, the side effects of these interactionsas implemented by the real DOCmay be unnecessary or even detrimental.
AFake Objectis 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.
AFake Objectis a kind ofTest Doublethat is similar to aTest Stubin many ways, including the need to install into the SUT a substitutable dependency. Whereas aTest Stubacts as a control point to inject indirect inputs into the SUT, however, theFake Objectdoes not: It merely provides a way for the interactions to occur in a self-consistent manner. These interactions (i.e., between the SUT and theFake 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 ofTest StubsandMock Objects, where the responses are either hard-coded or configured by the test.
While the test does not normally configure aFake Object, complex fixture setup that would typically involve initializing the state of the DOC may also be done with theFake Objectdirectly usingBack Door Manipulation. Techniques such asData Loader(seeBack Door Manipulation) andBack Door Setup(seeBack Door Manipulation) can be used quite successfully with less fear ofOverspecified Software(seeFragile Test) because they simply bind us to the interface between the SUT and theFake Object; the interface used to configure theFake Objectis a test-only concern.
When to Use It
We should use aFake Objectwhenever 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 aTest StuborMock Object. It must also be easier to create a lightweight implementation than to build and program suitableMock Objects, at least in the long run, if building aFake Objectis to be worthwhile.
Using aFake Objecthelps us avoidOverspecified Software因为我们不编码的层层序es 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 aMock ObjectorTest Stubinstead.
Some specific situations where we replace the real component with aFake Objectare described next.
Variation: Fake Database
With theFake Databasepattern, the real database or persistence layer is replaced by aFake Objectthat 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 aFake Objectis the use of a small-footprint, diskless database instead of a full-featured disk-based database. This kind ofIn-Memory Databasewill improve the speed of tests by at least an order of magnitude while giving up less functionality than aFake 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 avoidData Sensitivity(seeFragile Test) andBehavior Sensitivity(seeFragile 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 aFake Objectinvolves two basic concerns:
- 构建Fake Objectimplementation
- Installing theFake Object
构建Fake Object
MostFake Objectsare hand-built. Often, theFake Objectis used to replace a real implementation that suffers from latency issues owing to real messaging or disk I/O with a much lighterin-memoryimplementation. 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 aFake Objectto 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 theFake Objectto handle additional tests. Over time, we may find that we can run all of our tests using theFake 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 theFake Objectinto 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 isDependency Injection; more traditional developers may favorDependency Lookup. The latter technique is also more appropriate when we introduce aFake Database(seeFake Object)为了加快执行自定义er tests;Dependency Injectiondoesn’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 ourService 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 aFake Objectare very similar to those for adding aMock Object. If one doesn’t already exist, we use a Replace Dependency with Test Double refactoring to introduce a way to substitute theFake Objectfor 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 wedo notneed to configure theFake Objectwith expectations or return values; we merely set up the fixture in the normal way.
Example: Fake Database
In this example, we’ve created aFake Objectthat replaces the database—that is, aFake Databaseimplemented 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 theFake 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 theFake Databaseinto 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.