Dummies
- passed around but never actually used. Usually they are just used to fill parameter lists
Fakes
- have working implementations, but usually take some shortcut which makes them not suitable for production (an in memory database is a good example)
Stubs (classical unit testing)
- make the SUT believe it's talking with its real collaborators
- state verification
- provide canned answers to calls
- may also record information about calls, such as an email gateway stub that remembers the messages it 'sent', or maybe only how many messages it 'sent'
- typically, you have to create not just the SUT but also all the collaborators that the SUT needs in response to the test
- sometimes, alot of work goes into creating test fixtures, but they can be resused
- cares about the final state - not how that state was derived
- it's important to only think about what happens from the external interface and to leave all consideration of implementation until after you're done writing the test
- stubs do not define behavior, just a return value
- mainly useful when testing query methods that return a result and do not change the observable state of the system (are free of side effects)
Mocks (mockist/behavior driven testing)
- make the SUT believe it's talking with its real collaborators
- behavior verification
- pre-programmed objects with expectations which form a specification of the calls they are expected to receive
- typically, the SUT and mocks for its immediate collaborator objects
- less fixture setup required, but mocks must be created every time
- you are testing the outbound calls of the SUT to ensure it talks properly to its suppliers
- Mockist tests are thus more coupled to the implementation of a method. Changing the nature of calls to collaborators usually cause a mockist test to break (This can be worsened by the nature of mock toolkits)
- couples tests to implementation (changing the nature of calls to collaborators usually causes tests to break)
- Coupling to the implementation interferes with refactoring, since implementation changes are much more likely to break tests than with classic testing
- writing the test makes you think about the implementation of the behavior
- must constantly think about how the SUT is going to be implemented in order to write the expectations
- encourage behavior rich objects
- Mocks act as a canary in coal mine: they are an early warning system that your code is beginning to depart from the path of small methods, each having a single responsibility, and each interacting with a very small set of collaborators
- if you aren’t using tests to drive your design, there’s little point to using mock objects
- Mock objects make more sense in an outside-in model of test-driven development
- Mock objects enable developers to ferret out the needed interfaces of objects that don’t exist yet
- The mocks you write while TDDing one layer of the design gives you the clues you need to work out the necessary responsibilities of the next layer down
- mocks expect a certain method call, but define no return value
- mainly useful when tesing command methods that change the state of a system but do not return a value
Examples
Stub Example
public interface MailService {
public void send (Message msg);
}
public class MailServiceStub implements MailService {
private List messages = new ArrayList();
public void send (Message msg) {
messages.add(msg);
}
public int numberSent() {
return messages.size();
}
}
We can then use state verification on the stub like this.
class OrderStateTester...
public void testOrderSendsMailIfUnfilled() {
Order order = new Order(TALISKER, 51);
MailServiceStub mailer = new MailServiceStub();
order.setMailer(mailer);
order.fill(warehouse);
assertEquals(1, mailer.numberSent());
}
Mock Example
class OrderInteractionTester...
public void testOrderSendsMailIfUnfilled() {
Order order = new Order(TALISKER, 51);
Mock warehouse = mock(Warehouse.class);
Mock mailer = mock(MailService.class);
order.setMailer((MailService) mailer.proxy());
mailer.expects(once()).method("send");
warehouse.expects(once()).method("hasInventory")
.withAnyArguments()
.will(returnValue(false));
order.fill((Warehouse) warehouse.proxy());
}
}
Example Discussion
In both cases I'm using a test double instead of the real mail service. There is a difference in that the stub uses state verification while the mock uses behavior verification.In order to use state verification on the stub, I need to make some extra methods on the stub to help with verification. As a result the stub implements MailService but adds extra test methods.
Mock objects always use behavior verification, a stub can go either way. The difference is in how exactly the double runs and verifies the results.
References
http://xunitpatterns.com/http://martinfowler.com/articles/mocksArentStubs.html
http://confreaks.com/videos/659-rubyconf2011-why-you-don-t-get-mock-objects
http://devblog.avdi.org/2011/09/06/making-a-mockery-of-tdd/
http://jmock.org/oopsla2004.pdf
http://martinfowler.com/bliki/CommandQuerySeparation.html
No comments:
Post a Comment