6

Wasn't sure how to phrase that title, sorry!

I've just come across this in our code base, and was wondering what the consensus was about how to unit test it:

C# (-ish, sorry, this is paraphrased from memory)

class DateCalculator
{
   public static DateTime CalculateLeadTime(DateTime deliveryDate)
   {
      //return the result of some determinate calculation involving deliveryDate
   }
}

//New example
class OrderController
{
    public ActionResult GetLeadTime(WidgetOrder order)
    {
        var leadTime = DateCalculator.CalculateLeadTime(order.GetDeliveryDate() );
        return Json(leadTime.Value.Date.ToString("dd/MM/yyyy") );
    }
}

//Old, incorrect example
class OrderService
{
   public DateTime GetLeadTime(WidgetOrder order)
   {
      return DateCalculator.CalculateLeadTime(order.GetDeliveryDate() );
   }
}

We've then got two unit tests, one to verify that CalculateLeadTime works (this is very easy, you can put in several test values and predict what comes out), and one that GetLeadtime works (still easy, but...). My question is: what would you test here, and where?

It makes sense to me to test CalculateLeadTime in isolation, because it could be a complicated calculation, and it's already a nice small predictable unit. But would you then go on to test some of the same stuff (maybe a single representative input and output?) in your GetLeadTime method? Or would you simply test that any DateTime value is returned, and assume that the calculation is correct because you're testing that somewhere else? Or would you not test the Calculate method on its own, because you know that you're going to test it when you use it, and you don't like the duplication?

EDIT: to muddy the waters a bit, I can see the code now, and rather than a Service, the calling class is an MVC Controller, and the Action under scrutiny is an AJAX method so returns the result of the calculation wrapped in JSON. The current test verifies that JSON is returned, but not the contents of it. Does this change anything?

2 Answers 2

9

You're making a big mistake when you put assumptions into your unit tests about how the unit is implemented.

Consider what would happen were the implementation of OrderService to change so that it no longer used DateCalculator (or, more generally, if you wanted to test a different implementation with the same unit tests). Your unit tests for OrderService would no longer be able to catch most of the potential errors.

While it is true that you can use white-box information to tailor unit tests to code, the unit tests should not be designed to make assumptions about how the code works. So, yes: I would absolutely write tests that may end up running the same code, because code isn't written in stone and I don't want my tests to be concretely coupled to the code it's supposed to be checking.

3
  • That makes sense to me, thanks. So you'd recommend definitely testing fully both the Service and the DateCalculator if they exist, regardless of whether one currently uses the other. If DateCalculator only exists to assist the Service, might it be reasonable on grounds of simplicity to fold it into the Service class (as a private method or whatever, seems a bit like "defactoring"), then only test the Service?
    – frumious
    Commented Jun 15, 2014 at 22:22
  • Just updated the question slightly to match reality - the Service is in fact a Controller returning JSON
    – frumious
    Commented Jun 16, 2014 at 10:55
  • @frumious If DateCalculator only exists to be used by Service, and you cannot anticipate a situation where it might be used by DifferentService (which based on its name I rather doubt), it would be sensible to include it as a private nested class and unit test them in conjunction. Commented Jul 28, 2014 at 19:30
3

Ideally you should change your design a little bit and decouple OrderService from DateCalculator.CalculateLeadTime by using dependency injection:

class OrderService
{
   Func<DateTime,WidgetOrder> _calculateLeadTime;

   public OrderService(Func<DateTime,WidgetOrder> calculateLeadTime)
   {
       _calculateLeadTime=calculateLeadTime;
   }

   public DateTime GetLeadTime(WidgetOrder order)
   {
      return _calculateLeadTime(order.GetDeliveryDate() );
   }
}

Now its easy to unit test OrderService in isolation: instead of DateCalculator.CalculateLeadTime, pass a mock function through the constructor for your test and check if it is correctly called from the GetLeadTime. This changes the situation so the meaningful tests for OrderService are not a superset of the tests for DateCalculator any more.

Of course, you may add an integration test to your test suite, which checks if OrderService.GetLeadTime returns the expected value when it is constructed with the intended DateCalculator.CalculateLeadTime function. But typically for those integration tests you need only a smaller range of test cases as for your unit tests. Since you have tests for the individual behaviour of both units, you already trust their core functionality and you need only make sure they both work together as intended.

In the current simplistic example, it is debatable if the situation does really need all three kinds of tests, but in general (assumed some more complex functions in your real world scenario), this typically pays off.

2
  • 1
    I don't agree. If you consider it a goal that you unit test as many methods, classes, etc. as you can, then your answer implies that you can never have one method or class call another directly without DI. Before you know it, you'll have an interface and factory for every method in the system! Just so you can unit test methods that simply call other methods. Now you run the risk of objects being built wrong. This sort of complexity can add more bugs than it prevents.
    – Brandon
    Commented Jun 15, 2014 at 21:22
  • 1
    @Brandon: surely you can test classes which don't use DI and call other static methods directly - its just that the test becomes harder, or you have to test things twice (like in the example above). That's the point of my answer.
    – Doc Brown
    Commented Jun 16, 2014 at 5:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.