Lately I am using fake objects more than the other common kinds of doubles – stubs, spies and mocks. This is quite recent, I used to prefer stubs, spies or mocks. I find that fakes make my tests less fragile in the case of outside-in development when it comes to changes in the design. The problem is that there could be defects in the implementation of the fake objects. A tricky one may appear in the case of in-memory repositories:
public class InMemoryRepo : JobRepository {
readonly List jobs = new List();
public void Add(Job job)
{
jobs.Add(job);
}
public void Update(Job job)
{
// TODO
}
public List FindBy(string ownerId)
{
return jobs.Where(job => job.IsOwnedBy(ownerId)).ToList();
}
}
This implementation is misleading. If we are test-driving a service expecting it to update a Job instance, we may forget to invoke the repository’s Update method and it would still work.
[TestClass]
public class ServiceTest {
[Test]
public void AssignsOwnerToJob()
{
var service = Factory.CreateJobServiceWithFakeRepository();
var job = service.CreateJob(irrelevantTitle);
service.Assign(job, irrelevantOwnerId);
service.JobsOwnedBy(irrelevantOwnerId).Should().HaveCount(1);
}
}
public class Service {
private JobRepository repo {get; set;}
public Service(JobRepository repo){
this.repo = repo;
}
public Job CreateJob(string title)
{
var job = new Job();
repo.Add(job);
/*...*/
return job;
}
public void Assign(Job job, string ownerId){
/*...*/
job.Assign(ownerId);
/* BUG: am forgetting the call to "repo.Update(job)" right here */
}
public List JobsOwnedBy(string ownerId){
return repo.FindBy(ownerId);
}
}
The problem is that the in-memory repository is using the same object references all the time. When an object is passed in as an argument, it’s actually a copy of the reference. However if the repository was accessing a database it would most likely return different instances (references). A better implementation of the repository would be this one:
public class InMemoryRepo : JobRepository {
readonly List jobs = new List();
public void Add(Job job)
{
/* clone: the object added to the repo
is different to the given one */
jobs.Add(job.Clone());
}
public void Update(Job job)
{
/* find the given job,
remove it from the collection and
add a clone of the given one */
}
public List FindBy(string ownerId)
{
return jobs.Where(job => job.IsOwnedBy(ownerId)).ToList();
}
}
Conclusion: the behavior of the fake should be the same than the original when it comes to handling object references.