单元测试是否可以帮助花旗避免这种昂贵的错误?


86

我读到了这句话:在合法交易被误认为测试数据15年之后,花在编程上的错误使花旗集团损失了700万美元

在1990年代中期引入该系统时,程序代码过滤掉了从089到100的三位数分支代码的所有交易,并将这些前缀用于测试目的。

但是在1998年,公司在扩展业务时开始使用字母数字分支代码。其中包括代码10B,10C等,系统将该代码视为在排除范围内,因此,它们的交易已从发送给SEC的所有报告中删除。

(我认为这说明使用非显式数据指示符是次优的。填充并使用语义上显式的Branch.IsLive属性要好得多。)

除此之外,我的第一个反应是“单元测试会在这里有所帮助”……但是他们会吗?

我最近读了《为什么大多数单元测试会引起人们的兴趣浪费》,所以我的问题是:在引入字母数字分支代码后失败的单元测试会是什么样?



17
看来他们还错过了一项集成测试,该测试检查了导出到SEC的交易量。如果您构建导出功能,那将是一个合理的检查。
卢克·弗兰肯

31
本文的作者似乎并不了解单元测试。有些说法很荒谬(“单元测试不太可能测试任何给定方法的一万亿分之一以上的功能”),另一些说法则破坏了回归的机会(“看一看一年内从未失败的测试,考虑将它们扔掉”)。还是像“将单元测试转变为断言”之类的建议,这些建议应该更改针对运行时异常的失败测试?
Groo

25
@gnat我没有阅读外部链接,但我仍然觉得这个问题有意义
Jeutnarg

23
就其价值而言,我几乎不同意“为什么大多数单元测试都是浪费”中的所有内容。我会写一个反驳,但是这个余量太小而无法容纳它。
罗伯特·哈维

Answers:


19

您是否真的在问,“单元测试是否对这里有所帮助?”,或者您在问,“任何种类的测试都可能对这里有所帮助?”。

最有帮助的测试形式是代码本身的前提条件断言,即分支标识符仅由数字组成(假设这是编码器编写代码时所依据的假设)。

然后,这可能在某种形式的集成测试中失败,并且一旦引入了新的字母数字分支ID,该声明就会崩溃。但这不是单元测试。

或者,可以对生成SEC报告的程序进行集成测试。此测试可确保每个真实的分支标识符都报告其事务(因此需要真实输入,以及所有使用中的分支标识符的列表)。因此,这也不是单元测试。

我看不到所涉及接口的任何定义或文档,但可能是单元测试可能未检测到错误,因为单元没有故障。如果允许单元假定分支标识符仅由数字组成,并且开发人员从未决定过代码应做的情况(如果代码不这样做),则他们不应编写单元测试以在非数字标识符的情况下强制执行特定行为,因为该测试将拒绝正确处理字母数字分支标识符的单元的假设有效实现,并且您通常不想编写阻止有效的单元测试未来的实现和扩展。或者也许是40年前写的一份文档(通过原始EBCDIC中的一些词典范围,而不是更人性化的整理规则)隐含地定义了10B是测试标识符,因为事实上它确实在089和100之间。 15年前,有人决定将其用作真实标识符,因此,“故障”不在正确实现原始定义的单元中:它在于未能注意到10B被定义为测试标识符的过程,因此不应将其分配给分支。如果将089-100定义为测试范围,然后引入标识符10 $或1.0,则在ASCII中也会发生同样的情况。碰巧在EBCDIC中,数字在字母后面。

可以想到的一个单元测试(或可以说是功能测试)可能已经保存了一天,这是对生成或验证新分支标识符的单位的测试。该测试将断言标识符必须仅包含数字,并将其写入以便允许分支标识符的用户采用相同的标识符。或者,也许某个地方有一个单元导入了真实的分支标识符,但从未看到过测试的标识符,并且可以对其进行单元测试以确保它拒绝所有测试标识符(如果标识符只有三个字符,我们可以枚举所有字符,然后比较它们的行为)验证程序与测试过滤器的验证程序以确保它们匹配,这是对点测试的常见异议)。然后,当有人更改规则时,单元测试将失败,因为它与新要求的行为相矛盾。

