Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dependency Injection & its relationship with automated testing via an example

Through SO, I found my way to this page: http://www.blackwasp.co.uk/DependencyInjection.aspx

There they provide a snippet of C# code to use as an example of code that could benefit from dependency injection:

public class PaymentTerms
{
    PaymentCalculator _calculator = new PaymentCalculator();

    public decimal Price { get; set; }
    public decimal Deposit { get; set; }
    public int Years { get; set; }

    public decimal GetMonthlyPayment()
    {
        return _calculator.GetMonthlyPayment(Price, Deposit, Years);
    }
}


public class PaymentCalculator
{
    public decimal GetMonthlyPayment(decimal Price, decimal Deposit, int Years)
    {
        decimal total = Price * (1 + Years * 0.1M);
        decimal monthly = (total - Deposit) / (Years * 12);
        return Math.Round(monthly, 2, MidpointRounding.AwayFromZero);
    }
}

They also include this quote:

One of the key problems with the above code is the instantiation of the PaymentCalculator object from within the PaymentTerms class. As the dependency is initialised within the containing class, the two classes are tightly coupled. If, in the future, several types of payment calculator are required, it will not be possible to integrate them without modifying the PaymentTerms class. Similarly, if you wish to use a different object during automated testing to isolate testing of the PaymentTerms class, this cannot be introduced.


My question is about the statement in bold:

  • Did the author actually mean Unit Testing or is there something about automated testing that I'm missing?
  • If the author DID intend to write automated testing, how would modifying this class to use dependency injection aid in the process of automated testing?
  • In either case, is this only applicable when there are multiple types of payment calculators?
  • If so, is it typically worth implementing DI right from the start, even with no knowledge of requirements changing in the future? Obviously this requires some discretion that would be learned through experience, so I'm just trying to get a baseline onto which to build.
like image 592
Kiley Naro Avatar asked Jan 19 '23 14:01

Kiley Naro


2 Answers

Did the author actually mean Unit Testing or is there something about automated testing that I'm missing?

I read this to mean unit testing. You can run unit tests by hand or in an automated fashion if you have a continuous integration/build process.

If the author DID intend to write automated testing, how would modifying this class to use dependency injection aid in the process of automated testing?

The modification would help all testing, automated or not.

In either case, is this only applicable when there are multiple types of payment calculators?

It can also come in handy if your injected class is interface-based and you'd like to introduce a proxy without having to change the client code.

If so, is it typically worth implementing DI right from the start, even with no knowledge of requirements changing in the future? Obviously this requires some discretion that would be learned through experience, so I'm just trying to get a baseline onto which to build.

It can help from the start, if you have some understanding of how it works and what it's good for.

There's a benefit even if requirements don't change. Your apps will be layered better and be based on interfaces for non-value objects (immutable objects like Address and Phone that are just data and don't change). Those are both best practices, regardless of whether you use a DI engine or not.

UPDATE: Here's a bit more about the benefits of interface-based design and immutable value objects.

A value object is immutable: Once you create it, you don't change its value. This means it's inherently thread-safe. You can share it anywhere in your app. Examples would be Java's primitive wrappers (e.g. java.lang.Integer, a Money class. etc.)

Let's say you needed a Person for your app. You might make it an immutable value object:

package model; 

public class Person {
    private final String first; 
    private final String last;

    public Person(String first, String last) {
        this.first = first;
        this.last = last;
    }

    // getters, setters, equals, hashCode, and toString follow
}

You'd like to persist Person, so you'll need a data access object (DAO) to perform CRUD operations. Start with an interface, because the implementations could depend on how you choose to persist objects.

package persistence;

public interface PersonDao {
    List<Person> find();
    Person find(Long id);
    Long save(Person p);    
    void update(Person p);
    void delete(Person p);
}

You can ask the DI engine to inject a particular implementation for that interface into any service that needs to persist Person instances.

What if you want transactions? Easy. You can use an aspect to advise your service methods. One way to handle transactions is to use "throws advice" to open the transaction on entering the method and either committing after if it succeeds or rolling it back if it throws an exception. The client code need not know that there's an aspect handling transactions; all it knows about is the DAO interface.

like image 75
duffymo Avatar answered Jan 22 '23 20:01

duffymo


The author of the BlackWasp article means automted Unit Testing - that would have been clear if you'd followed its automated testing link, which leads to a page entitled "Creating Unit Tests" that begins "The third part of the Automated Unit Testing tutorial examines ...".

Unit Testing advocates generally love Dependency Injection because it allows them to see inside the thing they're testing. Thus, if you know that PaymentTerms.GetMonthlyPayment() should call PaymentCalculator.GetMonthlyPayment() to perform the calculation, you can replace the calculator with one of your own construction that allows you to see that it has, indeed, been called. Not because you want to change the calculation of m=((p*(1+y*.1))-d)/(y*12) to 5, but because the application that uses PaymentTerms might someday want to change how the payment is calculated, and so the tester wants to ensure that the calculator is indeed called.

This use of Dependency Injection doesn't make Functional Testing, either automated or manual, any easier or any better, because good functional tests use as much of the actual application as possible. For a functional test, you don't care that the PaymentCalculator is called, you care that the application calculates the correct payment as described by the business requirements. That entails either calculating the payment separately in the test and comparing the result, or supplying known loan terms and checking for the known payment value. Neither of those are aided by Dependency Injection.

There's a completely different discussion to be had about whether Dependency Injection is a Good or Bad Thing from a design and programming perspective. But you didn't ask for that, and I'm not going to lob any hand grenades in this q&a.

You also asked in a comment "This is the heart of what I'm trying to understand. The piece I'm still struggling with is why does it need to be a FakePaymentCalculator? Why not just create an instance of a real, legitimate PaymentCalculator and test with that?", and the answer is really very simple: There is no reason to do so for this example, because the object being faked ("mocked" is the more common term) is extremely lightweight and simple. But imagine that the PaymentCalculator object stored its calculation rules in a database somehow, and that the rules might vary depending on when the calculation was being performed, or on the length of the loan, etc. A unit test would now require standing up a database server, creating its schema, populating its rules, etc. For such a more-realistic example, having a FakePaymentCalculator() might make the difference between a test you run every time you compile the code and a test you run as rarely as possible.

like image 31
Ross Patterson Avatar answered Jan 22 '23 19:01

Ross Patterson