亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

為了賬號安全,請及時綁定郵箱和手機立即綁定

單元測試:其實沒有絕對的概念

“单元测试”和“集成测试”这两个术语一直以来都比较模糊,甚至按照大多数软件术语的标准来看也是如此。

-- Martin Fowler

"...严格来说,根本不存在所谓的单元测试。"

-- Michael Belivanakis

单元测试是什么?

我在一个常用的搜索引擎上输入了上述问题,得到了以下结果(前三项)(加粗显示)

单元测试是用于验证应用程序中较小且独立部分(如函数或方法)准确性的代码片段。-- aws.amazon.com

单元测试是测试系统中的逻辑上可以独立隔离的最小代码单元的一种方式。在大多数编程语言中,这通常是指一个函数、子例程、方法或属性,这些通常是编程语言中的最小测试单元。-- smartbear.com

单元定义为系统被测(SUT)所表现出的一种单独行为,通常与需求相对应。尽管这可能暗示单元指的是过程式编程中的函数或模块,或面向对象编程中的方法或类,但这并不意味着函数/方法、模块或类总是对应于单元。从系统需求的角度来看,只有系统的外部边界才是相关的,因此只有那些对外部可见的系统行为的入口点才能定义单元。-- Kent Beck via 维基百科

在现代软件测试中,我经常看到的一个问题是,我们过于频繁地偏向于第二个定义:一个单元通常被认为是“系统中可以逻辑上独立的最小代码片段”。句子中的“可以”这个词在这里有很大的分量。实际上,我们几乎可以将任何代码片段逻辑上独立出来。

"一个数据单元是文件吗?"

你肯定也会说“绝对不可能”。

"假设我们有一个面向对象的程序,你们对类有什么看法?"

“大概不会。”你不太肯定地说。

"怎么样,试试这个方法?"

"-- 大概应该是这样," 带着更多的自信,认同上面提到的两个结果。

那个方法如果有300行代码会怎样?

啊,你还是把这拆分成几个小方法吧。

假设我们这么干。让我们将这个300行的函数拆分成10个每段30行的函数,正如一些计算机科学教授通常教授学生,这是关于函数长度的一个好建议。(参考 Reddit 上的帖子 [https://www.reddit.com/r/learnprogramming/comments/toynah/when_i_was_in_undergrad_they_told_us_no_function/#:~:text=In%20general%2C%20the%20most%20common,should%20only%20do%20one%20thing.] 中所说,函数应该只负责一件事。

    // 之前是
    def original(x: String, y: Int): Boolean = {

      // ...
      // 有数百行代码
      // ...

    }

全屏(点击退出)

// 之后
def 改进(x: String, y: Int): Boolean = {

    val 中间值A = a(x)
    val 中间值B = b(中间值A, y)
    val 中间值C = c(中间值B)
    // ...
    val 中间值H = h(中间值G)
    val 中间值I = i(中间值H)

    j(中间值I)

}

private def a(x: String): Long = {
    // ...
}

private def b(z: Long, y: Int): Double = {
    // ...
}

// 还有八个私有方法...

全屏,退出全屏

所有这些方法都可以是 private(或者用你所用语言中的等效词)。这样的话,它们只能被包含它们的那个类访问。这些方法原本就在一个方法里,所以我们可以确信其他人不会在别处使用这个逻辑。

但现在我们面临另一个决定:是否为这些单独的方法编写单元测试?很多人可能会想“当然了”。但这将来可能让重构变得更困难,因为“越接近实现的单元测试,越容易因改动而失效”。

实际上,我曾经在包含数百个测试的代码库中工作过,这些测试都紧密地与生产实现绑定在一起。只要给类添加一个字段,就需要更新一百多个并不关心这个字段的测试,因为这些测试也需要包含新字段才能编译通过。实际上,花在测试更改上的时间经常比修改生产代码还要多。

为这些较小的方法编写单元测试可能还需要我们将它们暴露得更public一些;外界只关心的是现在把这些方法整合在一起的那个被改进了的方法。

真正的问题是,这些功能模块是否可以算作代码的“单元”?

不行。

正如肯特·贝克所说的,这些不是“系统外部可见行为的入口”。

在上面重构的例子中,唯一的对外可见入口是 improved 函数,就像之前的 original 函数一样。但是这些贯穿始终的想法……

  1. 将大函数拆分成小函数,这样会更自然。
  2. 单元测试就是检查单个函数是否正常工作的测试。

...结合的结果比各个部分的总和还要糟糕得多:庞大的测试套件与过度公开的生产代码紧密耦合,编写费时很长,维护起来也很麻烦。

这样的情况让很多开发者觉得……之类的。

"大多数单元测试其实是在浪费时间"

James O. Coplien

测试类型

与其说是按照传统的方式去思考测试的角度,不如说从其他几个角度去思考会更有帮助。(https://qase.io/blog/test-pyramid/

  1. 这个测试是快还是慢?
  2. 这是黑盒测试还是白盒测试?
  3. 这个测试是基于开发的,还是为开发提供信息的?

快测试和慢测试:

让我先说清楚,在这种情况下,“快”并不一定代表“好”,“慢”也不一定代表“坏”。

快速测试是指能在几秒、甚至毫秒、微秒或更短时间内完成的测试。所以,快速测试必须完全在内存中运行。它们不会进行磁盘读写或网络请求。每次代码更改后都可以立即运行这些测试,而不影响开发进度,因此建议将其纳入开发人员的内循环中。每次编译时都可以运行这些测试。

慢测试可能需要几秒钟、几分钟甚至几小时才能运行。快速测试和慢速测试之间的界限大致在2到5秒左右。慢测试可能需要从磁盘读取大量输入文件、进行大量计算,或者在网络上传输数据。也就是说,它们可能是在[IO、CPU或网络]方面资源消耗较大的(https://stackoverflow.com/q/868568/2925434)。例如契约测试(通常会启动Docker容器进行测试)和性能测试(可能需要处理数GB的数据或数千次请求)都是慢测试的例子。这些测试应适当减少运行频率,因为它们可能会影响开发速度:对于几分钟以内的测试,每次提交到`main/master`分支之前运行可能是合适的;而对于时间更长的测试,每天或每周运行一次可能是比较合适的。

黑盒测试和白盒测试

黑盒测试不假设被测试对象的内部结构。它们只提供输入并检查输出结果,就是这样。可观察的输出通常是一个方法的返回值,但黑盒测试也可能检查是否产生了副作用,比如日志是否被记录、指标是否被更新,或者状态是否发生变化。

白盒测试特别测试被测试对象的内部。它们是具有内省性质的。具有“当'x'发生时,函数a()应该调用函数b()”这类断言的测试就是白盒测试。它们明确地测试事情应如何发生(一些代码应如何实现),而不是仅仅测试它是否已经实现。依赖大量模拟框架的测试往往是白盒测试,断言某个方法是否在收到某些输入后已被调用的测试。

如果你不关心它是如何实现的——只要它能做它该做的事——你应该写黑盒测试。这种情况通常如此,所以默认情况下最好选择黑盒测试。

开发导向的测试与测试导向的开发

开发知情的测试是响应性地编写,也就是说,首先是编写生产代码,然后才编写测试。开发知情的测试体现了系统当前的行为。传统的“单元测试”几乎都是开发知情的测试。

开发告知的测试是先编写的,即先编写测试,然后编写生产代码。测试驱动开发(TDD)是一种软件开发方法,鼓励仅编写开发告知的测试,确保系统的所有行为都被100%的测试所覆盖。

开发相关的测试也可以提供信心,证明一些棘手的逻辑已经被正确实现。比如说,你可以编写一个regex来解析美国电话号码,同时添加一些测试以确保能够捕获诸如有效的号码、无效的号码、格式错误的号码等。

  • 用括号括起来的区号
  • 空格、无空格和连字符的使用方式
  • 是否有+1国家代码

仅仅通过查看正则表达式,很难确定它是否涵盖了所有情况。通常,编写几个简单的测试来确认最常见的特殊情况已被正确处理更有说服力的做法。

我一直以一种能帮助开发的方式写修复bug的测试。首先,我编写一个本来应该通过但因为存在bug而预期会失败的测试。然后,我在生产代码中修复这个bug,确保修复后测试可以通过。这样表明——如果这个测试最初就存在的话——它本来可以抓住这个bug。这让我们有信心,这个bug不会再出现了。

"大多数单元测试其实是在白费"

以上提到的三种看待测试的方式可以提供一些见解和理解,解释了为什么James O. Coplien这样的开发者认为大多数单元测试被视为浪费时间。

大多数单元测试都是由开发过程引导的

在我的经验里,据我所知,TDD 并不是被大多数开发者所采用的来说。

因此,大多数测试是由开发过程驱动的。开发人员编写一些生产代码,然后编写测试,通常是为了确保达到一个合理的代码覆盖率。代码覆盖率标准

这些测试并不是为了发现错误而写的,也不是为了帮助开发者思考一些棘手的实现问题而写的,因此它们的价值可能不那么容易被理解。

大多数单元测试并不测试系统对外的可见行为

如前所述,将大型函数拆分成更小的函数,并为每个 函数 而不是为每个 外部可见的行为 编写测试,会导致许多紧密耦合于生产实现的测试。因此,这些测试本质上是脆弱的。每当实现细节有任何细微变化,即使系统的外部行为保持不变,这些测试都需要更新。

这通常发生在使用模拟库时,因为对模拟对象调用的每个方法都必须明确,并指定返回值。

在最坏的情形下,开发人员有时会将生产代码复制粘贴到测试中,断言测试中预期的结果等于生产代码中的实际结果。这种白盒测试毫无价值,尽管这确实增加了代码覆盖率。

新的测试框架

传统测试金字塔旨在向开发者强调应该主要编写“单元测试”,再少一些的“集成测试”,以及更少的“端到端测试”。虽然金字塔的不同版本可能会使用不同的术语来指代后两个层次,不过几乎所有定义都认同金字塔的底部应该是“单元测试”。Google建议将“单元测试”、“集成测试”和“端到端测试”的比例设定为70%、20%和10%。

想法在于,你需要用一些小且快速的测试来覆盖代码逻辑的主要部分,这些测试可以在开发过程中的内循环(例如,见此处)中反复运行。你的集成测试应该覆盖各单元之间的交互;而端到端测试则要确保用户操作能够达到预期的整体效果。

这条建议是可以的,前提是所有开发者对“单元测试”和“集成测试”的定义达成一致。很明显,实际情况并非如此(请参阅本博客帖子顶部的搜索引擎结果)。然而,我们可以根据这些客观标准(快速 vs. 慢速,黑盒 vs. 白盒,对开发有指导 vs. 被开发影响)来构建一个新的测试金字塔。

如图所示,新的测试层级
点击链接查看图片

基础

尽量选择黑盒测试。当需要外部依赖时,优先使用伪造实现(如 模拟实现),而不是模拟对象(参见 这篇文章),并添加相应的契约测试以确保外部依赖按预期工作。这将使整个测试都在内存中运行,从而足够快,可以在每次提交到 mainmaster 分支之前运行。你会发现大多数测试都是这些快速的黑盒测试。

请注意,这等同于“unit test”。如前所述,传统的“unit test”通常较快,但有时会是白盒测试,并且往往受到开发过程的影响。

中间部分

更偏好指导开发的测试,而非开发驱动的测试(更偏好使用TDD风格的开发方式)。测试往往机械地编写,提供很少的价值。

更倾向于慢速的黑盒测试,而不是快速的白盒测试。前者更容易维护,因为与实际生产代码的依赖性较低。

传统的“集成测试”和“端对端”测试都归类为“慢速黑盒测试”。

尽量少写白盒测试代码。换句话说,尽量少做代码内部的审查。仅测试可观察的输出,而不是内部实现细节。

仅在必要时才编写为开发编写的测试。如果生产实现没问题,就说明它有效运行。如果不行,你就会发现一个 bug,编写一个为开发编写的测试,并修复该 bug。这个过程就像之前说的。

最后

或者

结论

传统的单元、集成和端到端测试的分类并不明确。对什么是“单元测试”的不同理解导致了混乱,加上出于好意但不当的建议,减少每个函数、类等的行数以提高代码可读性,导致出现了一些难以维护且价值不高的测试套件,这对开发人员的生产力造成了负面影响。

根据上述三个标准对测试进行分类,有助于创建更易于维护的测试用例,从而提供更多价值。

點擊查看更多內容
TA 點贊

若覺得本文不錯,就分享一下吧!

評論

作者其他優質文章

正在加載中
  • 推薦
  • 評論
  • 收藏
  • 共同學習,寫下你的評論
感謝您的支持,我會繼續努力的~
掃碼打賞,你說多少就多少
贊賞金額會直接到老師賬戶
支付方式
打開微信掃一掃,即可進行掃碼打賞哦
今天注冊有機會得

100積分直接送

付費專欄免費學

大額優惠券免費領

立即參與 放棄機會
微信客服

購課補貼
聯系客服咨詢優惠詳情

幫助反饋 APP下載

慕課網APP
您的移動學習伙伴

公眾號

掃描二維碼
關注慕課網微信公眾號

舉報

0/150
提交
取消