Answer To ‘Why Should We Avoid Fakes When Unit Testing?’

One could easily get the wrong idea from the title question. I don’t mean fakes, as in all fake collaborators. Most types of fake collaborators are useful and help us during unit testing. However, mimicking fakes, imitating the behaviour of real collaborating classes is another matter.

At the end of yesterday’s post, I asked the question in the title. Today I’ll answer it.

I’ll explain in terms of yesterday’s SalesTaxCalculator example. If you want to refresh your mind, here is the link to yesterday’s article.

FakeTaxSelector example

Unit tests for the SalesTaxCalculator functioned with either a StubTaxSelector or a FakeTaxSelector.

But why does SalesTaxCalculator need ISalesTaxSelector?

When we examine the listing for SalesTaxCalculator below, we see the taxSelector reference gets the tax rate for a given country code:

  public class SalesTaxCalculator
  {
     public decimal TaxRate { get; }

     public SalesTaxCalculator(string countryCode, ISalesTaxSelector taxSelector)
     {
        TaxRate = taxSelector.Select(countryCode);
     }

     public decimal CalcSalesTax(SalesInvoice invoice)
     {
        return invoice.Total * TaxRate;
     }
  }

When we are unit testing the behaviour of SalesTaxCalculator, are we interested in

  1. that taxSelector retrieves a tax rate for a country code, or
  2. that taxSelector brings us a tax rate?

For the sake of unit testing, we only care that we get a tax rate from the call to taxSelector.Select(). The behaviour of SalesTaxCalculator does not mind how taxSelector gets the tax rate—only that it does get a tax rate. 

 

This is where the unit test using the StubTaxSelector is clear—it is seeded with a tax rate—here 0.20m or 20%—and it faithfully returns this value in a call to taxSelector.Select():

  [Fact]
  public void Test_SalesTaxCalculator_CalcSalesTax_Using_StubTaxSelector()
  {
     var stubTaxSelector = new StubTaxSelector(0.20m);
     var taxCalculator = new SalesTaxCalculator("UK", stubTaxSelector);
     var salesInvoice = new SalesInvoice { Total = 123.45m };

     var tax = taxCalculator.CalcSalesTax(salesInvoice);

     tax.Should().Be(24.69m);
  }

Notice how all the factors in the verification of the unit test are on display in the unit test. 

However, the unit test using FakeTaxSelector is less transparent. Since it mimics a real implementation of ISalesTaxSelector, it internally ties a 20% sales tax rate to the “UK” countryCode. At a glance, it’s not clear why the verified result of the taxCalculator.CalcSalesTax() should be 24.69:

  [Fact]
  public void Test_SalesTaxCalculator_CalcSalesTax_Using_FakeTaxSelector()
  {
     var fakeTaxSelector = new FakeTaxSelector();
     var taxCalculator = new SalesTaxCalculator("UK", fakeTaxSelector);
     var salesInvoice = new SalesInvoice { Total = 123.45m };

     var tax = taxCalculator.CalcSalesTax(salesInvoice);

     tax.Should().Be(24.69m);
  }

It’s like this second unit test is testing the behaviour of both the SalesTaxCalculator and the FakeTaxSelector! Yet, we are not interested in how FakeTaxSelector works—it’s not a class that will ever be deployed and run!

I recommend avoiding fakes imitating the behaviour of real collaborators. Instead, you can stub any desired return values.

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply