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.
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.
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.
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?
One of the biggest benefits would be to be able to substitute the PaymentCalculator with a mock/fake implementation during the test.
If PaymentTerms was implemented like this:
public class PaymentTerms
{
IPaymentCalculator _calculator;
public PaymentTerms(IPaymentCalculator calculator)
{
this._calculator = calculator;
}
...
}
(Where IPaymentCalculator is the interface declaring the services of the PaymentCalculator class.) This way, in a unit test, you would be able to do this:
IPaymentCalculator fakeCalculator = new FakePaymentCalculator()
PaymentTerms paymentTerms = new PaymentTerms(fakeCalculator);
// Test the behaviour of PaymentTerms, which uses a fake in the test.
With the PaymentCalculator type hardcoded into PaymentTerms, there would be no way to do this.
UPDATE: You asked in comment:
Hypothetically speaking, if the PaymentCalculator class had some instance properties, the person developing the unit test would probably create the FakePaymentCalculator class with a constructor that always used the same values for the instance properties, right? So how then are permutations tested? Or is the idea that the unit test for PaymentTerms populates the properties for FakePaymentCalculator and tests several permutations?
I don't think you have to test any permutations. In this specific case, the only task of the PaymentTerms.GetMonthlyPaymend()
is to call _calculator.GetMonthlyPayment()
with the specified parameters. And that is the only thing you need to unit test, when you write the unit test for that method.
For example, you could do the following:
public class FakePaymentCalculator
{
public decimal Price { get; set; }
public decimal Deposit { get; set; }
public int Years { get; set; }
public void GetMonthlyPayment(decimal price, decimal deposit, int years)
{
this.Price = price;
this.Deposit = deposit;
this.Years = years;
}
}
And in the unit test, you could do this:
IPaymentCalculator fakeCalculator = new FakePaymentCalculator()
PaymentTerms paymentTerms = new PaymentTerms(fakeCalculator);
// Calling the method which we are testing.
paymentTerms.GetMonthlyPayment(1, 2, 3);
// Check if the appropriate method of the calculator has been called with the correct parameters.
Assert.AreEqual(1, fakeCalculator.Price);
Assert.AreEqual(2, fakeCalculator.Deposit);
Assert.AreEqual(3, fakeCalculator.Years);
This way we tested the only thing, which is the responsibility of the PaymentTerms.GetMonthlyPayment()
, that is calling the GetMonthlyPayment()
method of the calculator.
However, for this kind of tests, using a mock would be much more simpler than implementing an own fake. If you're interested, I recommend you to try out
Moq, which is a really simple, yet useful Mock library for .NET.
精彩评论