Injecting Testability into Your Test Designs

Better Software Magazine
Volume-Issue: 
2005-04
Summary:

The term dependency injection has attained buzzword status within the programming community. Follow Agile developer J.B. Rainsberger as he goes beyond the buzz and breaks down a specific example of how injecting a dependency can improve the testability of your design.

The term dependency injection has attained buzzword status within the programming community. As with any idea that gains so much visibility so quickly, it has its detractors—mainly those who fear the idea will become another golden hammer. Their fears are well founded, as we saw with the book Design Patterns. Shortly after it appeared, programmers began writing systems riddled with global data, claiming to be improving the design with the Singleton pattern. These programmers likely skimmed the pattern description, failed to read about the drawbacks, and then went merrily on their way. One way to avoid repeating this pattern is to see a variety of both good and bad examples of how to apply a design idea. I would like to contribute to this cause by presenting an example of how to improve the testability of a design by injecting a dependency.

Suppose we are building a website to rival PayPal.com by providing a better electronic funds transfer service. Naturally, our site is transferbuddy.biz, taking full advantage of one of the cool new top-level domains. Before we can hope to compete with the big players in this market space, we need to implement a simple transfer of funds between members of our site. I start by test-driving a basic transfer feature, resulting in these tests:

  • The Happy Path: The sender has enough funds to cover a transfer to the receiver.
  • The sender transfers his entire balance to the receiver, which we allow.
  • The sender has insufficient funds to cover the transfer, so it fails.
  • The sender attempts to transfer $0, which we disallow.
  • The sender attempts to transfer funds to himself, which we disallow.

Each of the tests follows a similar rhythm: create a sender and receiver, execute the transfer, check the receipt, then verify the sender’s and receiver’s balances. (Amounts are in Canadian dollars only, for now. We’ll tackle multiple currencies later.) The receipt indicates either a successful transfer or a denial of the request, including the specific reason. After checking the receipt, the test verifies that the money has (or has not) changed hands. You can find a few of these tests in Listing 1.

As nice as it is to have completed our first feature, transferbuddy.biz is in business to turn a profit, so the next important feature is obvious: We need to collect a fee for each funds transfer. Our crack financial analysis department has identified a fairly simple scheme: Receiving funds is free, but sending funds costs $2 plus 2.9 percent of the value of the transaction. Returning to our Happy Path test, the fee for the successful transfer comes to $4.90 on the $100 transfer. I test-drive this feature, adding a method named calculateFee() to the class FundsTransfer . I write three tests, calculating the fee on a $0 transfer, a $1,000 transfer, and amounts such as $157.41 and $157.42 that force me to round down and up, respectively. As I write these tests, I feel a familiar twinge that usually indicates the onset of a design problem. It has to do with the choice of putting the method calculateFee() on the class FundsTransfer.

What triggers my vague uneasiness is being forced to deal with irrelevant details when writing these tests. Following is an example:

The Test Example merely wants to check the fee amount, but that requires creating a FundsTransfer object. To do this, I have to supply the sending party, the receiving party, and the amount of the transfer, even though the sender and receiver have no bearing on how the fee is calculated. These irrelevant objects make the test unnecessarily

Upcoming Events