单元测试如何促进设计?


43

我们的同事提倡编写单元测试,因为它实际上是在帮助我们改进设计和重构事物,但我不知道如何做。如果我正在加载CSV文件并进行解析,那么单元测试(验证字段中的值)将如何帮助我验证设计?他提到了耦合和模块化等问题,但是对我而言,这没有多大意义-但我的理论背景并不多。

这与您标记为重复的问题不同,我将对实际示例如何起到帮助作用感兴趣,而不仅仅是理论上说“有帮助”。我喜欢下面的答案和评论,但我想了解更多。



3
下面的答案确实是您需要了解的所有信息。那些整日都在写那些由聚合根依赖注入的工厂工厂的人坐在旁边,是一个安静地针对单元测试编写简单代码的人,这些单元测试可以正常运行,易于验证并且已经被记录在案。
罗伯特·哈维

4
@gnat做单元测试不会自动暗示TDD,这是一个不同的问题
Joppe

11
“单元测试(验证字段中的值)” -您似乎正在将单元测试与输入验证相混淆。
jonrsharpe

1
@jonrsharpe鉴于它是解析CSV文件的代码,他可能正在谈论验证某个CSV字符串是否提供预期输出的实际单元测试。
JollyJoker '17

Answers:


3

单元测试不仅有助于设计,而且这是其主要优点之一。

编写测试优先级可以消除模块化和简洁的代码结构。

当您先编写代码进行测试时,您会发现给定代码单元的任何“条件”都会自然而然地推到依赖关系(通常是通过模拟或存根),当您在代码中采用它们时。

“给定条件x,期望行为y”通常会变成要提供的存根x(在这种情况下,测试需要验证当前组件的行为),并且y将成为模拟,将在以下位置验证对它的调用测试结束(除非它是“应该返回y”,在这种情况下,测试将仅明确验证返回值)。

然后,一旦该单元按照指定的方式运行,就继续编写发现的依赖项(for xy)。

这使编写干净的模块化代码成为一个非常自然的过程,否则,通常很容易模糊职责并将行为耦合在一起而没有意识到。

稍后编写测试将告诉您何时代码结构不良。

当为一段代码编写测试变得困难时,因为存根或模拟的事情太多了,或者由于事情紧密地结合在一起,您就知道自己的代码有待改进。

当“更改测试”成为一个负担,因为一个单元中有很多行为时,您就会知道自己的代码有待改进(或者仅仅是编写测试的方法,但是根据我的经验,通常不是这种情况) 。

当你的场景变得太复杂(“如果xyz,然后......”),因为你需要更多的抽象,你知道你有改进你的代码,以使。

当由于重复和冗余而在两个不同的固定装置中进行相同的测试时,您知道自己的代码有待改进。

这是Michael Feathers的精彩演讲,展示了代码中可测试性与设计之间的紧密联系(最初由displayName发表在注释中)。演讲还讨论了一些关于良好设计和可测试性的普遍抱怨和误解。


@SSECommunity:到目前为止,只有2个投票,这个答案很容易被忽略。我强烈推荐与该答案相关联的Michael Feathers演讲。
displayName

103

单元测试的好处在于,它们允许您像其他程序员一样使用您的代码。

如果您的代码难以进行单元测试,那么使用它可能会很尴尬。如果您不能不加循环地注入依赖项,那么您的代码可能会变得不灵活。而且,如果您需要花费大量时间来设置数据或弄清楚处理该事务的顺序,那么被测试的代码可能耦合太多,并且很难使用。


7
好答案。我总是喜欢将测试视为代码的第一个客户端;如果编写测试很麻烦,那么编写消耗API或我正在开发的内容的代码将很痛苦。
斯蒂芬·伯恩

41
以我的经验,大多数单元测试不会 “像其他程序员一样使用您的代码。” 他们使用您的代码,因为单元测试将使用该代码。没错,它们将揭示许多严重的缺陷。但是,为单元测试而设计的API可能不是最适合常规使用的API。简单地编写单元测试通常需要底层代码公开太多内部信息。同样,根据我的经验-会对您如何处理感兴趣。(请参阅下面的答案)
user949300 '17

7
@ user949300-首先我不是测试的忠实拥护者。我的答案首先基于代码(当然还有设计)的想法。API不应为单元测试而设计,而应为您的客户而设计。单元测试可以帮助您近似客户,但这是一种工具。他们在那里为您服务,反之亦然。而且,它们肯定不会阻止您编写糟糕的代码。
Telastyn

