单元测试使用DI的类而无需内部测试


12

我有一个在1个主要班级和2个较小的班级中重构的班级。主要类使用数据库(就像我的许多类一样)并发送电子邮件。因此,主要类别有一个IPersonRepository和一个IEmailRepository注入,后者又将其发送给2个较小的类别。

现在,我想对主类进行单元测试,并且学会了不对类的内部工作进行单元测试,因为我们应该能够在不破坏单元测试的情况下更改内部工作。

但作为类使用IPersonRepositoryIEmailRepository,我HAVE指定一些方法为(模拟/虚拟)的结果IPersonRepository。主类基于现有数据计算一些数据并将其返回。如果要测试,我不知道如何在不指定IPersonRepository.GetSavingsByCustomerId返回值x的情况下编写测试。但是,然后我的单元测试“知道”内部工作原理,因为它“知道”了哪些方法可以模拟,哪些方法不可以。

如何在不了解内部原理的情况下测试已注入依赖项的类?

背景:

以我的经验,像这样的许多测试会为存储库创建模拟,然后为模拟提供正确的数据,或者测试执行期间是否调用了特定方法。无论哪种方式,测试都会了解内部信息。

现在,我已经看到了有关理论的演示文稿(我以前已经听说过),即测试不应该知道实现的知识。首先,因为你不是测试如何它的工作原理,而且还因为当你改变现在所有的单元测试失败,因为他们“知道”关于落实执行。尽管我喜欢测试的概念不了解实现的概念,但我不知道如何实现。


1
一旦您的主类期望一个IPersonRepository对象,该接口及其描述的所有方法就不再是“内部”的了,因此这实际上不是测试的问题。您真正的问题应该是“如何将类重构为较小的单元,而又不会在公共场合公开太多”。答案是“保持那些接口精简”(例如,通过坚持接口隔离原则)。这就是@DavidArno答案中的恕我直言第二点(我想我不需要在另一个答案中重复这一点)。
布朗

Answers:


7

主类基于现有数据计算一些数据并将其返回。如果要测试,我不知道如何在不指定IPersonRepository.GetSavingsByCustomerId返回值x的情况下编写测试。但是,然后我的单元测试“知道”内部工作原理,因为它“知道”了哪些方法可以模拟,哪些方法不可以。

没错,这违反了“不测试内部原理”的原则,是人们忽略的一种常见现象。

如何在不了解内部原理的情况下测试已注入依赖项的类?

您可以采用两种解决方案来解决此冲突:

1)提供的完整模拟IPersonRepository。您当前描述的方法是通过仅模拟将要调用的方法,将模拟与被测方法的内部工作方式耦合。如果您为的所有方法提供了模拟IPersonRepository,那么您将删除该耦合。内部工作方式可以更改而不会影响模拟,从而使测试不那么脆弱。

这种方法的优点是使DI机制保持简单,但是如果您的接口定义了许多方法,它可能会产生很多工作。

2)不要注入IPersonRepository,注入GetSavingsByCustomerId方法或注入节省值。注入整个接口实现的问题是,然后注入(“告诉,不问”)“问,不告诉”系统,将两种方法混为一谈。如果您采用“纯DI”方法,则应向该方法提供(告诉)该方法要调用的确切方法(如果需要节省值),而不是被提供一个对象(然后必须有效地要求该方法调用)。

这种方法的优点是它避免了对模拟的需求(除了注入被测方法之外的测试方法)。缺点是它可能导致方法签名响应方法更改的要求而更改。

两种方法都有其优点和缺点,因此请选择最适合您需求的方法。


1
我真的很喜欢第2个主意。我不知道为什么以前没有想到过。我已经在IRepository上创建依赖项已有很多年了,而没有问自己为什么要对整个IRepository创建一个依赖项(在很多情况下会包含很多方法签名),而我却只依赖于一个或多个。 2种方法。因此,与其注入IRepository,不如更好地注入或传递所需的(仅)方法签名。
米歇尔

1

我的方法是创建存储库的“模拟”版本,该版本可从包含所需数据的简单文件中读取。

这意味着单个测试不会“知道”模拟程序的设置,尽管显然整个测试项目将引用模拟程序并具有设置文件等。

这避免了模拟框架所需的复杂模拟对象设置,并允许您在应用程序的实际实例中将“模拟”对象用于UI测试等。

由于已完全实现“模拟”,而不是为测试场景专门设置了“模拟”,因此更改实现,例如,假设GetSavingsForCustomer现在也应该删除客户。不会破坏测试(除非它确实会破坏测试),您只需要更新单个模拟实现,并且所有测试都将针对该模拟运行,而无需更改其设置


2
这仍然导致我为测试的类正在使用的方法精确地创建带有数据的模拟。所以我仍然有一个问题,当更改实现时,测试会失败。
米歇尔(Michel)

1
编辑以解释为什么我的方法更好,尽管对于我认为您所指的情况仍然不够完美
Ewan

1

单元测试通常是白盒测试(您可以访问真实代码)。因此,一定程度地了解内部知识是可以的,但是对于初学者而言,不熟悉它是容易的,因为您不应该测试内部行为(例如“先调用方法,然后调用b,然后再调用一次”)。

注入提供数据的模拟程序是可以的,因为您的类(单元)取决于该外部数据(或数据提供者)。但是,您应该验证结果(而不是获得结果的方法)!例如,您提供一个Person实例并验证电子邮件是否已发送到正确的电子邮件地址(例如,通过提供电子邮件的模拟文件),该模拟文件只存储收件人的电子邮件地址,以供以后测试使用-码。(不过,我认为Martin Fowler称其为Stubs而非Mocks)

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.