当以“适当”的方式进行单元测试时,即对每个公用电话进行存根并返回预设值或模拟,我觉得我实际上并没有进行任何测试。我从字面上看我的代码,并基于通过公共方法的逻辑流创建示例。
这听起来像您正在测试的方法需要其他几个类实例(您必须对其进行模拟),并自行调用多个方法。
出于您概述的原因,此类代码确实很难进行单元测试。
我发现有帮助的是将这些类分为:
- 具有实际“业务逻辑”的类。它们很少或不使用对其他类的调用,并且易于测试(值输入-值输出)。
- 与外部系统(文件,数据库等)交互的类。这些包装了外部系统,并为您提供了方便的界面。
- 将“所有内容捆绑在一起”的类
然后,来自1.的类很容易进行单元测试,因为它们只接受值并返回结果。在更复杂的情况下,这些类可能需要自己执行调用,但是它们只会从2开始调用类(而不是直接调用例如数据库函数),并且从2开始易于模仿(因为它们仅展示所需包装系统的各个部分)。
通常无法对2和3.中的类进行有意义的单元测试(因为它们自己没有做任何有用的事情,它们只是“胶水”代码)。OTOH,这些类往往相对简单(很少),因此集成测试应充分涵盖它们。
一个例子
一堂课
假设您有一个类,该类从数据库检索价格,应用一些折扣,然后更新数据库。
如果将所有这些都放在一个类中,则需要调用很难模拟的DB函数。用伪代码:
1 select price from database
2 perform price calculation, possibly fetching parameters from database
3 update price in database
所有这三个步骤都需要数据库访问,因此需要进行很多(复杂的)模拟,如果代码或数据库结构发生更改,则很可能会破坏模拟。
分开
您分为三类:PriceCalculation,PriceRepository,App。
PriceCalculation仅进行实际计算,并提供所需的值。应用将所有内容捆绑在一起:
App:
fetch price data from PriceRepository
call PriceCalculation with input values
call PriceRepository to update prices
那样:
- PriceCalculation封装了“业务逻辑”。测试很容易,因为它不会自己调用任何东西。
- 通过建立模拟数据库并测试读取和更新调用,可以对PriceRepository进行伪单元测试。它几乎没有逻辑,因此代码路径很少,因此您不需要太多的测试。
- 无法对应用进行有意义的单元测试,因为它是粘合代码。但是,它也非常简单,因此集成测试就足够了。如果以后的应用程序变得太复杂,则需要扩展更多的“业务逻辑”类。
最后,事实证明PriceCalculation必须执行自己的数据库调用。例如,因为只有PriceCalculation知道其需要哪些数据,所以App无法提前获取它。然后,您可以将其传递给PriceRepository(或其他一些存储库类)的实例,并根据PriceCalculation的需求进行定制。然后需要模拟该类,但这将很简单,因为PriceRepository的接口很简单,例如PriceRepository.getPrice(articleNo, contractType)
。最重要的是,PriceRepository的接口将PriceCalculation与数据库隔离,因此对数据库模式或数据组织的更改不太可能更改其接口,从而破坏了模拟。