我读到了这句话:花15年的合法交易误认合法交易后,编程错误使花旗集团损失了700万美元。

1990年代中期系统引入后,程序代码被过滤掉了排除了使用089到100的三位数分支代码并使用这些前缀进行测试的所有交易。
但是在1998年,随着公司业务的扩展,该公司开始使用字母数字分支代码。其中包括代码10B,10C等,系统将其视为在排除范围内,因此它们的交易已从发送给SEC的任何报告中删除。

(我认为这说明了使用非显式数据指标是次优的。填充并使用语义上显式的Branch.IsLive属性会更好。)
此外,我的第一个反应是“单元测试将有助于在这里” ...但是会吗?
我最近阅读了为什么大多数单元测试会引起人们的兴趣浪费,所以我的问题是:在引入字母数字分支代码后失败的单元测试会是什么样?

评论

如果不查看外部链接就没有意义的问题是否应该关闭?

看来他们也错过了一项集成测试,该测试检查了导出到SEC的交易量。如果您构建导出功能是合理的选择。

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

@gnat我没有阅读外部链接,但我仍然发现这个问题很有意义

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

#1 楼

您是在问“单元测试是否在这里有所帮助?”,还是在问“是否有任何一种测试可以在这里有所帮助?”。

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

然后这可能会失败在某种形式的集成测试中,一旦引入了新的字母数字分支ID,则断言就会爆发。但这不是单元测试。或者,可以对生成SEC报告的程序进行集成测试。此测试可确保每个真实的分支标识符都报告其事务(因此需要真实输入,以及所有使用中的分支标识符的列表)。因此,这也不是单元测试。

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

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

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

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


认为这是浪费大量的精力,这有点令人不安



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

评论


断言是好的!

–user186205
16年7月15日在16:27

@nocomprende:正如里根所言,“信任,但要验证”。

–史蒂夫·杰索普(Steve Jessop)
16年7月15日在17:26



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

–user186205
16年7月15日在17:40

“所有测试都通过了,但是某些测试比其他测试通过的更多!”

–格雷厄姆
16年7月18日在12:18

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

– radarbob
16年7月18日在13:33

#2 楼

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

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

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

评论


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

–犹他那
16年7月14日在16:06

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

– JimmyJames
16年7月14日在16:40

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

– jpmc26
16 Jul 14'20:39



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

– JimmyJames
16年7月14日在20:41

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

– dj18
16年7月14日在21:54

#3 楼

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

业务规则已更改。

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

所以,不,单元测试不会抓住这一点。

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

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

评论


拥有一支不同的团队来进行代码开发而另一团队进行“单元”测试是否可行?那怎么可能呢?...我一直在重构代码。

–塞尔吉奥
16年7月14日在11:50



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

–丹妮丝
16年7月14日在12:39

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

–布赖恩·诺伯劳赫(Brian Knoblauch)
16年7月14日在14:50

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

– Ryan Cavanaugh
16年7月14日在18:14

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

–马克
16年7月14日在18:47

#4 楼

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

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

评论


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

–user186205
16年7月14日在13:28

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

–罗伯特·哈维(Robert Harvey)
16年7月15日在15:09

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

–罗伯特·哈维(Robert Harvey)
16 Jul 15 '15:18



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

–罗伯特·哈维(Robert Harvey)
16 Jul 15'15:22



o_O @您的第一句话。单元测试在编码时给人一种错误的安全感,就像在驾驶时把手放在方向盘上时给人一种错误的安全感。

–djechlin
16年7月15日在22:45

#5 楼

不,不一定。

最初的要求是使用数字分支代码,因此将为接受各种代码但拒绝任何10B之类的组件进行单元测试。该系统将在正常工作时通过。(

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

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

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

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

评论


发现。还有单元测试的主要(唯一?)问题。节省了我说自己的答案的措辞,因为我会说完全相同的一件事(但可能更糟!):)

–轨道轻赛
16 Jul 15'23:04



#6 楼

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

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

但是,如果已使用类型测试,则在实施原始系统时应有人编写了一对属性,以检查用于测试分支的特定代码是否被视为测试数据。然后检查分支代码的数据类型定义是否已更新(为了测试分支代码从数字到数字的任何更改都有效),这是必需的……该测试将开始在新范围内测试值,并且很可能已经确定了故障。

当然,QuickCheck最早是在1999年开发的,因此为时已晚,要抓住这个问题。 />

评论


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

– jk。
16年7月14日在9:15

#7 楼

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

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

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


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

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

#8 楼

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


创建类似bool isTestOnly(string branchCode) { ... }的函数

使用此函数来确定要滤除哪些报告
在断言中重用该函数,在分支创建代码中,用于验证或断言没有(不能)使用这种类型的分支代码创建分支!
请在实时运行时启用此断言(而不是“除了在仅调试的开发人员版本的代码中进行了优化之外”)!

另请参见:




将'debug'和'release分开建? (即在运行时启用断言)

应该测试内部实现还是仅测试公共行为? (即系统测试可能比单元测试更有用,覆盖面更广)


#9 楼

这样做的好处是可以快速失败。

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


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感到尴尬。

#10 楼

如此之多的答案,甚至连Dijkstra的引文都没有:


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


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

#11 楼

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

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

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

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

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

评论


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

–绿巨人
16 Jul 15'7:45



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

–绿巨人
16年7月15日在7:51

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

–伊万
16年7月15日在7:58

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

–绿巨人
16年7月15日在8:11



如果检查是在IsValidCode中进行的,则该函数将在没有其自身的显式检查的情况下通过,那么有可能会错过它,但是随后我们将有更多的测试,模拟验证器等额外的机会,针对特定的“测试编号”

–伊万
16年7月15日在8:40