02 November 2006

Mock Object Clinic (I)

Dylan Smith has posted a comparison of testing with and without mocks. He makes some good points, but his example is flawed. In some kind of pseudocode, his test is like this:
  test calculate pay
   mockEmployee := mock(IEmployee)
   employee := Employee( mockEmployee )
   startDate := Date(1, July, 2006)
   endDate := Date(31, July, 2006)

   expect once ( mockEmployee.labour(startDate, endDate) ) will returnValue( 40 )
   expect once ( mockEmployee.payRate() ) will returnValue( 48 )
   expect once ( mockEmployee.premium() ) will returnValue( 2)

   assertEqual( 2000, employee.calculatePay(startDate, endDate) )
with an implementation like this:
Employee( target ) implements IEmployee
 this.target := target

Employee.calculatePay( startDate, endDate )
 labour := target.labour(startDate, endDate)
 rate := target.payRate()
 premium := target.premium()

 return labour * (rate + premium)
Incidentally, both the real and mock Employees implement the IEmployee interface. The problems I see with this example are:
  • the Employee class isn't complicated enough to mock. It's basically a struct with a helper method. We don't usually mock simple classes that have no interactions with third parties
  • the record/playback style of mocking is too brittle. It's OK for the simple cases and to learn the approach, but we want to think in terms of expressing constraints between objects, not just simple matching.
  • everything's a getter. Mocks should push you towards a "Tell, Don't Ask" style of coding, where behaviour is passed around rather than data.
  • I find the double implementation of IEmployee confusing. The test doesn't really help to flush out the relationship between an Employee object and its neighbours
Off the top of my head, I think an alternative (contrived) example would be something like:
  test reports pay to payroll
    timesheet := Timesheet()
      .set( Week(3), Hours(40) )
    employee := Employee( HourlyRate(50), timesheet )

    payroll := mock(Payroll)
    expect once (payroll.addEmployeeForWeek( employee, Week(3), Rate(2000) )

    employee.reportPayForWeekTo( Week(3), payroll )

Employee( hourlyRate, timesheet )
  this.hourlyRate := hourlyRate
  this.timesheet = timesheet

Employee.reportPayForWeekTo( week, payroll )
  payroll.addEmployeeForWeek(this, week,
                             hourlyRate.valueOf( timesheet.hoursForWeek(week) ))
Note that now the expectation doesn't have to return anything because we're sending a message to the payroll object. Also, more of the objects have behaviour on them and the employee has a more meaningful relationship with its neighbour than before.

No comments: