Are You Using Mocks When You Should Be Using Test Data Builders?
This post is a follow-up to a question that came up during a presentation that Jason and I gave at the March XP Toronto meeting.
We want to test the
addMember() method of the CareTeamMembershipService. This method makes a Patient a member of a CareTeam.
As you can see from the implementation below there are two business rules to enforce: (1) patients can only be added to care teams at facilities where the patient is registered and (2) patients have to meet the care team membership criteria.
public void addMember(Patient patient, CareTeam careTeam) {
if (!patient.isRegisteredAt(careTeam.getFacility())) {
throw new CareTeamAdminException();
}
if (!patient.meets(careTeam.getMembershipCriteria())) {
throw new CareTeamAdminException();
}
careTeamMembershipDao.create(patient.getId(), careTeam.getId());
}
Here is the test for the
addMember() method using mock objects and test data builders:
public void should_permit_add_for_appropriate_care_team() {
final Facility jacobi = aFacility().build();
final Patient patient = aPatient().at(jacobi).age(18).with(DIABETES).build();
final CareTeam careTeam = anAdultCareTeam().at(jacobi).with(DIABETES).build();
context.checking(new Expectations() {{
one(careTeamMembershipDao).create(patient.getId(), careTeam.getId());
}});
sut.addMember(patient, careTeam);
}
Now that I'm quite used to jMock and test data builder syntax, I find that this test reads like a specification.
During our presentation there was one very interesting question from the audience that was never answered directly: Could you use a Mock instead of a Test Data Builder for the
Facility instance?How about we try right now and see what happens?
Use a mock for the jacobi Facility instance.
public void should_permit_add_for_appropriate_care_team() {
final Facility jacobi = context.mock(Facility.class);
final Patient patient = aPatient().at(jacobi).age(18).with(DIABETES).build();
final CareTeam careTeam = anAdultCareTeam().at(jacobi).with(DIABETES).build();
context.checking(new Expectations() {{
one(careTeamMembershipDao).create(patient.getId(), careTeam.getId());
}});
sut.addMember(patient, careTeam);
}
The test fails because
Facility is not an interface.One solution is to create interfaces for all your domain objects. Yech. Alternatively the jMock framework will let you mock concrete and abstract classes although it is discouraged. To enable this feature you have to configure the context in a special way.
Use a ClassImposteriser mockery.
final Mockery context = new JUnit4Mockery() {{
setImposteriser(ClassImposteriser.INSTANCE);
}};
Now the test fails because of an unexpected invocation:
facility.register().
Add expectations on the facility mock object.
public void should_permit_add_for_appropriate_care_team() throws Exception {
final Facility jacobi = context.mock(Facility.class);
context.checking(new Expectations() {{
one(jacobi).register(with(any(Patient.class)));
one(jacobi).register(with(any(CareTeam.class)));
}});
final Patient patient = aPatient().at(jacobi).age(18).with(DIABETES).build();
final CareTeam careTeam = anAdultCareTeam().at(jacobi).with(DIABETES).build();
context.checking(new Expectations() {{
one(careTeamMembershipDao).create(patient.getId(), careTeam.getId());
}});
sut.addMember(patient, careTeam);
}
This fix is not so simple because we have to put both of the register expectations before we've actually created our patient and careTeam. We also have to specify the expected arguments with matchers rather than the actual arguments.
Now the test fails because of an unexpected invocation: facility.getPatients().
Add another expectation on the facility mock object.
public void should_permit_add_for_appropriate_care_team() throws Exception {
final Facility jacobi = context.mock(Facility.class);
context.checking(new Expectations() {{
one(jacobi).register(with(any(Patient.class)));
one(jacobi).register(with(any(CareTeam.class)));
}});
final Patient patient = aPatient().at(jacobi).age(18).with(DIABETES).build();
final CareTeam careTeam = anAdultCareTeam().at(jacobi).with(DIABETES).build();
context.checking(new Expectations() {{
one(jacobi).getPatients(); will(returnValue(singleton(patient)));
one(careTeamMembershipDao).create(patient.getId(), careTeam.getId());
}});
sut.addMember(patient, careTeam);
}
Now the test finally passes. However, I find it way less readable as a specification. Why does the specification for the addMember() function require expectations on the Facility instance?
Here is the same test where all the test data builders have been replaced by mock objects.
public void should_permit_add_for_appropriate_care_team() throws Exception {
final Facility jacobi = context.mock(Facility.class);
final Patient patient = context.mock(Patient.class);
final CareTeam careTeam = context.mock(CareTeam.class);
final MembershipCriteria membershipCriteria = context.mock(
MembershipCriteria.class);
context.checking(new Expectations() {{
one(patient).getId(); will(returnValue("id1"));
one(careTeam).getId(); will(returnValue("id2"));
one(careTeam).getFacility(); will(returnValue(jacobi));
one(patient).isRegisteredAt(jacobi); will(returnValue(true));
one(careTeam).getMembershipCriteria(); will(returnValue(membershipCriteria));
one(patient).meets(membershipCriteria); will(returnValue(true));
one(careTeamMembershipDao).create("id1", "id2");
}});
sut.addMember(patient, careTeam);
}
This is what my tests looked like when I used mock objects without test data builders.
I find it really hard to separate the expectations that have to do with the system under test from the expectations that have to do with the data setup. I also have to know lots of details about the internals of the depended-on components:
patient, careTeam, membershipCriteria, and facility.So always use mock objects with test data builders and your tests can look like this:
public void should_permit_add_for_appropriate_care_team() {
final Facility jacobi = aFacility().build();
final Patient patient = aPatient().at(jacobi).age(18).with(DIABETES).build();
final CareTeam careTeam = anAdultCareTeam().at(jacobi).with(DIABETES).build();
context.checking(new Expectations() {{
one(careTeamMembershipDao).create(patient.getId(), careTeam.getId());
}});
sut.addMember(patient, careTeam);
}
Sweet.


