Skip to content

Instantly share code, notes, and snippets.

@kaiwren
Created August 24, 2009 17:33
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kaiwren/173982 to your computer and use it in GitHub Desktop.
Save kaiwren/173982 to your computer and use it in GitHub Desktop.
// We're going to pick on Mad Service today. If you had a hand in writing this once upon a time, do not fret.
// This isn't accusation or finger-poiniting -- I see everyone's name all over this code, including my own, which
// makes it a nice target.
// This code just needs some tenderness. Let's cook it a hot bowl of soup and play some soft Jazz.
// Step zero! Does the class name make any sense?
public MadServiceImpl {
// ...
}
// Shishir asks: "What's a Mad?"
// Shishir could very well be the next developer to join our project (or any other project, for that matter).
// Make it easy for him... don't use abbreviations unless they're so core to the business they're used everywhere.
// ...even then, reconsider.
// In the case of Member Appreciation Discount, it's NOT core to the business so we can't abbreviate it in code.
public MemberAppreciationDiscountServiceImpl {
// ...
}
// That's better. Always use the domain language. IntelliJ and Eclipse type for you anyway, so what do you care?
// Step one... find that dirty method.
public boolean checkEligibility(Customer customer, GymPass familyPass) {
if (!familyPass.belongsTo(ProductGroupInternalIdentifier.familyPassProductGroups())) {
return true;
}
boolean validEmployeeStatus = validateEmployeeStatus(customer.primaryAffiliation());
boolean doesntHaveOverlappingPasses = validEmployeeStatus
&& transactionService.getPassOverlappingMonths(customer, familyPass.getEffectiveBeginDate(),
familyPass.getEffectiveEndDate()).size() == 0;
boolean doesntHaveMad = doesntHaveOverlappingPasses && !hasMadForPassEffectiveDates(customer, familyPass);
return doesntHaveMad && (familyPassDao.retrieveFamilyPass(customer, familyPass.getEffectiveBeginDate(),
familyPass.getEffectiveEndDate()) == null);
}
// Whew. A doozy.
// Shishir asks: "Is... uh, THIS... how you write all your domain rules?"
// I cough.
// There's more. That method has buddies.
private boolean validateEmployeeStatus(Affiliation affiliation) {
if (affiliation instanceof Employee) {
Employee employee = (Employee) affiliation;
return new MadEligibilityValidator(addressExclusionDao, clock).validEmployeeStatus(employee.getEmployeeStatus());
}
return true;
}
public boolean hasMadForGymPassEffectiveDates(Customer customer, GymPass pass) {
List<DateRange> dateRanges = FiscalYear.getFiscalYearsBetweenDates(pass.getEffectiveBeginDate(),
pass.getEffectiveEndDate());
DateTime passBeginDateWithDayOfMonthAsFirstDayOfMonth = pass.getEffectiveBeginDate().withDayOfMonth(1).toDateTimeAtStartOfDay();
DateTime passEndDateWithDayOfMonthAsLastDayOfMonth = pass.getEffectiveEndDate().dayOfMonth().withMaximumValue().toDateTimeAtStartOfDay();
for (DateRange dateRange : dateRanges) {
MemberAppreciationDiscount discount = getMad(customer, dateRange);
if (discount == null)
continue;
List<LocalDate> enrolledMonths = discount.getEnrolledMonthsAsDates();
for (LocalDate enrolledLocalDate : enrolledMonths) {
Interval overlappingInterval = new Interval(passBeginDateWithDayOfMonthAsFirstDayOfMonth, passEndDateWithDayOfMonthAsLastDayOfMonth).overlap(enrolledLocalDate.toInterval());
if (overlappingInterval != null)
return true;
}
}
return false;
}
public MemberAppreciationDiscount getMad(Customer customer, DateRange fiscalYear) {
logger.debug(customer);
return madDao.retrieveByCustomerAndMadYear(customer, fiscalYear);
}
// That's a lot of method for one little tiny service. When I picked this to chop up I didn't think I had quite so much
// work ahead of me. Nice job, Steve. Sigh.
// Can you spot the pain points?
// How about the fact that only one of those 3 methods is private? Even a private method can be consumed here at home
// (in MadServiceImpl, that is) ...but once she's public we're in for a world of pain. Also, why the heck is our public method
// calling another public method in the same class? Can you tell me which is MORE public? (One of them has to be.) I have no idea.
// I've said this a few times now, but remember: It's not as if one public method calling another public method is WRONG... or even BAD.
// Enums aren't wrong or bad.
// Booleans aren't wrong or bad.
// Ifs aren't wrong or bad.
// Switches aren't wrong or bad.
// Statics aren't wrong or bad.
// Singletons aren't wrong or bad.
// Inheritance isn't wrong or bad.
// Those things aren't wrong. They're not bad. THEY'RE SCARY AS HELL AND SMELL LIKE A BUM. Maybe you'll want to do these things sometime,
// but remember someone else has to smell that bum once you're done with it. There are plenty of smelly things about one public
// method consuming another public method on the same class, but chances are good you just need a new kind of object.
// I'll skip a step here and tell you that's our problem in this method.
// Stop. Get a beer. Make that object.
// What else scares you about this code? You have 5 seconds to tell me! Quick!
// Quicker!!!
// Time's up! Here they are:
// 1. transactionService
// 2. familyPassDao
// 3. addressExclusionDao
// 4. clock
// 5. madDao
// 6. logger (doesn't really count, since it doesn't need to be there)
// Damn. How'd we find all those out in less than 5 seconds? Were you reading the code top-to-bottom? You're a fast reader.
// How do slow readers like me have to do it?
// ...the same way we extract one object from another (which, at the end of the day, is what we're doing here): static.
// Temporarily mark all your offending methods with static and see what blows up. Other instance methods will show up (which you
// then must also mark as static), and fields will show up (which you have to deal with differently).
// Sometimes I'm convinced the only reason they put 'static' in Java was to find bad code. It's pretty much useless for anything
// else. Is there some cool trick we can do with 'switch' that could justify its existance?
// Step two... find that filthy test.
public void testShouldCheckIfCustomerHasMadForPassEffectiveDates() {
// ...
}
// It appears to be testing 5 full scenarios, which would be decent coverage for a service method if it weren't so
// gargantuan. Whoever wrote this test-of-tests had a lot more energy than I do.
// Service tests are a lot of work and we're programmers... we hate work!
// IMPORTANT TANGENT RANT: STEVE'S STOLEN LAWS (STARTING IN THE MIDDLE)
// POINT 17A, SUBPOINT 4: Good Programmers Are Lazy[TM]. (See: http://c2.com/cgi/wiki?LazinessImpatienceHubris)
// POINT 04R, SUBPOINT 6: Good Programmers Read ALL of c2.com. All the good books are written, and it's not a secret it's
// all on c2 alrady. Plus, if there happens to be a book out there you still want to read, c2.com will
// probably reference it somewhere.
// Okay, whatever. Shut up, Steve! Get to the point! Anyway.
// Let's take a look at the test, then refactor the test before we hit the code.
public void testShouldCheckIfCustomerHasMadForPassEffectiveDates() {
Set<MonthEnum> enrolledMonths = new HashSet<MonthEnum>(Arrays.asList(MonthEnum.JAN, MonthEnum.FEB, MonthEnum.DEC));
final DateRange dateRange = new DateRange(2007, 2008);
MadYear madYear = MadYear.createForTests(enrolledMonths, dateRange);
final MemberAppreciationDiscount discount = new MemberAppreciationDiscountBuilder().madYear(madYear).toMemberAppreciationDiscount();
GymPass longTermFamilyPass = FamilyPassFixture.createLongTermFamilyPass("123");
longTermFamilyPass.getPass().setEffectiveBeginDate(new ClockForUT(2007, DateTimeConstants.JULY, 1).date());
longTermFamilyPass.getPass().setEffectiveEndDate(new ClockForUT(2008, DateTimeConstants.JANUARY, 1).date());
Employee employee = new Employee();
employee.setEmployeeStatus("Leave Of Absence");
employee.setAffiliationType(AffiliationTypeFixture.UEM);
Customer customer = new CustomerBuilder().affiliation(employee).toCustomer();
customer.setPrimaryAffiliationType(AffiliationTypeFixture.UEM);
assertFalse(madService.checkEligibility(customer, longTermFamilyPass.getPass()));
mockery.checking(new Expectations() {
{
exactly(3).of(transactionServiceMock)
.getPassOverlappingMonths(with(any(Customer.class)), with(any(LocalDate.class)), with(any(LocalDate.class)));
will(returnValue(new ArrayList()));
exactly(6).of(madDaoMock).retrieveByCustomerAndMadYear(with(any(Customer.class)), with(any(DateRange.class)));
will(returnValue(discount));
one(familyPassDaoMock).retrieveFamilyPass(with(any(Customer.class)), with(any(LocalDate.class)), with(any(LocalDate.class)));
will(returnValue(null));
}
});
longTermFamilyPass.getPass().setEffectiveBeginDate(new ClockForUT(2007, DateTimeConstants.JULY, 1).date());
longTermFamilyPass.getPass().setEffectiveEndDate(new ClockForUT(2008, DateTimeConstants.JANUARY, 1).date());
assertFalse(madService.checkEligibility(this.customer, longTermFamilyPass.getPass()));
longTermFamilyPass.getPass().setEffectiveBeginDate(new ClockForUT(2007, DateTimeConstants.JULY, 1).date());
longTermFamilyPass.getPass().setEffectiveEndDate(new ClockForUT(2007, DateTimeConstants.NOVEMBER, 30).date());
assertTrue(madService.checkEligibility(this.customer, longTermFamilyPass.getPass()));
longTermFamilyPass.getPass().setEffectiveBeginDate(new ClockForUT(2008, DateTimeConstants.FEBRUARY, 28).date());
longTermFamilyPass.getPass().setEffectiveEndDate(new ClockForUT(2008, DateTimeConstants.AUGUST, 31).date());
assertFalse(madService.checkEligibility(this.customer, longTermFamilyPass.getPass()));
longTermFamilyPass.getPass().setEffectiveBeginDate(new ClockForUT(2008, DateTimeConstants.MARCH, 2).date());
longTermFamilyPass.getPass().setEffectiveEndDate(new ClockForUT(2009, DateTimeConstants.AUGUST, 31).date());
assertFalse(madService.hasMadForPassEffectiveDates(this.customer, longTermFamilyPass.getPass()));
}
// Wait... did you go read c2.com yet? All of it? Really? Even the dialogues between Kent Beck, Martin Fowler and Ward Cunningham?
// (It was a live wiki, edited by the rockstars of the industry, once upon a time.)
// I don't believe you. Go back and find more to read.
// test refactoring step #1... what's wrong with the test name?
// Should Check If Customer Has Mad For Pass Effective Dates
// Could the customer read this? does it describe intent? does it tell you about the example you, as a developer, can read
// inside the test? does it tell you a success or a failure condition?
// Not so much. Let's re-word it.
public testCustomerShouldBeEligibleForMemberAppreciationDiscountIfItOverlapsWithAPassHeHolds() {
// ...
}
// this, of course, becomes:
// Customer should be eligible for Member Appreciation Discount if it overlaps with a pass he holds.
// Your tests are many things:
// - They are an example of how the next developer (including yourself) should consume your code.
//
// - They are a specification. At the end of the project (heck, at the end of each iteration) you should be comfortable sending
// a text file with a list of test names (humanized, of course) to your customer and your customer should agree with everything
// you've written.
//
// - They are TESTS. A test doesn't prove anything, but it does tell you that for one very particular scenario, your code probably works.
//
// - They are CODE. You'll write bad tests as often as you'll write bad code. Since all code is bad, you'd better make darn sure your
// tests are simple, small, and readable. Inside and out.
//
// - They are the only confidence you, as a developer, should have in your code. Whether it's an acceptance test script written by a QA
// (manual or automated) or the world's tiniest unit test, this is your starting point. Without tests, the only assumption you can
// reasonably make about your code is that it doesn't work. This is why you should love your QAs. QAs are developers, and they're
// doing the only job that lets you sleep at night.
//
// - They are a central point of conversation. When your story fails BA volleyball, you should know exactly which test you need to
// check to determine where the faulty code lies. Read the test names out to the BA! S/he should know exactly what you're talking
// about -- particularly at such a high level as a service test. Tests have nothing to do with programming. They have everything
// to do with the business, and I hear BAs are pretty good at that stuff.
//
// Then show the BA your code. If s/he can't read it, you've screwed up. Code is not for computers, it's for humans.
// Write every line of code with the next person in mind, not the computer.
//
// Think about that for a minute. You JUST played that story. If your BA, who knows the story inside and out, can't read your code,
// what makes you think the next developer (who knows nothing about the story you just played) will understand your code?
// Seriously. Have you read c2.com yet? All of it? Don't read another blog until you've read all of c2. These days, every
// developer blog you find is just some 30-year-old Ruby-writing doofus telling you what c2 and smalltalk and lisp could have
// told you 10 years ago. Go read it, dammit.
// Okay, now that we have a name for our first extracted test, let's pour the cupcake batter into the tray.
public void testCustomerShouldBeEligibleForMemberAppreciationDiscountIfItOverlapsWithAPassHeHolds() {
// delete this shiznoz:
// Set<MonthEnum> enrolledMonths = new HashSet<MonthEnum>(Arrays.asList(MonthEnum.JAN, MonthEnum.FEB, MonthEnum.DEC));
// final DateRange dateRange = new DateRange(2007, 2008);
// MadYear madYear = MadYear.createForTests(enrolledMonths, dateRange);
// final MemberAppreciationDiscount discount = new MemberAppreciationDiscountBuilder().madYear(madYear).toMemberAppreciationDiscount();
GymPass longTermFamilyPass = FamilyPassFixture.createLongTermFamilyPass("123");
longTermFamilyPass.getPass().setEffectiveBeginDate(new ClockForUT(2007, DateTimeConstants.JULY, 1).date());
longTermFamilyPass.getPass().setEffectiveEndDate(new ClockForUT(2008, DateTimeConstants.JANUARY, 1).date());
Employee employee = new Employee();
employee.setEmployeeStatus("Leave Of Absence");
employee.setAffiliationType(AffiliationTypeFixture.UEM);
Customer customer = new CustomerBuilder().affiliation(employee).toCustomer();
customer.setPrimaryAffiliationType(AffiliationTypeFixture.UEM);
assertFalse(madService.checkEligibility(customer, longTermFamilyPass.getPass()));
}
// Mmm! That was easy! All we had to do was copy everything up to the first assert. And we can delete that MemberAppreciationDiscount
// object, since it's not used in this test. See? Things are getting cleaner already. Just in case you're not sure what I mean
// by "cleaner":
// IMPORTANT TANGENT RANT: STEVE'S STOLEN LAWS (STARTING IN THE MIDDLE)
// POINT 32X, SUBPOINT orange: [ Deleting code == hooray! ]
// You've made a change. What's next? (That was a hint.)
// You know it. Run those tests, boss. If you're not sure whether you should run your tests... run them. If you change
// something, no matter how small, run them. Keep your grass green as much of the time as you can. The society grooming committee
// will be around any day now and you don't want to be caught with a sun-scorched lawn.
// Taking another look at this test (and the corresponding code), it looks like it should be hitting a DAO or something... I'm not sure
// how it's bailing out of the called method so early. I decided to debug it -- guess what I found?
// Well, paint me orange and call me sweet! I had no idea! The code returns false because of this code:
public boolean validEmployeeStatus(String employeeStatus) {
// ... some other crap
employeeStatus.equalsIgnoreCase(LEAVE_OF_ABSENCE)
// more crap
}
// I guess this line in the test was more important than I thought: employee.setEmployeeStatus("Leave Of Absence");
// Lessons learned from this one? Don't just guess at the test. Whether you're cleaning up an older test or working with code under
// a nice clean test, make sure you understand what the test is testing. If it's not testing anything, delete it.
// Obvious examples of "not testing anything" include:
// 1) <expected> is the same object as <actual>
// 2) there are no asserts or mock expectations
// 3) there are no asserts and the developer who wrote the test didn't comment it to declare he only wanted to test a mock's expectations
// 4) the test code looks freakishly similar to the code under test
// ... I'm sure you can think of plenty others.
// Anyway. Our test is now:
public testEmployeeShouldBeIneligibleForMemberAppreciationDiscountIfHeIsOnALeaveOfAbsence()
// or An Employee should be ineligible for Member Appreciation Discount if she is on a leave of absence.
// I think our client would be pleased to know we're testing this acceptance criteria so directly. Her only complaint might be that the
// sentence isn't sexually neutral ('he or she', 's/he', or any other politically correct silliness). But I think we can live with that.
// Okay, now onto the mocks. This is where things get fun.
public void testShouldCheckIfCustomerHasMadForPassEffectiveDates() {
// ...
mockery.checking(new Expectations() {
{
exactly(3).of(transactionServiceMock)
.getPassOverlappingMonths(with(any(Customer.class)), with(any(LocalDate.class)), with(any(LocalDate.class)));
will(returnValue(new ArrayList()));
exactly(6).of(madDaoMock).retrieveByCustomerAndMadYear(with(any(Customer.class)), with(any(DateRange.class)));
will(returnValue(discount));
one(familyPassDaoMock).retrieveFamilyPass(with(any(Customer.class)), with(any(LocalDate.class)), with(any(LocalDate.class)));
will(returnValue(null));
}
});
// gym pass 1
longTermFamilyPass.getPass().setEffectiveBeginDate(new ClockForUT(2007, DateTimeConstants.JULY, 1).date());
longTermFamilyPass.getPass().setEffectiveEndDate(new ClockForUT(2008, DateTimeConstants.JANUARY, 1).date());
assertFalse(madService.checkEligibility(this.customer, longTermFamilyPass.getPass()));
// gym pass 2
longTermFamilyPass.getPass().setEffectiveBeginDate(new ClockForUT(2007, DateTimeConstants.JULY, 1).date());
longTermFamilyPass.getPass().setEffectiveEndDate(new ClockForUT(2007, DateTimeConstants.NOVEMBER, 30).date());
assertTrue(madService.checkEligibility(this.customer, longTermFamilyPass.getPass()));
// gym pass 3
longTermFamilyPass.getPass().setEffectiveBeginDate(new ClockForUT(2008, DateTimeConstants.FEBRUARY, 28).date());
longTermFamilyPass.getPass().setEffectiveEndDate(new ClockForUT(2008, DateTimeConstants.AUGUST, 31).date());
assertFalse(madService.checkEligibility(this.customer, longTermFamilyPass.getPass()));
// gym pass 4
longTermFamilyPass.getPass().setEffectiveBeginDate(new ClockForUT(2008, DateTimeConstants.MARCH, 2).date());
longTermFamilyPass.getPass().setEffectiveEndDate(new ClockForUT(2009, DateTimeConstants.AUGUST, 31).date());
assertFalse(madService.hasMadForPassEffectiveDates(this.customer, longTermFamilyPass.getPass()));
}
// Whoa, whoa. "exactly(6)"? What in the name of backwards-paddling canoes...? If you're setting up mocks for multiple asserts,
// you have a pretty clear indication something is wrong with your test. It's fairly obvious, but I've marked up the offending
// asserts with '// gym pass N'. These dudes need to spend a little less time together. By the time we get around to refactoring
// MaaaaaadServiceImpl, we won't have the first clue what's going on in this test if it breaks.
// You know what to do here. Cut those 4 passes, and their respective asserts, into appropriately-sized tests:
public void testShouldCheckIfCustomerHasMadForPassEffectiveDates4() {
Set<MonthEnum> enrolledMonths = new HashSet<MonthEnum>(Arrays.asList(MonthEnum.JAN, MonthEnum.FEB, MonthEnum.DEC));
final DateRange dateRange = new DateRange(2007, 2008);
MadYear madYear = MadYear.createForTests(enrolledMonths, dateRange);
final MemberAppreciationDiscount discount = new MemberAppreciationDiscountBuilder().madYear(madYear).toMemberAppreciationDiscount();
GymPass longTermFamilyPass = FamilyPassFixture.longTermFamilyPass("123");
longTermFamilyPass.getPass().setEffectiveBeginDate(new ClockForUT(2007, DateTimeConstants.JULY, 1).date());
longTermFamilyPass.getPass().setEffectiveEndDate(new ClockForUT(2008, DateTimeConstants.JANUARY, 1).date());
mockery.checking(new Expectations() {
{
exactly(2).of(madDaoMock).retrieveByCustomerAndMadYear(with(any(Customer.class)), with(any(DateRange.class)));
will(returnValue(discount));
}
});
// gym pass 4
longTermFamilyPass.getPass().setEffectiveBeginDate(new ClockForUT(2008, DateTimeConstants.MARCH, 2).date());
longTermFamilyPass.getPass().setEffectiveEndDate(new ClockForUT(2009, DateTimeConstants.AUGUST, 31).date());
assertFalse(madService.hasMadForPassEffectiveDates(this.customer, longTermFamilyPass.getPass()));
}
// I'm starting at the bottom, because it's the most interesting. This test is covering hasMadForPassEffectiveDates() while
// the others are covering checkEligibility(). Uh... yuck. The next time you see this, slap the person who wrote it. Hard.
// What if you're tempted to add an assert to a test that's already there? Slap yourself. Hard.
// What if the assert you're adding is for the same method, like the other 4 calls?
// Slap yourself. Really hard.
// You ARE allowed to have multiple asserts per test. But DO NOT call the object under test more than once. If you're adding
// more than one assert to a test, ask yourself why. Every time. Chances are there's probably a smaller, simpler test you could write.
// Alright, now on to the less interesting guys:
public void testShouldCheckIfCustomerHasMadForPassEffectiveDates1() {
Set<MonthEnum> enrolledMonths = new HashSet<MonthEnum>(Arrays.asList(MonthEnum.JAN, MonthEnum.FEB, MonthEnum.DEC));
final DateRange dateRange = new DateRange(2007, 2008);
MadYear madYear = MadYear.createForTests(enrolledMonths, dateRange);
final MemberAppreciationDiscount discount = new MemberAppreciationDiscountBuilder().madYear(madYear).toMemberAppreciationDiscount();
GymPass longTermFamilyPass = FamilyPassFixture.createLongTermFamilyPass("123");
longTermFamilyPass.getPass().setEffectiveBeginDate(new ClockForUT(2007, DateTimeConstants.JULY, 1).date());
longTermFamilyPass.getPass().setEffectiveEndDate(new ClockForUT(2008, DateTimeConstants.JANUARY, 1).date());
mockery.checking(new Expectations() {
{
exactly(1).of(transactionServiceMock)
.getPassOverlappingMonths(with(any(Customer.class)), with(any(LocalDate.class)), with(any(LocalDate.class)));
will(returnValue(new ArrayList()));
exactly(1).of(madDaoMock).retrieveByCustomerAndMadYear(with(any(Customer.class)), with(any(DateRange.class)));
will(returnValue(discount));
}
});
// gym pass 1
longTermFamilyPass.getPass().setEffectiveBeginDate(new ClockForUT(2007, DateTimeConstants.JULY, 1).date());
longTermFamilyPass.getPass().setEffectiveEndDate(new ClockForUT(2008, DateTimeConstants.JANUARY, 1).date());
assertFalse(madService.checkEligibility(this.customer, longTermFamilyPass.getPass()));
}
public void testShouldCheckIfCustomerHasMadForPassEffectiveDates2() {
Set<MonthEnum> enrolledMonths = new HashSet<MonthEnum>(Arrays.asList(MonthEnum.JAN, MonthEnum.FEB, MonthEnum.DEC));
final DateRange dateRange = new DateRange(2007, 2008);
MadYear madYear = MadYear.createForTests(enrolledMonths, dateRange);
final MemberAppreciationDiscount discount = new MemberAppreciationDiscountBuilder().madYear(madYear).toMemberAppreciationDiscount();
GymPass longTermFamilyPass = FamilyPassFixture.createFamilyPass("123");
longTermFamilyPass.getPass().setEffectiveBeginDate(new ClockForUT(2007, DateTimeConstants.JULY, 1).date());
longTermFamilyPass.getPass().setEffectiveEndDate(new ClockForUT(2008, DateTimeConstants.JANUARY, 1).date());
mockery.checking(new Expectations() {
{
exactly(1).of(transactionServiceMock)
.getPassOverlappingMonths(with(any(Customer.class)), with(any(LocalDate.class)), with(any(LocalDate.class)));
will(returnValue(new ArrayList()));
exactly(2).of(madDaoMock).retrieveByCustomerAndMadYear(with(any(Customer.class)), with(any(DateRange.class)));
will(returnValue(discount));
one(familyPassDaoMock).retrieveFamilyPass(with(any(Customer.class)), with(any(LocalDate.class)), with(any(LocalDate.class)));
will(returnValue(null));
}
});
// gym pass 2
longTermFamilyPass.getPass().setEffectiveBeginDate(new ClockForUT(2007, DateTimeConstants.JULY, 1).date());
longTermFamilyPass.getPass().setEffectiveEndDate(new ClockForUT(2007, DateTimeConstants.NOVEMBER, 30).date());
assertTrue(madService.checkEligibility(this.customer, longTermFamilyPass.getPass()));
}
public void testShouldCheckIfCustomerHasMadForPassEffectiveDates3() {
Set<MonthEnum> enrolledMonths = new HashSet<MonthEnum>(Arrays.asList(MonthEnum.JAN, MonthEnum.FEB, MonthEnum.DEC));
final DateRange dateRange = new DateRange(2007, 2008);
MadYear madYear = MadYear.createForTests(enrolledMonths, dateRange);
final MemberAppreciationDiscount discount = new MemberAppreciationDiscountBuilder().madYear(madYear).toMemberAppreciationDiscount();
GymPass longTermFamilyPass = FamilyPassFixture.createLongTermFamilyPass("123");
longTermFamilyPass.getPass().setEffectiveBeginDate(new ClockForUT(2007, DateTimeConstants.JULY, 1).date());
longTermFamilyPass.getPass().setEffectiveEndDate(new ClockForUT(2008, DateTimeConstants.JANUARY, 1).date());
mockery.checking(new Expectations() {
{
exactly(1).of(transactionServiceMock)
.getPassOverlappingMonths(with(any(Customer.class)), with(any(LocalDate.class)), with(any(LocalDate.class)));
will(returnValue(new ArrayList()));
exactly(1).of(madDaoMock).retrieveByCustomerAndMadYear(with(any(Customer.class)), with(any(DateRange.class)));
will(returnValue(discount));
}
});
// gym pass 3
longTermFamilyPass.getPass().setEffectiveBeginDate(new ClockForUT(2008, DateTimeConstants.FEBRUARY, 28).date());
longTermFamilyPass.getPass().setEffectiveEndDate(new ClockForUT(2008, DateTimeConstants.AUGUST, 31).date());
assertFalse(madService.checkEligibility(this.customer, longTermFamilyPass.getPass()));
}
// These tests now have:
// - only one assert
// - only one setup
// - only one set of mock expectations (which makes the internals much clearer for the next person who will read the test)
// Before we go back and rename those tests, let's take a minute to think about how we're going to change the service after this.
// Why was this test written like this in the first place? (It certainly did save some space, even if it was completely incomprehensible.)
// Why? Because of the mocks. People hate mocks. I hate mocks. Stubs are better (and these mocks are halfway to a life of stubbery anyway),
// but mocks and stubs are both are lame hacks. In a perfect world, the network and the hard disk would be no slower than your processor or RAM.
// Were this the case, you might never mock anything but a disconnected 3rd party service, because... why bother? Unfortunately, the world
// isn't perfect, and hard disks are slow. If we're going to the database, we have to mock for our own sanity.
// But what else? Mocks also tell us something about our code. For one, as Ashwin mentioned in his JMock tutorial, they tell us when code is
// getting "chatty"... too many mocks mean objects are tightly coupled and talking too much. Objects are, more often than not, talking
// too much because one of them is doing too much work. In this case, it's our service. He's all over the place!
// - validating
// - looping
// - nesting loops! (good lord!)
// - constructing or consuming all sorts of crazy objects -- dates, times, lists, domain objects, gym passes, and customers -- oh my!
// - asking questions of other objects and acting on the answer, rather than being lazy doing as little as he can before passing the buck
// (Like good programmers, good objects are also lazy; lazy programmers + lazy objects is lazy object-oriented zen. Have you read c2 yet?)
// Thankfully, on the surface, he's a good guy. He plays nice in conversation:
public boolean checkEligibility(Customer customer, GymPass familyPass) {
// ...
}
// That's pretty solid. A straightforward question for him to answer: Is this customer eligible for MAD? Yes or no? A rename wouldn't
// hurt. Say 'canCustomerGetSomeHotMemberAppreciationDiscountAction' or perhaps the concise 'isCustomerEligibleForMemberAppreciationDiscount'.
// Neither of these tells us what the pass is for, though -- which is a shame. Maybe Java 7 will have split method invocation or named
// parameters, but don't hold your breath. In the meantime, you should either write a Javadoc for this method or explain what the parameter
// means in your tests. Given that we're talking about a public method, I'd prefer it if there were both. A Javadoc saves me a trip to the
// test when I'm consuming an object elsewhere. At a minimum, rename familyPass to something which tells you what KIND of pass it is.
// By 'kind' I don't mean 'family'. Where should this pass come from? It's a dependency. It has to have some semantic meaning, so
// call it out.
// Why don't we walk the call stack and find out what that pass is for?
public boolean checkEligibility(Customer customer, GymPass familyPass) { }
// leads to:
private boolean notEligibile(Customer customer, GymPass pass) { }
// leads to:
private CustomerPassEligibility.EligibilityType eligibilityTypeFor(Customer customer, GymPass pass) { }
// leads to:
public CustomerPassEligibility eligibilityFor(Customer customer, GymPass pass) { }
// leads to:
public GymPassService.CustomerPassEligibility passEligibilityForCustomer(Customer customer, GymPass pass) { }
// WHOoooAAAAAA. As it turns out, we're talking about GYM PASS eligibility here! Damn. I shouldn't have to walk potential call stacks to
// find that out, should I? Nope. We've got some responsibility in the wrong place and some pretty poor naming going on here, but this
// code sample is ancient and I completely trust no one on this team would rush a story like this again.
// With this knowledge, let's go back to our first test and name it appropriately:
public void testEmployeeShouldBeIneligibleForMemberAppreciationDiscountIfHeIsOnALeaveOfAbsence() { }
// becomes
public void testEmployeeShouldBeIneligibleForAFamilyGymPassIfHeIsOnALeaveOfAbsence() { }
// and the others follow like so:
public void testEmployeeShouldBeEligibleForAFamilyGymPass If It Overlaps The Beginning Of His Member Appreciation Discount Period() { }
public void testEmployeeShouldBeIneligibleForAFamilyGymPass If It Doesnt Overlap His Member Appreciation Discount Period() { }
public void testEmployeeShouldBeEligibleForAFamilyGymPass If It Is Overlapped By His Member Appreciation Discount Period() { }
public void testEmployeeShouldBeEligibleForAFamilyGymPass If It Overlaps The End Of His Member Appreciation Discount Period() { }
// Now these are nicer tests! It's easy to tell they cover boundary conditions around the issue we're concerned about. (they were there
// already, I just gave them names... so kudos to whoever thought of the test scenarios originally). Now they cover some good scenarios,
// they're easy to read, they're about as concise as service tests will get, and they're named something the client would understand.
// Cool! It would be even cooler if they were unit tests around a domain object. But that will take a little more work.
// For the time-being, let's take a break and talk about c2 again.
// I'm shocked to see that Wikipedia's page on c2 doesn't mention C2: http://en.wikipedia.org/wiki/C2
// A bit of history, here. C2 was the world's FIRST wiki. You'd think Wikipedia, of all websites, would pay homage to the first wiki
// ever EVER.
// The first person to edit the Wikipedia page and properly reference c2.com gets beer and cake.
// ...Learn about it here: http://www.aboutus.org/C2.com
// ...now go edit the Wikipedia page.
//
// Done?
//
// Good. Now get back to reading c2.
// Ha ha! Just kidding. Finish this document first, then read all of c2. And even before you get back to this document, send me a text:
// +91 99 23700661 -- tell me you've updated Wikipedia and, if you're the first, you get a SERIOUS prize. Like, a way awesome prize.
// You have no idea how crazy great it will be, but it will be DARN GREAT.
// Okay, what's next? Well, the next step would be to replace the constructors and fixtures with builders. But before you do this,
// ask yourself: What's the purpose of a builder? Why do we prefer it over direct construction or a fixture? Because Steve says
// builders are cool? Or is there something else? Surely there must be a good reason... and perhaps a good reason to treat these tests as
// "good enough" for now, until the code is fixed up. If you can't see why (and to a large extent, I *hope* you have questions
// at this point)... feel free to bring it up with me. Understand why you do the things you do. If you're doing something
// during the workday, you should have a reason you believe in. At the worst, that reason should be this:
// "The team disagreed and we had to choose one of two options for consistency."
// But you should still understand both sides of the argument and form your own opinion, even if you appreciate others'.
// Now that we can get back to the code, let's cut out some domain objects, push this interface back to GymPassService (where it
// belongs) and keep these 5 tests green the entire time. This is left as an exercise for the reader.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment