依赖注入&通过一个例子,它与自动化测试的关系
本文关键字:关系 自动化测试 一个 注入 依赖 | 更新日期: 2023-09-27 18:10:24
通过SO,我找到了这个页面:http://www.blackwasp.co.uk/DependencyInjection.aspx
他们提供了一段c#代码,作为可以从依赖注入中获益的代码示例:
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);
}
}
还包括以下引用:
的实例化是上述代码的关键问题之一从PaymentTerms类中获取PaymentCalculator对象。作为依赖关系是在包含类中初始化的类是紧密耦合的。如果,在未来,几种类型支付计算器是必需的,它将不可能集成它们不需要修改PaymentTerms类。类似的,如果你愿意的话在自动化测试期间使用不同的对象来隔离测试
我的问题是关于黑体的语句:
- 作者实际上是指单元测试还是我遗漏了一些关于自动化测试的内容?
- 如果作者确实打算编写自动化测试,如何修改该类以在自动化测试过程中使用依赖注入帮助?
- 无论哪种情况,这只适用于有多种类型的支付计算器吗?
- 如果是这样,通常值得从一开始就实现DI,即使不知道未来的需求会发生变化吗?显然,这需要一些自由裁量权,可以通过经验来学习,所以我只是想找到一个基线来构建。
作者实际上是指单元测试吗我错过的自动化测试?
我认为这意味着单元测试。如果您有一个持续的集成/构建过程,您可以手工运行单元测试,或者以自动化的方式运行单元测试。
如果作者确实打算编写自动化测试,如何编写的过程中修改这个类来使用依赖注入自动化测试?
修改将有助于所有的测试,无论是否自动化。
在这两种情况下,这只适用于有多个类型付款计算器?
如果你注入的类是基于接口的,并且你想在不改变客户端代码的情况下引入代理,它也可以派上用场。
如果是这样,通常值得从一开始就实现DI吗?即使不知道未来需求的变化?显然,这需要一些谨慎,这是通过经验,所以我只是试图得到一个基线,在此基础上建立。
如果你对它的工作原理和它的好处有一些了解,它从一开始就会有所帮助。
即使需求不变,也有好处。你的应用程序将分层更好,并基于非值对象的接口(不可变对象,如地址和电话,只是数据,不会改变)。无论您是否使用DI引擎,这些都是最佳实践。
更新:这里有更多关于基于接口的设计和不可变值对象的好处。
值对象是不可变的:一旦你创建了它,你就不能改变它的值。这意味着它本身就是线程安全的。你可以在应用程序的任何地方共享它。例如Java的原始包装器(例如Java .lang。整数,一个Money类。等)
假设你的应用需要一个Person,你可以把它设为一个不可变的值对象:
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
}
您希望持久化Person,因此需要一个数据访问对象(DAO)来执行CRUD操作。从接口开始,因为实现可能取决于您选择如何持久化对象。
package persistence;
public interface PersonDao {
List<Person> find();
Person find(Long id);
Long save(Person p);
void update(Person p);
void delete(Person p);
}
你可以要求DI引擎将该接口的特定实现注入到任何需要持久化Person实例的服务中。
如果你想要事务呢?一件容易的事。您可以使用方面来建议您的服务方法。处理事务的一种方法是使用"抛出建议"在进入方法时打开事务,如果成功则提交,如果抛出异常则回滚。客户端代码不需要知道有一个方面在处理事务;它只知道DAO接口。
BlackWasp文章的作者指的是自动化单元测试——如果您遵循它的automated testing
链接,这将会很清楚,该链接指向一个名为"创建单元测试"的页面,该页面以"自动化单元测试教程的第三部分检查…"开头。
单元测试的支持者通常喜欢依赖注入,因为它允许他们看到他们正在测试的东西的内部。因此,如果您知道PaymentTerms.GetMonthlyPayment()
应该调用PaymentCalculator.GetMonthlyPayment()
来执行计算,那么您可以将计算器替换为您自己的结构之一,这样您就可以看到它确实被调用了。不是因为您想要将m=((p*(1+y*.1))-d)/(y*12)
的计算更改为5
,而是因为使用PaymentTerms
的应用程序可能有一天想要更改付款的计算方式,因此测试人员想要确保确实调用了计算器。
依赖注入的使用不会使功能测试(无论是自动的还是手动的)变得更容易或更好,因为好的功能测试使用了尽可能多的实际应用程序。对于功能测试,您不关心是否调用了PaymentCalculator,您只关心应用程序是否按照业务需求的描述计算出正确的支付。这需要在测试中分别计算付款并比较结果,或者提供已知的贷款条款并检查已知的付款值。这两种方法都不需要依赖注入。
从设计和编程的角度来看,依赖注入是好是坏,这是一个完全不同的讨论。但这不是你要求的,我也不会在这个问题上投掷任何手榴弹。
你还在评论中问道"这是我想要理解的核心。我仍然在努力的是为什么它需要一个FakePaymentCalculator?为什么不创建一个真实的、合法的PaymentCalculator实例并使用它进行测试呢?",答案非常简单:对于本例,没有理由这样做,因为被伪造的对象(更常用的术语是"mock ")非常轻量级和简单。但是想象一下,PaymentCalculator对象以某种方式将其计算规则存储在数据库中,并且规则可能根据执行计算的时间或贷款的长度等而变化。单元测试现在需要建立一个数据库服务器,创建它的模式,填充它的规则,等等。对于这样一个更现实的示例,拥有FakePaymentCalculator()
可能会在每次编译代码时运行的测试和尽可能少运行的测试之间产生差异。
最大的好处之一是能够在测试期间用模拟/假实现替代PaymentCalculator。如果作者确实打算编写自动化测试,如何修改该类以在自动化测试过程中使用依赖注入帮助?
如果PaymentTerms像这样实现:
public class PaymentTerms
{
IPaymentCalculator _calculator;
public PaymentTerms(IPaymentCalculator calculator)
{
this._calculator = calculator;
}
...
}
(其中IPaymentCalculator是声明PaymentCalculator类的服务的接口。)这样,在单元测试中,您可以这样做:
IPaymentCalculator fakeCalculator = new FakePaymentCalculator()
PaymentTerms paymentTerms = new PaymentTerms(fakeCalculator);
// Test the behaviour of PaymentTerms, which uses a fake in the test.
将PaymentCalculator类型硬编码到PaymentTerms中,就没有办法这样做了。
更新:你在评论中问:
假设地说,如果PaymentCalculator类有一些实例属性,开发单元测试的人可能会创建带有构造函数的FakePaymentCalculator类,该构造函数总是为实例属性使用相同的值,对吗?那么排列是如何测试的呢?或者是PaymentTerms的单元测试填充FakePaymentCalculator的属性并测试几种排列的想法?
我认为你不需要测试任何排列。在这个特定的情况下,PaymentTerms.GetMonthlyPaymend()
的唯一任务是用指定的参数调用_calculator.GetMonthlyPayment()
。这是你唯一需要进行单元测试的东西,当你为那个方法编写单元测试时。例如,您可以这样做:
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;
}
}
在单元测试中,你可以这样做:
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);
这样我们测试的唯一的事情,这是PaymentTerms.GetMonthlyPayment()
的责任,那就是调用计算器的GetMonthlyPayment()
方法。但是,对于这类测试,使用mock要比实现自己的mock简单得多。如果你感兴趣,我建议你试一试Moq,这是一个非常简单,但很有用的。net模拟库