由于测试的存在是有充分的理由的,因此由于业务需求的变化而需要将其删除的点成为了某人获得工作的机会,“在代码中的每个位置都找到了我们想要的行为。更改”。当然,这很困难,因此也不可靠,因此决不能保证节省时间。但是,如果您在假设单位属性的测试中掌握了假设,那么您就给了自己一个机会,因此工作量并没有完全浪费。

我当然同意,如果没有首先用“有趣的形状”输入定义单位,那么就没有要测试的东西。杂乱的名称空间划分可能很难正确测试,因为困难不在于实现您的有趣定义,而在于确保每个人都理解并尊重您的有趣定义。那不是一个代码单元的本地属性。此外,将某些数据类型从“数字字符串”更改为“字母数字字符串”类似于使基于ASCII的程序处理Unicode:如果您的代码与原始定义紧密耦合,并且在数据类型是程序工作的基础,因此它经常是紧密耦合的。

认为这很大程度上是浪费精力,这有点令人不安

如果您的单元测试有时失败(例如,在重构时),并且这样做可以为您提供有用的信息(例如,您的更改是错误的),那么您的工作就不会浪费。他们不做的是测试系统是否正常运行。因此,如果您编写单元测试而不是进行功能测试和集成测试,那么您可能会次优地使用时间。


断言很好!

3
@nocomprende:正如里根所言,“信任,但要验证”。
史蒂夫·杰索普

1
我还要说“单元测试不好!” 但是我认为大多数人会错过对Animal Farm的提法,而实际上开始批评我,而不是思考我在说什么(膝跳反应无效),但我没有那么说。也许一个更聪明,更博学的人可以说明这一点。

2
“所有测试都通过了,但是有些测试比其他测试通过的更多!”
格雷厄姆

1
测试是一条红鲱鱼。这些家伙只是不知道如何定义“分支代码”。这就像美国邮局不知道添加4位数字时正在更改邮政编码的定义一样。
Radarbob

120

单元测试可能已经发现分支代码10B和10C被错误地归类为“测试分支”,但是我发现该分支分类的测试不可能足够广泛地捕获该错误。

另一方面,对所生成报告的抽查可能表明,分支10B和10C在报告中始终比现在允许该漏洞存在的15年要早得多。

最后,这很好地说明了为什么将测试数据与实际生产数据混合在一个数据库中是个坏主意。如果他们使用了包含测试数据的单独数据库,则无需将其从官方报告中过滤掉,也就不可能过滤掉太多。


80
+1单元测试永远无法弥补糟糕的设计决策(例如混合测试和真实数据)
Jeutnarg

5
尽管最好避免将测试数据与真实数据混合,但是如果需要修改真实数据,则可能很难验证生产系统。例如,通过修改生产中的银行帐户总数来验证银行系统是一个坏主意。使用代码范围来指定含义是有问题的。记录的更明确的属性可能是更好的选择。
JimmyJames

4
@Voo我认为有一个默认的假设,即在实际或已部署的生产系统测试中,值得或有必要进行某种程度的复杂性或可靠性要求。(考虑由于错误的配置变量而可能导致多少错误。)我可以看到大型金融机构就是这种情况。
jpmc26 2016年

4
@Voo我不是在谈论测试。我说的是系统验证。在实际的生产系统中,它有很多方法可能会失败,而这些方法与代码无关。如果要将新的银行系统投入生产,则数据库或网络等中可能存在一些问题,导致无法将交易应用于帐户。我从没在银行工作过,但我敢肯定,开始用虚假交易修改真实帐户已经不受欢迎了。这样一来,您可以设置伪造帐户或等待祈祷。
JimmyJames

12
@JimmyJames在医疗保健中,通常定期将生产数据库复制到测试环境中,以对尽可能接近真实的数据进行测试。我认为银行也可以做到。
2013年

75

该软件必须处理某些业务规则。如果有单元测试,则单元测试将检查该软件是否正确处理了业务规则。

业务规则已更改。

显然没有人意识到业务规则已更改,也没有人更改软件以应用新的业务规则。如果有单元测试,则必须更改这些单元测试,但是没有人会这样做,因为没有人意识到业务规则已更改。

因此,不,单元测试不会发现这一点。

例外情况是,单元测试和软件是由独立的团队创建的,而进行单元测试的团队更改了测试以应用新的业务规则。然后,单元测试将失败,希望可以导致软件更改。

当然,在相同情况下,如果仅更改软件而不更改单元测试,则单元测试也将失败。每当单元测试失败时,这并不意味着软件是错误的,这意味着软件或单元测试(有时两者)都是错误的。


2
在一个团队中进行代码开发而另一个在“单元”测试中工作的团队是否可行?那怎么可能呢?...我一直在重构代码。
塞尔吉奥

2
@Sergio从一个角度来看,在保留行为的同时重构更改了内部结构-因此,如果编写的测试是在不依赖内部结构的情况下测试行为的,则它不需要更新。
丹妮丝

1
我已经多次看到这种情况。软件投入生产后没有任何抱怨,然后突然间,用户抱怨说它不再起作用,并且多年来逐渐失效。这就是当您决定不遵循标准通知流程而去更改内部程序时发生的事情
Brian Brian Bnoblauch

42
“业务规则已更改”是关键观察。单元测试验证您是否已实现您认为已实现的逻辑,而不是您的逻辑是正确的
Ryan Cavanaugh

5
如果我对所发生的事情是正确的,则不太可能编写用于捕获此问题的单元测试。选择测试的基本原理是测试一些“好”案例,一些“坏”案例以及带有任何边界的案例。在这种情况下,您将测试“ 099”,“ 100”和“ 101”。由于旧系统下的“拒绝非数字”测试涵盖了“ 10B”,而新系统下的“ 10B”大于101(因此被测试涵盖了),因此没有理由对其进行测试-除了在EBCDIC,“ 10B”在“ 099”和“ 100”之间排序。
2016年

29

否。这是单元测试的主要问题之一:它们使您陷入一种虚假的安全感。

如果您所有的测试都通过了,那并不意味着您的系统运行正常。这意味着您的所有测试都通过了。这意味着您有意识地思考并编写测试的设计部分正在按您有意地认为的那样工作,无论如何,这实际上并不是什么大问题:那是您实际上一直在密切注意的东西到,所以很可能您还是正确地做到了!但这并不能捕捉到您从未想到的案例,例如本案例,因为您从未想过为它们编写测试。 (如果有的话,您将意识到这意味着必须更改代码,并且已经进行了更改。)


17
父亲曾经问我:你为什么不想到自己没有想到的事情?(只有他以前常常说“如果你不知道,!” 就使人困惑。)但是我怎么知道我不知道呢?

7
“这意味着您有意识地思考并编写测试的设计部分正在按您有意地认为的那样工作。” 非常正确。如果您正在重构,或者系统中其他地方发生了一些改变而违反了您的假设,则此信息将非常宝贵。陷入错误的安全感的开发人员根本不了解单元测试的局限性,但这并没有使单元测试成为一种无用的工具。
罗伯特·哈维

12
@MasonWheeler:与您一样,作者认为单元测试应该以某种方式证明您的程序有效。没有。让我重复一遍:单元测试不能证明您的程序有效。 单元测试证明您的方法可以满足您的测试合同,仅此而已。本文的其余部分将落空,因为它基于该无效的前提。
罗伯特·哈维

5
自然地,那些错误信念的开发人员在单元测试完全失败时会感到失望,但这是开发人员的错,而不是单元测试的错,并且不会使单元测试提供的真正价值失效。
罗伯特·哈维

5
o_O @您的第一句话。单元测试给您一种错误的安全感,同时进行编码,就像把手放在方向盘上给您一种错误的安全感。
djechlin '16

10

不,不一定。

最初的要求是使用数字分支代码,因此将为接受各种代码并拒绝任何类似10B的组件进行单元测试。该系统本来可以正常工作(过去)。