3
根据我的经验,单元测试的最大问题是,编写好的测试与一开始编写好的代码一样困难。如果您不能从不好的代码中分辨出好的代码,那么编写单元测试将不会使您的代码变得更好。在编写单元测试时,您必须能够区分是平滑,愉快的用法还是“笨拙”或困难的用法。它们可能会使您稍微使用代码,但不会强迫您认识到您所做的事情很糟糕。
jpmc26

2
@ user949300-我在这里想到的经典示例是需要connString的存储库。假设您将其公开为可写的公共属性,并且必须在new()存储库之后进行设置。这个想法是,在第5次或第6次编写了一个忘记执行该步骤的测试之后-从而崩溃了-您将自然而然地倾向于将connString强制为类不变-在构造函数中传递-从而使您的API更好,并且可以编写避免这种陷阱的生产代码。这不是保证,但确实有帮助,imo。
斯蒂芬·伯恩

31

我花了很长时间才意识到,但是进行测试驱动开发(使用单元测试)的真正好处(编辑:对我来说,您的里程可能会有所不同)是您必须预先进行API设计

一种典型的开发方法是首先弄清楚如何解决给定的问题,并利用该知识和初始实施设计来某种方式来调用您的解决方案。这可能会给出一些相当有趣的结果。

在进行TDD时,您首先必须编写将使用您的解决方案的代码。输入参数和预期输出,以便可以确保它是正确的。反过来,这又需要您弄清楚实际需要执行的操作,以便可以创建有意义的测试。然后只有这样,您才能实施该解决方案。根据我的经验,当您确切地知道代码应该实现什么时,它就会变得更加清晰。

然后,在实现单元测试之后,可以帮助您确保重构不会破坏功能,并提供有关如何使用代码的文档(在测试通过时,您就知道这是正确的!)。但是这些是次要的-最大的好处是首先创建代码的思路。


那当然是一个好处,但是我不认为这是“真正的”好处-真正的好处来自以下事实:为您的代码编写测试会自然地将“条件”推出依赖关系,并消除依赖关系的过度注入(进一步促进抽象化) )开始之前。
Ant P

问题在于,您需要预先编写与该API匹配的整套测试,然后它不能完全按需工作,因此您必须重写代码和所有测试。对于面向公众的API,它们很可能不会更改,这种方法很好。但是,仅在内部使用的代码API确实发生了很大变化,因为您弄清楚了如何实现需要大量半私有API协同工作的功能
Juan Mendes

@AntP是的,这是API设计的一部分。
托尔比约恩Ravn的安德森

@JuanMendes这并不少见,需要更改这些测试,就像更改需求时的任何其他代码一样。一个好的IDE将帮助您重构类,作为更改方法签名等时自动完成的工作的一部分。
ThorbjørnRavn Andersen

@JuanMendes,如果您编写好的测试和较小的单元,则实际上所描述的效果的影响很小。
蚂蚁P

6

我会100%同意单元测试有助于“帮助我们完善设计和重构事物”。

对于他们是否帮助您进行初步设计,我有两个想法。是的,它们显示出明显的缺陷,并且会迫使您考虑“如何使代码可测试”?这将减少副作用,简化配置和设置等。

但是,以我的经验来看,在真正理解设计之前就编写了过于简单的单元测试(诚然,这是对核心TDD的夸大,但编码人员经常在想得太多之前编写测试)常常导致乏味公开太多内部信息的领域模型。

我在TDD上的经验是几年前的,所以我很想听听哪些较新的技术可能会有助于编写不会对基础设计造成过多偏见的测试。谢谢。


大量的方法参数是代码异味和设计缺陷。
苏菲安

5

通过单元测试,您可以查看功能之间的接口如何工作,并且通常可以使您洞悉如何改进本地设计和总体设计。此外,如果在开发代码时开发单元测试,那么您将拥有现成的回归测试套件。开发UI或后端库都没关系。

一旦开发了程序(带有单元测试),就发现了错误,您可以添加测试以确认错误已修复。

我将TDD用于某些项目。我花了很多精力来制作从教科书或论文中得出的正确示例,并使用这些示例测试我正在开发的代码。我对这些方法的任何误解都非常明显。

我倾向于比我的一些同事宽松,因为我不在乎是先编写代码还是先编写测试。


这对我来说是一个很好的答案。您介意举几个例子吗,例如每个案例一个(当您了解设计知识时等)。
User039402 '02

5

当您要对解析器检测值进行正确定界的单元测试时,您可能希望将其从CSV文件传递给一行。为了使您的测试简单直接,您可能需要通过一种接受一行的方法进行测试。

这将自动使您将读取行与读取单个值区分开。

在另一个级别上,您可能不希望将各种物理CSV文件放入测试项目中,而是做一些更具可读性的操作,只是在测试中声明一个较大的CSV字符串以提高可读性和测试意图。这将导致您将解析器与您将在其他地方执行的任何I / O分离。

只是一个基本示例,只要开始练习,您就会在某些时候感受到魔力(我有)。


4

简而言之,编写单元测试有助于揭示代码中的缺陷。

由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?
   }
}

当我们的代码需要通常保存在数据库中的数据时,也可以得到类似的好处……只需准确输入所需的数据即可。


2
老实说,我不确定您对这个例子的理解。方法computeStuff与缓存有何关系?
约翰五世

1
@ user970696-是的,我的意思是“ computeStuff()”使用缓存。问题是“ computeStuff()是否一直正确工作(取决于缓存的状态)”因此,如果您不能直接设置/确定所有可能的缓存状态,则很难确认computeStuff()是否满足您的要求构建缓存,因为您对行“ cacheOfExpensiveComputation = buildBigCache();”进行了硬编码。(而不是直接通过构造函数传递缓存)
Ivan

0

取决于“单元测试”的含义,我认为真正的低级单元测试并不能像较高级别的集成测试那样促进良好的设计-测试一组角色(类,函数等)的测试。您的代码可以正确组合,以产生开发团队和产品所有者之间已达成共识的一系列理想行为。

如果您可以在这些级别上编写测试,那么它会带您创建不需要很多疯狂依赖的漂亮的,逻辑性的,类似于API的代码-拥有简单测试设置的愿望自然会促使您没有很多疯狂的依赖关系或紧密耦合的代码。

没错- 单元测试可能会导致您设计不佳,以及设计不佳。我已经看到开发人员采用了一些已经具有良好逻辑设计和单一关注点的代码,并且将其拆开并纯粹出于测试目的而引入了更多接口,结果使代码的可读性和更改难度降低,如果开发人员决定拥有许多低级别的单元测试意味着他们不必具有更高级别的测试,甚至可能会有更多的错误。一个特别喜欢的示例是我修复的一个错误,该错误中存在很多非常糟糕的,“可测试的”代码,它们与在剪贴板上和从剪贴板上获取信息有关。所有这些都分解成非常小的细节,并具有很多接口,测试中的许多模拟以及其他有趣的东西。仅有一个问题-没有任何代码与操作系统的剪贴板机制实际交互,

单元测试肯定可以驱动您的设计-但是它们不能自动地指导您进行良好的设计。您确实需要了解什么是好的设计,而不仅仅是“这段代码经过测试,因此是可测试的,因此很好”。

当然,如果您是“单元测试”意味着“不是通过UI驱动的任何自动化测试”的人之一,那么其中一些警告可能就没有那么重要了-正如我所说,我认为那些警告更高级别集成测试通常在驱动设计时更有用。


-2

代码通过所有测试时,单元测试可以帮助重构。

假设您实施了冒泡排序是因为您很着急而不关注性能,但是现在您想要快速排序,因为数据越来越长。如果所有测试均通过,则情况看起来不错。

当然,要使这项工作有效,测试必须是全面的。在我的示例中,您的测试可能无法涵盖稳定性,因为与冒泡无关。


1
的确如此,但是与直接影响代码设计质量相比,它更具有可维护性。
蚂蚁P

@ AntP,OP询问有关重构和单元测试的信息。
om

1
这个问题提到了重构,但是实际的问题是关于单元测试如何改善/验证代码设计-并不简化重构本身的过程。
蚂蚁P

-3

我发现单元测试对于促进项目的长期维护最有价值。当我几个月后回到一个项目并且不记得很多细节时,运行测试可以防止我遇到麻烦。


6
那当然是测试的一个重要方面,但是并不能真正回答问题(这不是测试为什么好,而是测试如何影响设计的原因)。
绿巨人
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.