Test Double Patterns
Some time ago I wrote an article about the consequences of using Test Double, but there was nothing about Test Double Patterns, nothing more than a simple list. Today I would like to change it and explain the differences between those patterns.
As I wrote in mentioned article that:
Test Double are patterns that allow us to control dependencies between the tested unit. To make it possible to provide wanted behavior whenever we want to or/and verify whether wanted behavior occurred.
So now when you have been reminded of the basics, we can move to the interesting part – let’s look at Test Double Patterns.
Dummy Object
Dummy is TD (Test Double) that is used when we would like to pass an object to fill the parameters list. It is never actually used. That’s the reason why it is not always treated as a one of TD – it does not provide any behavior.
Let’s assume that we’ve got Sender class that sends Reports. Because of some requirements we need to wrap it into another class to provide valid interface. Our class can look like this:
public class ReportProcessor implements Processor { private Sender sender; public ReportProcessor(Sender sender) { this.sender = sender; } @Override public void process(Report report) { sender.send(report); } }
Now, what can our tests look like? What do we need to verify? We have to check whether the report was passed to the send() method of Sender instance. It can be done as follows:
public class DummyTest { @Test public void shouldSentReportWhileProcessing() { Sender sender = aMessageSender(); ReportProcessor reportProcessor = aReportProcessor(sender); Report dummyReport = new Report(); reportProcessor.process(dummyReport); then(sender).should().send(dummyReport); } private ReportProcessor aReportProcessor(Sender sender) { return new ReportProcessor(sender); } private Sender aMessageSender() { return spy(Sender.class); } }
As you can see, there’s no interaction with our Dummy Object. The report is only created and passed as a parameter. No behavior, just presence.
Fake Object
Fake Object is just a simpler and lighter weight implementation of the object that tested class depends on. It provides expected functionality.
An important thing to remember when you are deciding for it is to make it as simple as possible. Any additional logic may have a great impact on your tests fragility and accuracy.
Let’s assume that we’ve got a ReportService with method create() and the responsibility of it is to create a Report only in case if it was not already created. For simplicity we can assume that title identify an object – we cannot have two reports with the same title:
public class ReportService { private ReportRepository reportRepository; public ReportService(ReportRepository reportRepository) { this.reportRepository = reportRepository; } public void create(Title title, Content content) { if (!reportRepository.existsWithTitle(title)) { Report report = new Report(title, content); reportRepository.add(report); } } }
We can test this code in a various ways, but we will decide for using Fake Object:
class FakeReportRepository implements ReportRepository { private Map<Title, Report> reports = new HashMap<>(); @Override public void add(Report report) { reports.put(report.title(), report); } @Override public boolean existsWithTitle(Title title) { return reports.containsKey(title); } @Override public int countAll() { return reports.size(); } @Override public Report findByTitle(Title title) { return reports.get(title); } }
And our test will look like that:
public class FakeTest { @Test public void shouldNotCreateTheSameReportTwice() { FakeReportRepository reportRepository = new FakeReportRepository(); ReportService reportService = aReportService(reportRepository); reportService.create(DUMMY_TITLE, DUMMY_CONTENT); reportService.create(DUMMY_TITLE, DUMMY_CONTENT); Report createdReport = reportRepository.findByTitle(DUMMY_TITLE); assertThat(createdReport.title()).isSameAs(DUMMY_TITLE); assertThat(createdReport.content()).isSameAs(DUMMY_CONTENT); assertThat(reportRepository.countAll()).isEqualTo(1); } private ReportService aReportService(ReportRepository reportRepository) { return new ReportService(reportRepository); } }
Stub Object
We are using Stub Object in case when we are interested in method output and we want to be sure that whenever it will be called the result will be exactly as we want to.
Usually we do not check in our test whether the Stub was called or not, because we will know it through other assertions.
In this example we will look at a ReportFactory that creates a Report with creation date. For sake of testability we used Dependency Injection to inject DateProvider:
public class ReportFactory { private DateProvider dateProvider; public ReportFactory(DateProvider dateProvider) { this.dateProvider = dateProvider; } public Report crete(Title title, Content content) { return new Report(title, content, dateProvider.date()); } }
It allows us to use Stub in our test:
public class StubTest { @Test public void shouldCreateReportWithCreationDate() { Date dummyTodayDate = new Date(); DateProvider dateProvider = mock(DateProvider.class); stub(dateProvider.date()).toReturn(dummyTodayDate); ReportFactory reportFactory = new ReportFactory(dateProvider); Report report = reportFactory.crete(DUMMY_TITLE, DUMMY_CONTENT); assertThat(report.creationDate()).isSameAs(dummyTodayDate); } }
As you can see, we are interested only in the result of calling Stub.
Spy Object
In opposition to Stub Objects we are using Spies when we are interested in spied method’s input. We are checking whether it was invoked or not. We can check how many times it was called.
We can use real application objects as Spies as well. There’s no need to create any additional class.
Let’s back to our ReportProcessor from first paragraph:
public class ReportProcessor implements Processor { // code @Override public void process(Report report) { sender.send(report); } }
Probably you already noticed that we were using Spy in there, but let’s look at the test once again:
public class SpyTest { @Test public void shouldSentReportWhileProcessing() { Sender sender = spy(Sender.class); ReportProcessor reportProcessor = aReportProcessor(sender); reportProcessor.process(DUMMY_REPORT); then(sender).should().send(DUMMY_REPORT); } private ReportProcessor aReportProcessor(Sender sender) { return new ReportProcessor(sender); } }
We want to check that an object was wrapped in right manner and parameter is passed to its (wrapped object) method call. That’s why we use Spy in here.
Mock Object
Mock Object is often described as a combination of Stub and Spy. We are specify what kind of input we are expecting to receive and basing on this we are return proper result.
Invoking a Mock Object can also result with throwing exception if this is what we expecting.
Ok, let’s look once again at our ReportService:
public class ReportService { //code public void create(Title title, Content content) { if (!reportRepository.existsWithTitle(title)) { Report report = new Report(title, content); reportRepository.add(report); } } }
Now, instead of Fake Object we will use Mock Object:
@RunWith(MockitoJUnitRunner.class) public class MockTest { @Mock private ReportRepository reportRepository; @InjectMocks private ReportService reportService; @Test public void shouldCreateReportIfDoesNotExist() { given(reportRepository.existsWithTitle(DUMMY_TITLE)).willReturn(false); reportService.create(DUMMY_TITLE, DUMMY_CONTENT); then(reportRepository).should().add(anyReport()); } @Test public void shouldNotCreateReportIfDoesNotExist() { given(reportRepository.existsWithTitle(DUMMY_TITLE)).willReturn(true); reportService.create(DUMMY_TITLE, DUMMY_CONTENT); then(reportRepository).should(never()).add(anyReport()); } private Report anyReport() { return any(Report.class); } }
To clarify everything, our Mock Object is ReportRepository.existsWithTitle() method. As you can see, in first test we say that if we invoke method with DUMMY_OBJECT parameter it will return true. In the second test we check opposite case.
Our last assertion (then().should()) in both tests is another TD Pattern. Can you recognize which one?
Last word on the end
And that’s all that I have to say about Test Double Patterns today. I encourage you to use them intentionally and not blindly follow the habit to add @Mock annotation whenever it is possible.
I also invite you to read an article about the consequences of using Test Double to find out what kinds of problem can you experience while using TD Patterns and how to recognize and solve such problems.
If you would like to deepen your knowledge about those patterns even further there’s a great page which will help you doing this: xUnit Patterns: Test Double.
Good luck with your tests! Make them readable and valuable.
If you have some thoughts or questions about Test Double Patterns just share it in comments.
Reference: | Test Double Patterns from our JCG partner Sebastian Malaca at the Let’s talk about Java blog. |