然后,需求将发生变化,代码将更新,但这将意味着必须修改提供不良数据(现在是良好数据)的单元测试代码。

现在我们假设,管理系统的人员会知道是这种情况,并且会更改单元测试以处理新代码...但是,如果他们知道这种情况正在发生,他们也将知道会更改处理这些代码的代码。反正代码..他们没有那样做。如果您不知道更新测试,则最初拒绝代码10B的单元测试在运行时会高兴地说“这里一切都很好”。

单元测试适合于原始开发,但不适用于系统测试,尤其是长期遗忘了要求后的15年。

在这种情况下,他们需要的是端到端集成测试。您可以在其中传递希望使用的数据并查看是否可以使用的数据。有人会注意到他们的新输入数据没有产生报告,然后会进一步调查。


发现。以及单元测试的主要(唯一?)问题。救了我措辞自己的答案,因为我会说完全相同的一件事(但可能更糟!):)
轨道轻轨赛

8

类型测试(使用随机生成的有效数据测试不变式的过程,例如Haskell测试库QuickCheck以及受其他语言启发的各种端口/替代品)可能已经解决了这个问题,单元测试几乎肯定不会完成。

这是因为在更新分支代码的有效性规则时,不太可能有人会考虑测试这些特定范围以确保它们正常工作。

但是,如果已使用类型测试,则在实施原始系统时,应该有人编写了一对属性,一个用于检查特定于测试分支的代码是否被视为测试数据,另一个用于检查是否没有其他代码。是...当更新分支代码的数据类型定义时(为了允许测试分支代码从数字到数字的任何更改都有效),此测试将开始测试新范围,很可能已经确定了故障。

当然,QuickCheck最早是在1999年开发的,因此赶上这个问题已经为时已晚。


1
我认为将这种基于属性的测试称为更正常的做法,当然,编写基于属性的测试也是可能的,因为这种更改仍然可以通过(尽管我确实认为您更有可能编写可以找到它的测试)
jk。

5

我真的怀疑单元测试是否会对这个问题有所影响。听起来像是那些隧道视觉情况之一,因为功能已更改为支持新的分支代码,但这并未在系统的所有区域中执行。

我们使用单元测试来设计一个类。仅当设计已更改时才需要重新运行单元测试。如果某个特定单位没有变化,则未更改的单元测试将返回与以前相同的结果。单元测试不会向您显示更改对其他单元的影响(如果这样做的话,您不是在编写单元测试)。

您只能通过以下方式合理地检测到此问题:

  • 集成测试-但是您必须专门添加新的代码格式,才能通过系统中的多个单元(即,如果原始测试包括现在有效的分支,它们只会向您显示问题)
  • 端到端测试-企业应运行包含新旧分支代码格式的端到端测试

没有足够的端到端测试更加令人担忧。您不能依靠单元测试作为系统更改的唯一或主要测试。听起来好像只需要有人就新支持的分支代码格式运行报告。


2

运行时内置的断言可能有所帮助;例如:

  1. 创建一个像 bool isTestOnly(string branchCode) { ... }
  2. 使用此功能可以确定要过滤出的报告
  3. 在分支创建代码的断言中重用该函数,以验证或断言不是(无法)使用这种类型的分支代码创建分支!
  4. 在实时运行时启用此断言(而不是“除在代码的仅调试开发人员版本中以外,都已优化”)!

也可以看看:


2

解决的方法是快速失败

我们没有代码,也没有很多根据代码测试或不测试分支前缀的前缀示例。我们所拥有的是:

  • 089-100 =>测试分支
  • 10B,10C =>测试分支
  • <088 =>可能是真实分支
  • > 100 =>可能是真实分支

该代码允许数字和字符串的事实有点奇怪。当然,可以将10B和10C视为十六进制数,但是如果将前缀全部视为十六进制数,则10B和10C不在测试范围之内,将被视为实分支。

