Skip to the content.

Easy Fix, Difficult Test

A pipe fixed with wrenches

Image Source

Here is the sample app

What does it do?

We own a company that sells Books on-line in various cities around the world. To manage our purchases, we have developed an internal system that has two portals:

1. Customer Portal

This portal, provides our customers with features allowing them to search for and purchase books.

The purchase workflow is as follows:

  1. Customers add the books they want to purchase to a basket.
  2. They then checkout their basket.
  3. Upon checkout, our system should generate an invoice. The invoice should have the following properties:
    1. Tax rates and tax reduction rules should be applied for each item in the basket
    2. The total amount of the invoice should be the sum of the amount of all books (after tax) in the basket
    3. The currency of the invoice is the same currency as the respective country
  4. The Invoice is sent to the customer, and a copy of it is saved in our repository for future reference

Tip: It is important to note that each country has its own tax rates and tax reduction rules. You can find a table of those rules below.

2. Reporting Portal

The second portal is used by administrators to generate reports of the sales around the world.

The report should include the following:

  1. Accumulative sum of all the issued invoices’ amount
  2. The count of processed invoices
  3. The currency of the report should be in USD

Countries, Currencies, Language, Tax Rates, and Tax Reduction Rules

Country Currency Language Exchange Rate to USD Tax Rate Tax Reduction Rules
USA USD English 1.0 15% Reduction by 2% on Novels
France Euro French 1.14 25% No Reduction on taxes
UK Pound Sterling English 1.27 20% Reduction by 7% on Novels
Spain Euro Spanish 1.14 10% Removed taxes on all foreign language books
China Renminbi Mandarin 0.15 35% Removed taxes on all foreign language books
Japan YEN Japanese 0.0093 30% No Reduction on taxes
Australia Australian Dollar English 0.70 13% No Reduction on taxes
Germany Euro German 1.14 22% Dropped to 5% on books written by German Authors

Repository

The repository is our database where we store copies of all the issued invoices. The repository is defined by an interface that has 2 methods:

  1. addInvoice: adds an invoice to the repository’s database
  2. getInvoiceMap: return all the available invoices in a Map

Having this interface enables us to have different implementations for our database (Json, InMemory, Relational, NoSql, etc).

In this workshop, we wrote a JSON implementation for this Repository interface (JsonRepository.java). We assumed that we are storing our data in JSON format in a file under the resources folder.

Tip: Reading the repository.json file might help you understand the structure of the code faster.

On initialization, the class parses the Json file and loads the data into a Map.

The MainRepository singleton returns the currently configured Repository.

We found a bug!

We noticed that some of the numbers generated by the report were wrong.

Report Actual Expected
The total number of books sold 16 16
The total number of issued invoices 6 6
The total amount of all invoices in USD 1016.04 424.57

Fortunately, our great team mates gave us some clues as to the origin of the problem:

It looks like conversion rates and tax reductions were not applied. [The domain expert]

It’s weird because there is code for these in TaxRule and CurrencyConverter! [A senior developer]

We fixed it!

After some analysis, one of our developers was able to quickly identify the bugs in the code and provided us with quick fixes!

If you run the App now, you should see that The total amount of all invoices in USD is looking good: it’s 424.57.

Java

Sneak Peek at Bug Fix in Invoice.java
  public double computeTotalAmount() {
    double sum = 0.0;
    for (PurchasedBook purchasedBook : purchasedBooks) {
  -   double totalPrice = purchasedBook.getTotalPrice();
  +   double totalPrice = purchasedBook.getTotalPrice() * TaxRule.getApplicableRate(country, purchasedBook.getBook());
      sum += totalPrice;
    }
    return sum;
  }
Sneak Peek at Bug Fix in ReportGenerator.java
      public double getTotalAmount() {
          Map<Integer, Invoice> invoiceMap = repository.getInvoiceMap();
          double totalAmount = 0.0;
          for (Invoice invoice : invoiceMap.values()) {       
  -            totalAmount += invoice.computeTotalAmount();
  +            totalAmount += CurrencyConverter.toUSD(invoice.computeTotalAmount(), invoice.getCountry().getCurrency());
          }
          return getRoundedAmount(totalAmount);
      }

