我们的同事提倡编写单元测试,因为它实际上是在帮助我们改进设计和重构事物,但我不知道如何做。如果我正在加载CSV文件并进行解析,那么单元测试(验证字段中的值)将如何帮助我验证设计?他提到了耦合和模块化等问题,但是对我而言,这没有多大意义-但我的理论背景并不多。
这与您标记为重复的问题不同,我将对实际示例如何起到帮助作用感兴趣,而不仅仅是理论上说“有帮助”。我喜欢下面的答案和评论,但我想了解更多。
我们的同事提倡编写单元测试,因为它实际上是在帮助我们改进设计和重构事物,但我不知道如何做。如果我正在加载CSV文件并进行解析,那么单元测试(验证字段中的值)将如何帮助我验证设计?他提到了耦合和模块化等问题,但是对我而言,这没有多大意义-但我的理论背景并不多。
这与您标记为重复的问题不同,我将对实际示例如何起到帮助作用感兴趣,而不仅仅是理论上说“有帮助”。我喜欢下面的答案和评论,但我想了解更多。
Answers:
单元测试不仅有助于设计,而且这是其主要优点之一。
当您先编写代码进行测试时,您会发现给定代码单元的任何“条件”都会自然而然地推到依赖关系(通常是通过模拟或存根),当您在代码中采用它们时。
“给定条件x,期望行为y”通常会变成要提供的存根x
(在这种情况下,测试需要验证当前组件的行为),并且y
将成为模拟,将在以下位置验证对它的调用测试结束(除非它是“应该返回y
”,在这种情况下,测试将仅明确验证返回值)。
然后,一旦该单元按照指定的方式运行,就继续编写发现的依赖项(for x
和y
)。
这使编写干净的模块化代码成为一个非常自然的过程,否则,通常很容易模糊职责并将行为耦合在一起而没有意识到。
当为一段代码编写测试变得困难时,因为存根或模拟的事情太多了,或者由于事情紧密地结合在一起,您就知道自己的代码有待改进。
当“更改测试”成为一个负担,因为一个单元中有很多行为时,您就会知道自己的代码有待改进(或者仅仅是编写测试的方法,但是根据我的经验,通常不是这种情况) 。
当你的场景变得太复杂(“如果x
和y
和z
,然后......”),因为你需要更多的抽象,你知道你有改进你的代码,以使。
当由于重复和冗余而在两个不同的固定装置中进行相同的测试时,您知道自己的代码有待改进。
这是Michael Feathers的精彩演讲,展示了代码中可测试性与设计之间的紧密联系(最初由displayName发表在注释中)。演讲还讨论了一些关于良好设计和可测试性的普遍抱怨和误解。
单元测试的好处在于,它们允许您像其他程序员一样使用您的代码。
如果您的代码难以进行单元测试,那么使用它可能会很尴尬。如果您不能不加循环地注入依赖项,那么您的代码可能会变得不灵活。而且,如果您需要花费大量时间来设置数据或弄清楚处理该事务的顺序,那么被测试的代码可能耦合太多,并且很难使用。
我花了很长时间才意识到,但是进行测试驱动开发(使用单元测试)的真正好处(编辑:对我来说,您的里程可能会有所不同)是您必须预先进行API设计!
一种典型的开发方法是首先弄清楚如何解决给定的问题,并利用该知识和初始实施设计来某种方式来调用您的解决方案。这可能会给出一些相当有趣的结果。
在进行TDD时,您首先必须编写将使用您的解决方案的代码。输入参数和预期输出,以便可以确保它是正确的。反过来,这又需要您弄清楚实际需要执行的操作,以便可以创建有意义的测试。然后只有这样,您才能实施该解决方案。根据我的经验,当您确切地知道代码应该实现什么时,它就会变得更加清晰。
然后,在实现单元测试之后,可以帮助您确保重构不会破坏功能,并提供有关如何使用代码的文档(在测试通过时,您就知道这是正确的!)。但是这些是次要的-最大的好处是首先创建代码的思路。
通过单元测试,您可以查看功能之间的接口如何工作,并且通常可以使您洞悉如何改进本地设计和总体设计。此外,如果在开发代码时开发单元测试,那么您将拥有现成的回归测试套件。开发UI或后端库都没关系。
一旦开发了程序(带有单元测试),就发现了错误,您可以添加测试以确认错误已修复。
我将TDD用于某些项目。我花了很多精力来制作从教科书或论文中得出的正确示例,并使用这些示例测试我正在开发的代码。我对这些方法的任何误解都非常明显。
我倾向于比我的一些同事宽松,因为我不在乎是先编写代码还是先编写测试。
简而言之,编写单元测试有助于揭示代码中的缺陷。
由Jonathan Wolter,Russ Ruffer和MiškoHevery撰写的这份编写可测试代码的引人注目的指南,包含了许多示例,这些示例说明了代码缺陷如何阻碍测试,也阻止了相同代码的轻松重用和灵活性。因此,如果您的代码是可测试的,则更易于使用。大多数“道德”都是荒谬的简单技巧,可极大地改善代码设计(“ 依赖注入 FTW”)。
例如:当缓存开始逐出东西时,很难测试方法computeStuff是否正常运行。这是因为您必须手动将废话添加到缓存中,直到“ bigCache”快满为止。
public OopsIHardcoded {
Cache cacheOfExpensiveComputations;
OopsIHardcoded() {
this.cacheOfExpensiveComputation = buildBigCache();
}
ExpensiveValue computeStuff() {
//DOES THIS WORK CORRECTLY WHEN CACHE EVICTS DATA?
}
}
但是,当我们使用依赖项注入时,当缓存开始逐出东西时,测试computeStuff方法是否正确运行要容易得多。我们要做的就是在我们称为“ new HereIUseDI(buildSmallCache());
通知”的地方创建一个测试,我们对该对象进行了更细微的控制,并且立即支付了股息。
public HereIUseDI {
Cache cacheOfExpensiveComputations;
HereIUseDI(Cache cache) {
this.cacheOfExpensiveComputation = cache;
}
ExpensiveValue computeStuff() {
//DOES THIS WORK CORRECTLY WHEN CACHE EVICTS DATA?
}
}
当我们的代码需要通常保存在数据库中的数据时,也可以得到类似的好处……只需准确输入所需的数据即可。
取决于“单元测试”的含义,我认为真正的低级单元测试并不能像较高级别的集成测试那样促进良好的设计-测试一组角色(类,函数等)的测试。您的代码可以正确组合,以产生开发团队和产品所有者之间已达成共识的一系列理想行为。
如果您可以在这些级别上编写测试,那么它会带您创建不需要很多疯狂依赖的漂亮的,逻辑性的,类似于API的代码-拥有简单测试设置的愿望自然会促使您没有很多疯狂的依赖关系或紧密耦合的代码。
没错- 单元测试可能会导致您设计不佳,以及设计不佳。我已经看到开发人员采用了一些已经具有良好逻辑设计和单一关注点的代码,并且将其拆开并纯粹出于测试目的而引入了更多接口,结果使代码的可读性和更改难度降低,如果开发人员决定拥有许多低级别的单元测试意味着他们不必具有更高级别的测试,甚至可能会有更多的错误。一个特别喜欢的示例是我修复的一个错误,该错误中存在很多非常糟糕的,“可测试的”代码,它们与在剪贴板上和从剪贴板上获取信息有关。所有这些都分解成非常小的细节,并具有很多接口,测试中的许多模拟以及其他有趣的东西。仅有一个问题-没有任何代码与操作系统的剪贴板机制实际交互,
单元测试肯定可以驱动您的设计-但是它们不能自动地指导您进行良好的设计。您确实需要了解什么是好的设计,而不仅仅是“这段代码经过测试,因此是可测试的,因此很好”。
当然,如果您是“单元测试”意味着“不是通过UI驱动的任何自动化测试”的人之一,那么其中一些警告可能就没有那么重要了-正如我所说,我认为那些警告更高级别集成测试通常在驱动设计时更有用。