这可能意味着前缀存储为字符串,但在某些情况下被视为数字。这是我能想到的最简单的代码,它复制了这种行为(使用C#进行说明):

bool IsTest(string strPrefix) {
    int iPrefix;
    if(int.TryParse(strPrefix, out iPrefix))
        return iPrefix >= 89 && iPrefix <= 100;
    return true; //here is the problem
}

用英语来说,如果字符串是一个数字,并且介于89到100之间,则它是一个测试。如果不是数字,那就是测试。否则,这不是测试。

如果代码遵循此模式,则在部署代码时没有任何单元测试可以捕获此错误。以下是一些单元测试示例:

assert.isFalse(IsTest("088"))
assert.isTrue(IsTest("089"))
assert.isTrue(IsTest("095"))
assert.isTrue(IsTest("100"))
assert.isFalse(IsTest("101"))
assert.isTrue(IsTest("10B")) // <--- business rule change

单元测试表明“ 10B”应被视为测试分支。上面的@ gnasher729用户说,业务规则已更改,这就是上面的最后一个声明所显示的内容。在某个时候断言应该已经切换到isFalse,但是那没有发生。单元测试在开发和构建时运行,但此后再也没有。


这是什么教训? 该代码需要某种方式来表示已收到意外输入。这是编写此代码的另一种方法,该代码强调该代码期望前缀为数字:

// Alternative A
bool TryGetIsTest(string strPrefix, out bool isTest) {
    int iPrefix;
    if(int.TryParse(strPrefix, out iPrefix)) {
        isTest = iPrefix >= 89 && iPrefix <= 100;
        return true;
    }
    isTest = true; //this is just some value that won't be read
    return false;
}

对于不了解C#的用户,返回值指示代码是否能够解析给定字符串中的前缀。如果返回值为true,则调用代码可以使用isTest out变量检查分支前缀是否为测试前缀。如果返回值为false,则调用代码应报告预期的给定前缀,并且isTest out变量无意义,应将其忽略。

如果您可以接受例外处理,则可以改用以下方法:

// Alternative B
bool IsTest(string strPrefix) {
    int iPrefix = int.Parse(strPrefix);
    return iPrefix >= 89 && iPrefix <= 100;
}

这种选择更为简单。在这种情况下,调用代码需要捕获异常。无论哪种情况,代码都应该以某种方式向调用者报告它不希望将strPrefix转换为整数。这样,代码很快就会失败,银行可以迅速找到问题,而不会令SEC感到尴尬。


1

答案如此之多,甚至没有Dijkstra的名言:

测试表明存在缺陷,而不是缺陷。

因此,这取决于。如果对代码进行了正确的测试,则很可能不存在该错误。


-1

我认为这里进行单元测试可以确保问题永远不会存在。

考虑一下,您已经编写了bool IsTestData(string branchCode)函数。

您编写的第一个单元测试应为null和空字符串。然后,对于长度不正确的字符串,然后对于非整数字符串。

要使所有这些测试通过,您必须在函数中添加参数检查。

即使您仅测试“好”数据001-> 999而不考虑10A的可能性,在开始使用字母数字时,参数检查仍将迫使您重写该函数,以避免其抛出异常


1
这将无济于事-功能未更改,并且给定相同的测试数据,测试也不会开始失败。有人可能不得不考虑更改测试以使其失败,但是如果他们想到了这一点,那么他们可能也会考虑更改功能。
绿巨人

(或者也许我正在丢失一些东西,因为我不确定您所说的“参数检查”是什么意思)
绿巨人

为了通过简单的边缘案例单元测试,该函数将被强制为非整数字符串引发异常。因此,如果您开始使用字母数字分支代码而未对其进行专门编程,那么生产代码将出错
Ewan 2016年

但是函数不使用某些IsValidBranchCode函数来执行此检查吗?并且此功能可能已经更改,而无需修改IsTestData?。因此,如果您仅测试“良好数据”,则该测试将无济于事。为了开始失败,边缘案例测试必须包含一些现在有效的分支代码(而不仅仅是一些仍然无效的分支代码)。
绿巨人

1
如果检查是在IsValidCode中进行的,则该函数在没有自身显式检查的情况下通过,那么是有可能会错过它,但是随后我们将有更多的测试集,模拟验证器等,对于特定的“测试号码”
Ewan
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.