C++

Sneak Peek at Bug Fix in Invoice.cpp
          double sum = 0.0;
          for (const auto purchasedBook : purchasedBooks_)
          {
  -               double totalPrice = purchasedBook->getTotalPrice();
  +               double totalPrice = purchasedBook->getTotalPrice() * finance::getApplicableRate(country_, *purchasedBook->getBook());
                  sum += totalPrice;
          }
          return sum;
Sneak Peek at Bug Fix in ReportGenerator.cpp
          double totalAmount = 0.0;
          for (const auto id2Invoice : invoiceMap)
          {
  -               totalAmount += id2Invoice.second->computeTotalAmount();
  +               const auto& invoice = *id2Invoice.second;
  +               totalAmount += finance::toUSD(invoice.computeTotalAmount(), invoice.getCountry().getCurrency());
          }
          return getRoundedValueOf(totalAmount);

C#

Sneak Peek at Bug Fix in Invoice.cs

```diff c# public double ComputeTotalAmount() { var totalAmount = 0.0;

Sneak Peek at Bug Fix in ReportGenerator.cs

```diff c# public double GetTotalAmount() { var invoices = _repository.GetInvoiceMap().Values;

Kotlin

Sneak Peek at Bug Fix in Invoice.kt
      fun computeTotalAmount(): Double {
          var sum = 0.0
          for (purchasedBook in purchasedBooks) {
  -          val totalPrice: Double = purchasedBook.getTotalPrice()
  +          val totalPrice: Double = purchasedBook.getTotalPrice() * getApplicableRate(country, purchasedBook.book)
            sum += totalPrice
          }
          return sum
      }
Sneak Peek at Bug Fix in ReportGenerator.kt
      fun getTotalAmount(): Double {
        val invoiceMap = repository.getInvoiceMap()
        var totalAmount = 0.0
        for (invoice in invoiceMap.values) {
  -            totalAmount += invoice.computeTotalAmount()
  +            totalAmount += CurrencyOperations.toUSD(invoice.computeTotalAmount(), invoice.country.currency)
          }
          return getRoundedAmount(totalAmount);
      }

Scala

Sneak Peek at Bug Fix in Invoice.scala
    def computeTotalAmount: Double = {
      var sum = 0.0
  
      for (purchasedBook <- purchasedBooks) {
  -     val totalPrice: Double = purchasedBook.getTotalPrice()
  +     val totalPrice: Double = purchasedBook.getTotalPrice * getApplicableRate(country, purchasedBook.book)
        sum += totalPrice
      }
      sum
    }
Sneak Peek at Bug Fix in ReportGenerator.scala
  def getTotalAmount: Double = {
    val invoiceMap = repository.getInvoiceMap
    var totalAmount = 0.0
    for (invoice <- invoiceMap.values) {
  -     totalAmount += invoice.computeTotalAmount();
  +     totalAmount += toUSD(invoice.computeTotalAmount, invoice.country.currency)
    }
    getRoundedAmount(totalAmount)
  }

Now we need Unit Tests

1. On the Invoice

Your mission is to add a test for the Invoice class (java | c++ | c# | kotlin | scala) to cover the fix we added.
Instructions are provided in InvoiceTest (java | c++ | c# | kotlin | scala).

Make sure your test is correct: it must fail when you reintroduce the bug in Invoice.

Mocking a legacy code base is not a great idea. The only fake we are allowed is the InMemoryRepository class (java | c++ | c# | kotlin | scala)

2. [BONUS] On the ReportGenerator

If you have the time, do the same for ReportGenerator (java | c++ | c# | kotlin | scala).
You’ll find instructions in ReportGeneratorTest (java | c++| c# | kotlin | scala).

Mini Retro

Take a few minutes to discuss the good and the bad of this approach.

Then compare them to what people usually say in the Retrospectives Guide


Continue: