严格出于测试目的修改代码是否是错误的做法


77

我与一名程序员同事讨论关于仅修改一段可工作的代码以使其可测试(例如通过单元测试)是好是坏的做法。

我的观点是,在保持良好的面向对象和软件工程实践的范围内(当然不要“公开一切”等),这是可以的。

我同事的观点是,仅出于测试目的修改代码(有效)是错误的。

只是一个简单的示例,请考虑某些组件(用C#编写)使用的这段代码:

public void DoSomethingOnAllTypes()
{
    var types = Assembly.GetExecutingAssembly().GetTypes();

    foreach (var currentType in types)
    {
        // do something with this type (e.g: read it's attributes, process, etc).
    }
}

我建议可以修改此代码以调出另一个可以完成实际工作的方法:

public void DoSomething(Assembly asm)
{
    // not relying on Assembly.GetExecutingAssembly() anymore...
}

此方法接受一个Assembly对象进行处理,从而可以通过您自己的Assembly进行测试。我的同事认为这不是一个好习惯。

什么被认为是良好且常见的做法?


5
修改后的方法应将Type参数而不是用作参数Assembly
罗伯特·哈维

您是对的-Type或Assembly,重点是代码应允许将其作为参数提供,尽管似乎此功能仅用于测试...
liortal

5
您的汽车具有用于“测试”的ODBI端口-如果一切正常,则不需要。我的猜测是您的汽车比他的软件更可靠。
mattnz

24
他对代码“有效”有什么保证?
Craige

3
当然-这样做。经过测试的代码随时间推移是稳定的。但是,如果新引入的错误进行了某些功能改进而不是可测试性修复,则您将有更多的时间来解释新引入的错误
Petter Nordlander

Answers:


135

修改代码使其更具可测试性具有可测试性以外的其他优势。一般而言,可测试性更高的代码

  • 更容易维护,
  • 更容易推理,
  • 更宽松地耦合,并且
  • 在架构上具有更好的总体设计。

3
我没有在回答中提及这些事情,但我完全同意。重新编写代码以使其更具可测试性往往会带来一些令人愉悦的副作用。
杰森·斯威特

2
+1:我通常同意。当然,在明确的情况下,使代码可测试会损害这些其他有益的目标,并且在这些情况下,可测试性处于最低优先级。但是总的来说,如果您的代码不够灵活/可扩展以无法测试,那么它就不会足够灵活/可扩展以无法使用或老化。
2013年

6
可测试的代码是更好的代码。修改没有测试的代码总是存在内在风险,而在极短的时间内减轻风险的最简单,最便宜的方法就是放任不管,这很好……直到您真正需要更改代码。您需要做的就是将单元测试的好处出售给您的同事;如果每个人都在进行单元测试,那么就没有理由认为代码需要是可测试的。如果不是每个人都参与单元测试,那就没有意义了。
guysherman

3
究竟。我记得我当时在为自己的代码中的约1万行源代码编写单元测试。我知道代码运行良好,但是测试迫使我重新考虑某些情况。我不得不问自己“这种方法到底在做什么?”。我只是从新的角度看工作代码中发现了几个错误。
苏珊(Sulthan),

2
我一直单击向上按钮,但是我只能给您一点。我现在的雇主目光短浅,无法让我们实际执行单元测试,而我仍然有意识地编写自己的代码,就像在从事测试驱动程序一样,仍能从中受益。
AmericanUmlaut

59

有(貌似)对立的力量在起作用。

  • 一方面,您要强制执行封装
  • 另一方面,您希望能够测试该软件

支持将所有“实现细节”保密的支持者通常是出于保持封装的愿望。但是,保持所有内容锁定和不可用是一种错误的封装方法。如果使所有内容不可用是最终目标,那么唯一真正的封装代码就是:

static void Main(string[] args)

您的同事提议将其设为代码中的唯一访问点吗?外部调用者是否应该无法访问所有其他代码?

几乎不。那么,什么可以公开一些方法呢?最终不是主观的设计决定吗?

不完全的。即使在无意识的水平上,倾向于引导程序员的又是封装的概念。当公开方法可以正确保护其不变式时,您可以放心地公开

我不想公开不保护其不变式的私有方法,但通常您可以对其进行修改,以使其确实保护其不变式,然后将其公开(当然,使用TDD,您可以反之)。

为可测试性打开一个API是一件好事,因为您真正要做的是应用“开放/封闭原则”

如果您只有一个API调用者,那么您将不知道API到底有多灵活。很有可能,这很不灵活。测试充当第二个客户端,为您提供有关API灵活性的宝贵反馈

因此,如果测试表明您应该打开您的API,请这样做;否则,请执行该步骤。但不要通过隐藏复杂性而是通过以故障安全的方式公开复杂性来维护封装。


3
+1 在适当保护其不变式的情况下,公开它是安全的。
shambulator

1
我爱你的答案!:)我听过多少次:“封装是使某些方法和属性私有的过程”。这就像在面向对象编程时说的那样,这是因为您使用对象进行编程。:(我对依赖注入大师的回答如此清晰并不感到惊讶。我一定会读几次您的回答,以使我对我可怜的旧遗留代码微笑。–
Samuel

我对您的答案做了一些调整。顺便说一句,我阅读了您的《属性封装》一文,我听说过的使用“自动属性”的唯一令人信服的原因是,将公共变量更改为公共属性会破坏二进制兼容性。从一开始就将其公开为“自动属性”,以便以后可以添加验证或其他内部功能,而不会破坏客户端代码。
罗伯特·哈维

21

好像您在谈论依赖注入。对于IMO来说,这确实很普遍,对于可测试性来说是非常必要的。

为了解决更广泛的问题,即修改代码以使其可测试是否是一个好主意,请这样思考:代码具有多种职责,包括a)要执行的任务,b)供人类阅读的任务以及c)被测试。所有这三个都很重要,如果您的代码不能同时满足这三个职责,那么我认为这不是很好的代码。所以修改掉!


DI不是主要问题(我只是想举一个例子),关键是提供一种原本不打算创建的方法(仅出于测试目的)是否可以。
2013年

4
我知道。我以为我在第二段中以“是”解决了你的要点。
杰森·斯威特

13

这有点鸡和鸡蛋的问题。

对代码进行良好的测试覆盖率很好的最大原因之一是,它可以使您无畏地进行重构。但是您处于一种需要重构代码才能获得良好测试覆盖率的情况下!而你的同事很害怕。

我看到你同事的观点。您有(可能)有效的代码,并且如果您出于任何原因去对其进行重构,则有可能会破坏它。

但是,如果这是预期将进行不断维护和修改的代码,那么您每次对其进行任何工作时,都将面临这种风险。和重构现在和得到一些测试覆盖率现在可以让你冒这个风险,控制的条件下,并获取代码,以备将来更好地改变形状。

因此,我想说,除非这个特定的代码库是相当静态的,并且预计将来不会做大量工作,否则您想要做的是良好的技术实践。

当然,这是否是一个好的商业惯例,完全是“蠕虫的另一罐”。


一个重要的问题。通常,我会自由地执行IDE支持的重构,因为破坏任何东西的机会非常低。如果重构涉及更多的问题,那么我只会在需要更改代码时才这样做。对于更换零件,您需要进行测试以降低风险,从而甚至获得业务价值。
汉斯·彼得·斯特尔

关键是:我们是否要保留旧代码,是因为我们要赋予对象查询当前程序集的责任,还是因为我们不想添加一些公共属性或更改方法签名?我认为代码违反了SRP,因此,无论同事多么害怕,都应该将其重构。当然,如果这是许多用户使用的公共API,则您必须考虑一种策略,例如实现外观或任何可帮助您向旧代码授予防止过多更改的接口的策略。
塞缪尔

7

这可能仅仅是从其他的答案重点不同,但我要说的是,代码应该进行重构,严格以改善可测性。可测试性对于维护非常重要,但是可测试性本身并不是目的。因此,我将推迟进行任何此类重构,直到您可以预测该代码需要维护以达到某些业务目的为止。

在您确定此代码将需要某种维护的时候,将是重构可测试性的好时机。根据您的业务案例,可能是一个合理的假设,即所有代码最终都将需要维护,在这种情况下,我与此处其他答案(例如 Jason Swett的答案)之间的区别消失了。

综上所述:仅可测试性并不是(IMO)重构代码库的充分理由。可测试性在允许对代码库进行维护方面具有重要作用,但是修改代码功能(这会推动重构)是业务需求。如果没有这样的业务需求,那么最好进行客户会关心的事情。

(当然,新代码正在积极维护中,因此应将其编写为可测试的。)


2

我认为你的同事错了。

其他人已经提到了为什么这已经是一件好事的原因,但是只要您可以继续这样做,您就可以了。

发出此警告的原因是,对代码进行任何更改都会以必须重新测试代码为代价。根据您所做的工作,这项测试工作本身可能实际上是一项巨大的工作。

重构和开发新功能(不一定会使您的公司/客户受益)不一定是您决定的地方。


2

在检查是否通过代码的所有路径时,我已经使用代码覆盖率工具作为单元测试的一部分。作为我自己一个非常优秀的编码器/测试人员,我通常覆盖80-90%的代码路径。

当我研究未发现的路径并为其中的一些努力时,就是当我发现诸如“永远不会发生”的错误案例之类的错误时。因此,是的,修改代码并检查测试覆盖范围可以使代码更好。


2

这里的问题是您的测试工具很糟糕。您应该能够模拟出该对象并调用您的测试方法而无需更改它-因为尽管这个简单的示例确实非常简单且易于修改,但是当您遇到更复杂的事情时会发生什么。

许多人修改了他们的代码以引入IoC,DI和基于接口的类,只是为了使用需要更改这些代码的模拟和单元测试工具来进行单元测试。我不认为它们是健康的事情,不是当您看到非常简单明了的代码变成简单的噩梦般的复杂交互时,它完全是由使每个类方法与其他所有方法完全脱钩而来的。为了侮辱人身,我们对于私有方法是否应该进行单元测试有很多争论!(当然,他们应该做什么?

问题当然就在于测试工具的性质。

现在有更好的工具可以使这些设计变更永久消失。Microsoft提供了Fakes(nee Moles),使您可以对具体对象(包括静态对象)进行存根处理,因此您不再需要更改代码以适合该工具。在您的情况下,如果使用了Fakes,则将GetTypes调用替换为自己的返回有效和无效测试数据的调用-这很重要,建议的更改根本没有提供。

回答:您的同事是正确的,但可能是由于错误的原因。请勿更改要测试的代码,更改测试工具(或更改整个测试策略,以使用更多的集成样式的单元测试,而不要使用这种细粒度的测试)。

马丁·福勒(Martin Fowler)在他的文章《嘲笑不是存根》中对此领域进行了讨论。


1

良好的常用做法是使用单元测试和调试日志。单元测试可确保如果您对程序进行任何进一步的更改,则旧功能不会中断。调试日志可以帮助您在运行时跟踪程序。
有时,甚至发生了一些事情,我们仅需要出于测试目的而需要一些东西。为此更改代码并不罕见。但是应注意不要因此而影响生产代码。在C ++和C中,这是使用MACRO来实现的,MACRO是编译时实体。这样,在生产环境中根本就不会出现测试代码。不知道C#中是否有这样的规定。
另外,当您在程序中添加测试代码时,它应该清晰可见这部分代码是出于某些测试目的而添加的。否则,试图理解代码的开发人员只会在代码的那部分工作。


1

您的示例之间存在一些严重的差异。对于DoSomethingOnAllTypes(),有一个含义do something适用于当前程序集中的类型。但是DoSomething(Assembly asm)明确表示可以将任何程序集传递给它。

我指出这一点的原因是,许多仅用于测试依赖项注入步骤超出了原始对象的范围。我知道您说过“ 不'公开一切' ”,但这是该模型的最大错误之一,紧随其后的是:将对象的方法开放给它们不打算使用的方法。


0

您的问题没有给您的同事争论太多的背景,因此有进行猜测的空间

是否“不良做法”取决于更改的方式时间

在我看来,您提取方法的示例DoSomething(types)是可以的。

但我所看到的代码是不正常是这样的:

public void DoSomethingOnAllTypes()
{
  var types = (!testmode) 
      ? Assembly.GetExecutingAssembly().GetTypes() 
      : getTypesFromTestgenerator();

  foreach (var currentType in types)
  {
     if (!testmode)
     {
        // do something with this type that made the unittest fail and should be skipped.
     }
     // do something with this type (e.g: read it's attributes, process, etc).
  }
}

这些更改使代码更难于理解,因为您增加了可能的代码路径数量。

我的意思是如何以及何时

如果您有一个可行的实现,并且为了“实现测试功能”而进行了更改,则必须重新测试应用程序,因为您可能破坏了DoSomething()方法。

if (!testmode)是更更difficulilt理解和测试比提取方法。

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.