是时候谈谈Go的测试了

本篇内容是根据2019年4月份#83 It’s time to talk about testing音频录制内容的整理与翻译

测试是一门艺术还是一门科学?我们应该测试什么以及何时测试?测试的意义何在?测试会不会太过分?我们将在这一充满测试的剧集中探讨所有这些以及更多内容。

过程中为符合中文惯用表达有适当删改, 版权归原作者所有.



Mat Ryer: 大家好!欢迎来到 Go Time。我是 Mat Ryer。今天我们要讨论的是测试,和我一起的是来自 Go 社区的几位思想者和实践者。他们是 Johnny Boursiquot、Jaana B. Dogan 和独一无二的 Jon Calhoun。大家好!

Jaana Dogan: 大家好!

Johnny Boursiquot: 大家好!

Jon Calhoun: 你好,Mat!

Mat Ryer: 今天感觉怎么样?

Jon Calhoun: 感觉不错。

Johnny Boursiquot: 没什么可抱怨的。

Mat Ryer: 很好。如果你们有抱怨的事情,别找我,我帮不上忙… [笑声] 尤其是涉及到医学问题的时候。好吧… 测试是一个非常重要的话题,所以我们能做一集关于它的节目真的很棒。我注意到,每当我谈论测试时,总会引发很多争论,我一直在想这是为什么。

有些人说 “TDD 已死”,而另一些人则倡导它……各种不同的观点。我觉得这说明了测试本身的一些问题,希望我们今天能揭示出来……但或许我们可以先从谈论我们喜欢的关于测试的事情开始,因为测试中肯定有很多积极的方面,尽管也有很多负面的看法。

首先,假设有一些刚开始学习 Go 的人,他们还没有做过太多的测试……那么测试的意义是什么?我们为什么要做测试?我们希望从中得到什么?

Jaana Dogan: 对我来说,这是理解自己的一种方式,尤其是在长期项目中,同时也是一种表达我的代码应该做什么的方式。它有点像一个总结,你只是在解释它应该做什么,并且通过这种可重复的方式来检查它是否真的在做那些事情。

Mat Ryer: 是的,你希望用某种方式来描述代码本身之外的东西,描述你做出的承诺。

Jaana Dogan: 其实这是某种自我解释的代码。当你说它不是一种编码方式时,我以为你是指某种规格说明,类似于 “嘿,这个项目应该这样表现”,更像是一种规范。但测试本身也是一种规范,同时它也是代码。

Mat Ryer: 是的,它是代码。所以从非常实际的角度来看,在 Go 中,如果你编写了一个函数,比如说,它是一个问候函数;它会接受一个名字,并对这个名字说 “你好”,然后返回一个字符串---你可以编写一个测试,传入一些名字,并确保返回的结果与预期一致。这本质上就是单元测试。而它为你提供的就是,你可以看看测试代码,一目了然地看到你做了哪些承诺,或者你的函数应该做什么。

当然,良好测试和良好实践的目标之一是让这些测试持续运行,这样你至少可以假设它们都通过了测试,这样我们就能有一个良好的感觉。如果你在查看一个新的包,它是一个很好的方式来了解如何使用这个包。你可以去看看测试代码。

Jaana Dogan: 是的,完全正确。

Johnny Boursiquot: 接着 Jaana 刚才提到的内容,对我来说,测试在项目进展过程中会有不同的意义。一开始,我试图通过测试来思考我要解决的问题,并基本上说 “好吧,让我建立一些关于我期望它做什么的规范。” 但随着项目的进行,功能的增加或移除,测试变成了一种护栏;它们确认了我所做的更改。我假设的那些依旧成立的事情,在软件的演变过程中依旧保持不变,所以测试在项目演变的过程中,意义略有不同。

Jon Calhoun: 与此相关的是,我认为测试确实让你有机会澄清一些事情。一个很好的例子是,当我玩桌游时,我的朋友们总是开玩笑说我是那个会想到奇怪边界情况并想知道规则中是否有任何解释的人……但测试用例是一个很好的方式来说明 “如果你给我这个奇怪的输入,这就是你会得到的结果。”

Jaana Dogan: 这也是为什么覆盖率非常重要的原因,因为你通过覆盖这些情况来基本上声明你的期望……有时我看到人们忽略了一些测试用例,认为 “也许这个包或这个函数不应该这样表现,因为测试中没有体现。” 这就是我认为覆盖率很重要的原因之一。

Mat Ryer: 是的。我也认为人们很容易对覆盖率有点过于痴迷。我见过一些人,他们对自己有 100% 的代码覆盖率非常自豪。而在 Go 项目中,这意味着他们以某种方式测试了所有可能导致代码出错的地方……

Jaana Dogan: 是的。

Mat Ryer: ……他们为此提供了测试覆盖率。

Jaana Dogan: 我会说,对于动态类型的语言,100% 的覆盖率绝对是必要的。这是基础要求。 [笑声] 对吧……?但 100% 的覆盖率并不一定意味着这是好的覆盖率,或者它一定很重要。

Jon Calhoun: 我认为重要的是要理解覆盖率实际上意味着什么。正如 Mat 所说,如果你只是说你覆盖了所有的代码---这真的是 100% 的覆盖率吗?因为你并没有尝试每一个整数输入,也没有尝试每一个字符串输入……所以你可以在某种程度上覆盖了所有的代码,但这远非真正意义上的 100% 覆盖率。

Mat Ryer: 是的。

Jaana Dogan: 这也意味着你需要一些模糊测试,或者你需要通过不同的输入来强制测试所有可能性。你需要更聪明一些;也许你无法手动生成所有这些测试用例,对吧?

Jon Calhoun: 是的。至少对我来说,我宁愿与一个有 75% 覆盖率的代码库合作,但这些覆盖率是经过认真思考的并且是有价值的……而不是有人对我说 “嘿,我有 100% 的代码覆盖率”,但它只是简单的、非常基础的输入测试,实际上并没有真正测试当事情变糟时代码是否仍然能正常工作。

Johnny Boursiquot: 对,测试的"顺利路径"和其他所有路径,对吧…?

Jaana Dogan: 是的…

Mat Ryer: 但是,如果你确实有 100% 的代码覆盖率,你的测试是否会过度拟合测试代码?本质上,你的实际代码、你的程序代码变得非常脆弱。每次更改,每次实现的微调都可能导致这些测试失败,因为它们的覆盖率达到如此程度。因此,从这个角度来看,我认为测试可能会过度。我认为很多人可能确实会这么做。我过去肯定也有过这种情况,对我来说,这是在获得测试带来的好处与不过度测试之间找到平衡。

Jaana Dogan: 但这不也是一个关于兼容性的问题吗?如果你做出了行为承诺,比如说,你实际上可能希望能够覆盖这些细微的细节,这样你就不会在不知情的情况下改变行为---我的意思是,如果你的行为发生了变化,你应该被告知。

Mat Ryer: 是的,我想这是个很好的表达方式---它关乎你做出的承诺。这也是我认为很难笼统地讨论这个问题的原因,因为我认为这将随着你构建的东西的类型而变化。如果你在构建一个二进制编码器/解码器,那么测试覆盖率和其他方面会与构建一个邮件发送服务完全不同……你不觉得吗?

Jaana Dogan: 是的,确实如此。

Jon Calhoun: 是的。即使你在谈论一个网站,技术上来说,要实现 100% 的覆盖率,感觉像是 “好吧,如果我的硬盘坏了会发生什么。” 你可能需要模拟所有这些奇怪的情况……但这真的值得去做吗?与其说是一个编码器或其他更小的东西,你不需要考虑所有这些问题。

Jaana Dogan: 是的,我认为单元测试与集成测试是一个巨大的话题。而且运行某些集成测试非常复杂,以至于人们更愿意去 Canary(灰度测试环境)并尝试在生产环境中查看……因为环境和所有的粘合部分真的非常复杂。

Mat Ryer: 是的……所以我们应该花一点时间来讨论一下单元测试和集成测试之间的区别,以及其他类型的测试。无疑,单元测试是最容易理解的,因为它是最简单的测试类型。你有一个函数,通常来说……Dave Cheney 在最近的伦敦 Go Meetup 上做了一次演讲,他提出单元实际上应该是包,并且测试应该在这个边界上进行……他在这方面提出了一些非常有趣的观点。

但本质上,无论你测试的是什么单元---这就是单元测试;它是最小的部分。你编写测试代码,它运行实际代码,并检查输出。这些是单元测试。那么有人能告诉我们,集成测试是什么吗,如果单元测试是这样的?

Johnny Boursiquot: 我听过关于集成测试的不同定义。几乎每次当我开始谈论这个问题时,我都必须先给出一个前言,像是 “当我在这个项目中谈论集成测试时,我指的是这个。” 但基本的想法是,给定一个解决方案,给定一个解决某个特定问题的软件,解决方案中的不同组件是否很好地集成在一起,以便可以解决这个问题?这些组件能否很好地协作,真正提供解决方案?

你可以只有两个组件,也可以有十几个组件,甚至上百个组件---这无关紧要。所有这些在你的软件解决方案边界内的部分,是否能够彼此通信?

当然,也有人认为 “集成测试意味着你要超出当前软件的边界,超出当前的代码库或项目……现在,它能与外部服务进行通信吗?比如数据库?它能与其他 API 进行通信吗?” 你几乎要明确你所谈论的集成测试的含义,但至少在我看来,它是指在你当前的项目中,所有的部分是否能够很好地配合工作。

Jon Calhoun: 接着这个话题,我听过的一种描述方式有时很有帮助,就是你可以把它看作是 “我正在测试这个包/组件/它与其他东西的交互,并且假设我不会更改那些代码。因此,我必须确保我的代码可以与那些东西正确协作。”

Jaana Dogan: 是的。我认为你在运行集成测试之前会先运行单元测试,因为你首先要确保模块本身是正常运行的,而集成测试只是检查它是否能够与其他模块一起正常工作,对吧?

Jon Calhoun: 是的。我一直喜欢举的一个例子是,你可以为连接到 Stripe API 编写一个单元测试,测试 “只要它返回这个响应,我的代码就能正常工作”,但你实际上并没有验证 Stripe 是否真的返回了那个响应。所以,如果我们假设在集成测试定义中允许你与第三方服务通信……集成测试可能就是那个实际与 Stripe 通信并验证 “我真的得到了预期的响应” 的测试。这两者有着不同的目的,这有助于澄清它们在尝试做什么。

Jaana Dogan: 集成测试实际上非常有趣,因为虽然它们被归类为功能测试---功能测试的意思是你实际上在查看系统是否按预期运行,但它也非常依赖于那些外部服务的可靠性以及所有内部不同模块的可靠性。所以它有点介于功能测试和非功能测试之间。

Jon Calhoun: 我认为这就是为什么它如此困难---每个公司根据他们依赖的东西都有非常不同的集成测试理解……

Jaana Dogan: 是的… [笑]

Jon Calhoun: 如果你考虑像谷歌这样的公司,当他们发布一个东西时,规模与我把一个东西上传到一个小型 Linode 服务器 (译者注: 一家VPS提供商)上是完全不同的……那是一个非常不同的体验,和谷歌发布一个东西的体验完全不同,所以集成测试也会大不相同。

Jaana Dogan: 是的。即使在大型公司内部,也有不同的问题解决方法……这真的取决于项目和问题的具体情况……在这个话题上很难给出一些通用的建议。

Mat Ryer: 是的,完全正确。

Johnny Boursiquot: 这个话题很有趣---基本上,当我知道我要集成第三方解决方案时,我会确保在单元测试时不会真的触发那些外部实体,不管是API还是数据库。当然,有些人会主张真的去测试这些外部实体,但通常情况下,我会选择使用模拟(mocking)或替代(stubbing)的方式。不过,这里需要找到一个平衡。我有几次因为过度使用模拟或替代而吃了亏,幸好有时是在集成测试时发现问题,当我与那些服务交互时……但最糟糕的情况是,我在运行时才发现问题,比如在预生产或生产环境中,发现我假设某些接口会返回的响应并不正确,或者某些东西在幕后发生了变化。这时发现问题已经太晚了。所以这里需要找到一个平衡点。

很多人支持使用模拟和替代;老实说,我现在越来越少使用这些方法了,更倾向于像Jaana刚刚提到的那样,进行灰度发布(Canary部署),看看它在实际的流量下是否表现如预期。基本上,我试图减少对构建系统的虚假模拟,直接用真实流量测试它的表现。这比你在周围创建的所有模拟都要更真实地反映软件的实际表现。

Mat Ryer: 我知道Monzo银行也是这样做的。Monzo是一家位于伦敦的银行,他们的系统是用Go写的……据我所知,他们也是这样做的。他们使用测试卡,自动化测试系统可以在实时系统上执行,模拟真实的用户使用他们的卡、转账,甚至是执行他们支持的所有功能……这些测试可以在生产环境中连续运行,一旦出现任何问题,你会很早就能察觉到。

Jaana Dogan: 对。我见过一些案例,他们会复制部分实际的流量,并将其转发到测试环境中,看看在可靠性或性能方面是否与真实使用情况一致。他们通过这种方式进行一些性能测试,因为他们需要模拟看起来像真实使用的场景。他们要么复制实际的流量,要么查看事件日志,并尝试复制某个时间段,比如5分钟的真实流量,然后运行他们的测试。

Mat Ryer: 我曾经构建过一个系统,记录了真实的HTTP流量并保存下来,这些数据就成了测试文件。

Jaana Dogan: 对。

Mat Ryer: 我还在Go的包中见过类似的东西---我记得他们叫它“黄金文件”(golden files)。

Jaana Dogan: 对,没错。

Johnny Boursiquot: 对。

Mat Ryer: 你可以带着一个标志去运行测试,它会实际触发真实的服务并将结果保存到测试文件中。然后在之后的测试中,或者当你带着“short”标志运行时,你可以假定结果和上次实际获取的数据一样。这是一种很好的方式,虽然并不完美,但它依然能提供一定程度的信心,是值得拥有的。

Jaana Dogan: 没错。

Johnny Boursiquot: 对于那些刚接触Go并且想了解如何在Go中进行这些类型测试的人来说,有什么技术建议可以推荐给他们呢?举个例子,假设你不希望每次进行单元测试时都运行集成测试,因为集成测试耗时较长,或许你可以在测试的名字中加入“integration”(集成)的字样,然后在命令行运行Go测试时,使用工具链传递标志;比如你可以传递一个名字标志(name flag),或者更准确地说是“run”标志,指定要运行包含“integration”的测试。这样,你可以根据需要在不同时间运行不同类型的测试。这样你就不必每次运行测试时都运行所有的单元测试。这就是一个例子。

Jaana Dogan: 我有个问题。你是把集成测试像单元测试那样写成Go测试,还是你更倾向于有独立的main二进制文件,然后设置好整个环境再运行测试?

Johnny Boursiquot: 这完全取决于项目的规模。如果项目足够大,我们可能会有一个完全独立的系统来执行这些操作,而不是说“好吧,这是一个小型的微服务,它只做少数几件事,我们只需要确保它能与其他服务通信或执行某些功能。”但确实,你可以用几种不同的方式来做。

Jaana Dogan: 对。

Jon Calhoun: 我认为这也取决于你正在处理的项目,就像你刚才提到的那样……比如Mat之前讲过的,如果你在写一个API客户端并试图测试它与端点的交互是否正确,一个简单的方法来判断是否需要进行集成测试就是看它们是否提供了API密钥;如果有很大的几率你是在测试实际的API,而如果没有,你可以假设你的测试并不是在测试真实的API。因此,正如你刚才提到的,你可以传递“run”标志;你还可以传递一个标志,表示提供了API密钥,或者使用我喜欢的构建标签(build tags),但这稍微有点不同。

Jaana Dogan: 但我喜欢查看环境变量并跳过一些测试。我通常会写一个实用函数……我有一个测试包,里面包含一个实用函数,如果某个变量没有设置,它就会自动跳过测试……所以这是我在每个测试中都试图遵循的一种常见方法。我会查看环境变量,如果有足够的凭证或其他信息,我就会运行集成测试。如果有一个特定的环境变量表明“运行集成测试”,我就会执行这些测试。否则,那个函数会跳过测试,我会在所有测试函数的开头调用这个函数,这样我就不用每次都考虑这个问题了。

Mat Ryer: 是的,当然我们的目标是经常运行单元测试。你希望能够持续运行这些测试。我甚至设置了保存文件时自动运行该包的测试。但你也希望测试能快速完成,因为你需要即时反馈。使用Go进行单元测试时,你确实能得到这种快速反馈。当然,集成测试通常较慢,我认为这就是我们不经常运行它们的原因---你不想影响每个人的开发进度……同时你也希望鼓励大家运行测试,因为如果你不关注代码,它就会开始“变质”。这是我成为软件工程师后感到惊讶的一件事---我以为如果你不动代码,它就不会改变……但事实并非如此。如果你不看它,当你再运行时,一切都会出错。所以我们要鼓励大家多运行测试。

Jaana Dogan: 没有独立的代码,对吧?所有的依赖都会改变,即使你不改变代码。不存在不会改变的代码……除非你写的是操作系统并且从硬件到软件一手掌控,而且硬件永远不会改变。那样的话,我们可以说代码不需要改变。

Mat Ryer: 是的,但我们还得应对那些不稳定的测试……我一直认为测试是很科学的事情。就像是一个科学方法---我们设置一个环境,做出一些假设或断言,然后重复执行相同的步骤,进行测试……它感觉非常科学。然而,实际上有时测试会偶尔失败。如果你对项目的测试级别掌握不当,问题就会显现出来。我觉得这很深刻;如果你掌握错了……

Jaana Dogan: 你说科学,是指测试结果应该是一致的、可重复的吗?因为我认为处理不稳定测试是一门不同的“科学”。人们为此做了大量的统计分析,试图弄清楚“什么是一个不稳定的单元测试?哪些问题是真的需要关注的?”这已经是一个巨大的话题。

Mat Ryer: 是的,这确实很难。我只是无法相信,有时测试通过了,而有时它们却失败了。所以对于初级开发者,或者刚开始接触Go编程的人来说,当你遇到这种情况时---是的,这很奇怪,但它确实会发生。

Jaana Dogan: 是的。

Jon Calhoun: 我曾在一家公司工作,每到月底的24到48小时内,有些测试就会莫名其妙地失败……这是因为有人在处理时间时做了些奇怪的事情,我已经不记得具体是什么了,但这真的是最令人沮丧的事情。每到这两天,大家基本上都会忽略那些测试,然后继续提交代码……就像“我们到底在干什么?!” 之后的两天大家再回过头来尝试修复所有问题,并且避免发布任何东西。

Jaana Dogan: 是的,这是最危险的地方。如果有至少一个不稳定的测试,人们就会学会不再查看测试结果,完全忽视测试的存在。这就是为什么解决不稳定测试非常重要。

Mat Ryer: 这个观点非常好。这确实是常见情况。如果某个测试总是很烦人,作为开发者,你还要做实际的工作,没有时间去修复那些你没有上下文的测试。所以是的,这种情况下很常见的结果就是你停止了测试,甚至停止写测试。测试太混乱了,根本不起作用;于是你干脆把它们注释掉。这在现实中确实会发生。

Jon Calhoun: 这是我见过的公司减少测试量的头号原因---测试不可靠,所以人们不再相信它们,最终导致不再编写更多的测试,因为觉得没有意义。

Jaana Dogan: 当你遇到一个非常不稳定的测试,而且不容易修复时,你怎么办?我知道如果我们在测试套件中保留它,它会损害测试文化。你会暂时移除它直到修复为止,还是保留它,并告诉大家要小心,因为仅仅因为一个测试不稳定并且经常失败,并不意味着我们不应该重视测试?

Jon Calhoun: 我一直在尝试的一种方法---我还不确定我对它的感受如何---我主要是在小团队中开发。如果我看到这样的测试,我通常会使用构建标签(build tags)为测试添加标记;我会为集成测试创建一个构建标签……我会把不稳定的测试放在一个“flaky”标签中,基本上是说“这些测试是已知的不稳定测试”,这样人们知道如果没有运行这个标签,就不应该忽视测试结果。但如果运行了这个标签,那么可能是因为它不稳定。至少这样的话,大家知道什么时候可以忽略某些测试。

Jaana Dogan: 这是一个很好的方法。

Mat Ryer: 但是,测试不稳定的原因有哪些呢?

Jaana Dogan: 想象一下,有一个并发问题,而你没有API来控制并发,也没有确定性……你无法模拟某个特定的案例,或者其他情况。可能有多种原因,而你可能不一定知道现在该怎么做,但是你可能需要一周的时间来编写一个合适的测试来替换那个不稳定的测试……所以你可能暂时至少会标记它为不稳定测试,但仍然保留它。

Mat Ryer: Jaana,你会为了让测试变得更容易而改变设计吗?

Jaana Dogan: 你是指……指什么? [笑]

Mat Ryer: 比如,如果某个东西是非确定性的,你是否会通过某种方式改变设计,或者重新设计那个组件?

Jaana Dogan: 是的,绝对会。你完全可以重新设计。但想象一下你没有权限更改……比如说,假设我们在讨论并发问题。你有一个并发的播放调度器,你想测试它,但它没有提供合适的API,因此你无法创建一个临时的……你无法拦截它,无法改变调度器的行为,也无法停止调度器来检查沿途是否一切正常……所以你的选择非常有限。你要么不测试这个场景,要么请求他们提供一些钩子,以便你可以让调度器可测试……但如果你真的受限于测试这个场景,并且没有确定性的方式来测试这个组件,你该怎么办?

Mat Ryer: 这种情况以前也发生在我身上,真的很麻烦。我们曾经做过的一件事---我并不推荐,但它确实有用……我的免责声明是,开发者会为了让事情正常工作而做任何事情。我会在循环中运行测试,因为它大多数情况下是正常的。所以我会让它运行四到五次,如果每次都失败,那就是失败;如果有一次通过了,我就知道它是可以的。

Johnny Boursiquot: 哇哦。

Mat Ryer: 但这感觉不太好。我不会因此回家,想着“今天我在计算机领域表现得太棒了。” [笑声]

Jon Calhoun: 可以把这个写进你的简历里……

Johnny Boursiquot: 不择手段,确保测试通过。 [笑声]

Mat Ryer: 是的……但这就是态度。我们必须记住这一点。这就是人们在做的事情---他们有其他的目标要实现,所以他们会不惜一切去做。这是很自然的,我们作为个体,应该去抵抗这种趋势。但是,任何开发工具或者设计框架的人,应该牢记这一点。

Jaana Dogan: 我觉得我们在设计任何东西时,关于可测试性方面做得都很糟糕。我们的 API 设计---我们之前有一期节目讨论了 API 设计,但是我们从没谈过可测试性……但实际上,我们很多时候都在优化可用性。至于可测试性,它总是排在第二位,最后我们要么是有不稳定的测试,要么是不可测试的代码,或者是过度模拟的情况……这都不是很好。

Johnny Boursiquot: Jaana,你是指,比如你编写了一个 Go 包,然后为使用你包的用户提供了一些测试功能。我们不是在说你为自己功能编写的内部测试,而是为使用你包的其他人提供测试工具。

Jaana Dogan: 完全对。或者是以某种方式设计 API,使其可以被模拟或测试。记住用户需要能够测试某些东西是一个很好的练习,但我们在设计 API 时并不总是关心这个问题。

Mat Ryer: 这很有趣,因为当我们做 Machine Box 时,这就是我们经常讨论的一个话题,我们确保用户能够轻松地测试这些东西,或模拟它们,或者他们想做的任何事情。我认为这应该是一个一等公民的考虑因素。当然,如果你在生成代码,或者有其他机制---每个项目都不一样---持续思考这个问题很难……但 TDD(测试驱动开发)帮助我做到这一点,因为我通常会将我的代码放在一个不同的测试包中进行测试,这样我就必须确保它是可测试的。所以本质上,TDD 帮助我实现这一点。

有时我会注意到我们重复做的一些模式,我会把它们写进文档,或者甚至创建一个小的子包,或者在这个包里创建一个小包,提供一些常见或简单的测试功能,直接给用户。我很喜欢这样做。

Jon Calhoun: 在这样做的过程中,你有没有注意到什么可以使你的包更难测试的设计?有没有什么特别明显的东西?

Mat Ryer: 是的。任何形式的全局状态都会让你不得不关心---你不能随意地运行你的测试。我喜欢能够以任何顺序运行单元测试,也喜欢能够一次只运行一个测试,尤其是在我只关注某个小问题时。如果你有状态存在,那么你就必须关心它。有时你会得到一些小的 DSL(领域特定语言),你可以说“好的,作为这个用户登录,创建这些东西,然后执行这个操作”,然后你做真正的函数调用,或者调用 API,之后你可以检查并确保事情按预期发生了。但这对单元测试来说是很多工作。不过,只要它运行速度快,我还是会这么做。

还有什么难测试的呢……并发在 Go 中很难测试,但这取决于情况……我认为有时你必须相信自己。我们希望这些项目有很好的测试覆盖率,并且我们可以有信心把它们发布到实际环境中,或者发布代码并知道它会工作……但如果你有一个选择块(select block),很难测试,留下一部分未测试的代码有什么坏处呢?我们可以依赖这样一个事实:如果那个东西不工作,几乎没有什么会工作,所以我们基本知道它是工作的……这样的想法能让你夜晚睡得安心,还是会让你担心呢?

Jon Calhoun: 我更愿意放过这类东西,但我也处在一个与大多数人非常不同的环境中……就像我不在 Google 工作,也不处理数十亿美元的业务。如果我在做那样的工作,可能晚上会有点害怕。

Mat Ryer: 你所处的环境、工作项目的性质---所有这些可能都会影响到这点,对吗?

Jon Calhoun: 我肯定会这么说。因为如果你想想看,如果你写的是一个小商店,每月能赚 100 美元,那么如果你有个小 bug,最多也就是这个月损失 100 美元……但如果你在 Google 工作,可能因为你没有测试好某些东西而损失数百万美元。

Mat Ryer: 所以这里有一个风险评估的因素。

Jaana Dogan: 是的。

Mat Ryer: 你必须在测试时评估风险……因为这确实需要付出努力。我听到的一个理由是人们不喜欢 TDD,因为它让开发速度太慢。对我来说,现在已经不是这样了。TDD 让我更快,因为它给了我专注力,并且加快了我写代码的速度……因为如果我写错了,我很快就能看到它的错误。实际上,这让我跑得更快。

Jaana Dogan: 我有一个关于 TDD 的问题。我确实认为如果你有一个非常明确的规格,TDD 是一个很好的选择;比如你有一个要实现的编码/解码规范,里面有清晰的案例,那很棒。但如果你是在写一个和其他五个服务器通信的网络服务器,可能就不是那么理想的起点。所以你怎么看?你认为 TDD 适用于很多不同的问题,还是仅仅适用于那些规范非常明确且自包含的情况?

Jon Calhoun: 我觉得另一点是,这取决于你对 TDD 的定义是什么。因为我听过有些人说 TDD 就是写最少量的代码……你先写测试,然后写最少的代码让测试通过,但我不是那样工作的。那不是我的工作方式。

Mat Ryer: 我觉得那是极端 TDD。这是原教旨 TDD,我想。

Johnny Boursiquot: 你刚刚创造了一个新术语吗,“极端 TDD”?X-TDD。 [笑声] 其实 TDD 的争论已经持续很久了。我通常对我的团队说:“我不特别在意你在解决问题时是否遵循 TDD;只要你提交的代码或者拉取请求有测试,我就满意了。”所以回应 Jaana 的观点,当我在实验时,当我还不确定这个东西的形状时……我不会先写测试。不过,这不意味着这是对的或错的……这只是某个开发者的工作方式。我们每个人的思维方式不同,因此我们不必把 TDD 当做福音。

我们不必对它过于教条。最终的目标是“开发者所提出的解决方案是否可以验证?”对我来说,这就是测试的价值所在。你告诉我,作为开发者,你的解决方案是正确的,而这些测试证明了这一点。这是测试的最终价值。当其他人接手并开始修改代码时,他们将确切知道期望是什么,如果测试失败了,他们也知道需要检查哪里来修复。这就是测试的最终价值。我不在乎你是怎么写的,只要你写了测试就好。

Mat Ryer: 这很公平。不过我要说的是,给已经写好的代码写单元测试是我最痛苦的事情之一。 [笑声] 在开发过程中写测试---那是完全不同的体验。最终结果可能是一样的。如果你做得好,TDD 通常会更好……但你说得对,不能走得太极端,但确实有些情况下,这确实是构建某个东西的正确方式。

你知道的,当你在构思一些想法时……你只是想打开笔记本,随手写点东西---那对我来说不算,这只是过程的一部分,但你并没有真的在实现那个生产级的东西;那确实是一个不同的模式。不过即便在那些情况下,如果我在写一个 Go 包,我个人肯定会从测试开始,因为我会成为那个包的第一个用户,我觉得这是写代码和构建包的正确方式。

Jaana Dogan: 你是在 API 设计之前还是之后这样做的?我只是想知道先写测试的开销有多大……因为即使我会采用 TDD 方法,我也会先完善我的 API 设计,然后把我的 API 放在主包里,再创建一个测试文件,写一些测试并开始实现功能。

Mat Ryer: 我可能不会完全照你说的那样做,但说实话,我不会在意这些细节,因为我觉得 Johnny 说得对---只要最终结果是一样的,我觉得就没问题。如果我有一个 Go 包的想法,我会从测试开始,假设这个东西已经存在,然后开始使用它。这是我大多数时候做 API 设计的方式……我会说“哦,好吧,现在我调用这个包……我需要能够设置一个 HTTP 客户端,所以我该如何在设计中实现这一点?”然后我就会尝试成为用户。

我觉得即使人们在其他地方草拟代码,如果你试图成为用户,这会有所帮助;我觉得这是重要的事情。因为我们真正是在为用户构建这些东西。

Jaana Dogan: 我觉得我的方法类似,但它真的很有限;它并没有覆盖所有的测试。我称之为“示例驱动开发”。我会从 Go 的示例代码开始,这样我就可以感受到用户的使用体验,我只会为三个主要案例写示例。它帮助我塑造 API。

Mat Ryer: 是的,太棒了。

Johnny Boursiquot: 这很有趣,因为显然我们每个人都有稍微不同的方法……我也做类似的事情,不过我用的是 readme 文件。我基本上会说,如果我进入这个项目并开始查看 readme,告诉我如何使用它;告诉我该期待什么,我应该从哪里开始,进入这个包的入口点是什么?典型的使用方式是什么?

所以我会从 readme 开始,真的,因为我多次这样做,无论是为我自己,还是为我的团队成员。我可能不是实现这个包的人,但我会写一个 readme,然后把它交给另一位开发者或队友,他们就能确切知道我的期望是什么,这基本上成为了讨论设计和权衡的基础,即使在一行代码都没有写之前。我觉得这是一个非常棒的方式,让你真正理解你想要构建的东西。

Jaana Dogan: 是的。

Mat Ryer: 我怀疑我们很多人设计的方式也不一样。对我来说,设计确实是通过这个过程逐渐浮现的。我之前也像你描述的那样做过,Johnny,那有点像文档驱动开发,或者其他什么……实际上,我们做了一个小工具叫 Silk,它是一个 markdown 文件,你只需用它描述你的 API,然后你可以将这个 markdown 文件运行在一个真实的 API 上。所以它介于文档和真实测试代码之间。这个想法源于你提到的,Jaana,那些 Go 的示例其实是可以运行的,所以它不仅仅是一个示例,你可以运行它,如果输出不匹配,它会失败,之类的功能真的很酷。我很喜欢 Go 这个项目中,测试是一个一等公民。这确实帮助了这个语言和社区的发展。

Johnny Boursiquot: 所以我们谈到了 TDD,但还有另一个学科,BDD(行为驱动开发),听起来像是 Mat 你刚提到的东西;整个行为驱动开发,我们 Go 社区中实际上也有一些流行的包采用了这种方法。你们中有人试过这种方法吗?喜欢还是不喜欢?你们对这种测试方法有什么看法?

Jon Calhoun: 我可以说我对人们使用它没有意见……不过我对 BDD 的一个不喜欢的地方是,大多数 BDD 包都要求你几乎学一种新语言。这让我觉得“我已经学了 Go,我想用 Go 来写测试代码,这样其他人不用学新东西。”在 Go 中这不算太严重,但我记得在 Rails 中写 Ruby 时,感觉好像你已经不在写 Ruby 了;就像“我得学这个新东西来学会如何测试”,我并不想学新东西。

Mat Ryer: 但你还是在做同样的事,不是吗?我没做过很多这方面的事情,但这是不是只是一种不同的方式来完成同样的工作?也就是它会运行一些代码,然后你对其进行断言?它是否只是一种流畅的 API,你可以说“它应该这样做,这个应该等于这个”,几乎可以用语言表达出来?

Jaana Dogan: 是的,区别更多在于组织方式,以及你如何表达……它有助于自我记录哪些东西是相关的,但这就是唯一的区别。

Johnny Boursiquot: 我曾被这样的想法打动:如果你用行为驱动开发的方式,几乎是英语描述功能的方式来写测试,那么开发团队之外的人也可以参与进来。比如 Cucumber,当我还在用 Ruby on Rails 时,底层语言好像叫 Gherkin。那时有一种承诺,非工程师也可以写规格,然后交给开发团队,开发团队只需针对这些规格写测试。但我从未见过这种情况真正发生。

Jaana Dogan: 是的,因为没有人真的在乎。但我认为最初它确实很重要,因为你需要解释为什么要测试这个案例;你需要回答一些问题,比如“谁会从这种行为中受益?它实际上解决了什么问题?它为什么要这样做?”你需要用普通的英语表达出来,这样人们可以查看和探索……这有点像把产品设计文档作为测试的一部分。

Jon Calhoun: 另一个问题是,如果你让一个不写代码的人写测试,开发者可能会误解他们的测试,写出通过所有测试的代码,但实际上完全没有实现他们想要的功能。 [笑声]

Jaana Dogan: 是的,是的。

Mat Ryer: 所以它并没有起作用,对吗?

Jon Calhoun: 我觉得它永远不会起作用,因为最终其他人还是得学会如何让开发者满意,到那时你还不如直接写个文档呢。

Johnny Boursiquot: 没错。



Mat Ryer: 听众们,记住你们可以在 Slack 上加入我们的讨论,我们在 #gotimefm 频道。你也可以在 Twitter 上 @GoTimeFM 与我们互动。说到这个,Cory LaNou 在 Slack 频道中刚刚提到:“我写了成千上万行测试代码,但从未觉得我需要其他包来帮助我测试。”所以,亲爱的嘉宾,你们对只用标准库测试 vs 使用框架或其他工具有何看法?

Jon Calhoun: 我会说---我觉得这和我们讨论过的所有事情一样。每个人都有不同的偏好,但对我来说更重要的是,整个团队或项目能够统一选择一种方式。我认为在代码库或项目中保持一致性比具体选择什么工具更重要。

Jaana Dogan: 是的,我觉得工具也很重要,因为它能在保持一致性方面发挥作用。有些测试框架自带一些工具的风格,我尽量远离这些,因为我希望所有人(尤其是开源项目的用户)能够检出我的代码并运行测试,而不需要学习新东西。这对我来说非常重要。我关心测试,我关心人们运行测试;这就是为什么我想让它尽可能的易于接近。我个人对标准库的测试包很满意。不过,只要有测试框架支持 Go 测试,我愿意尝试。

Mat Ryer: 你会写一些小的辅助工具吗,比如检查相等性,或者检查是否为 nil 或错误?你会做些什么来帮助自己吗?

Jaana Dogan: 你是指测试工具吗?

Mat Ryer: 对。

Jaana Dogan: 是的,我有一个测试包。

Mat Ryer: 哦,所以你有自己的测试包……

Jaana Dogan: 是的,我有一些工具……你知道,我提到过,比如它会在环境不支持时自动跳过集成测试,或者其他类似功能……所以我尽量为我维护的每个项目维护一个测试包……不过这是根据项目需求定制的工具。

Johnny Boursiquot: 我个人使用过 testify/assert 包,因为它提供了那些我发现自己在每个项目中都要重新创建的核心基础工具。它带有一些额外功能,我通常不用那些。我觉得仅仅使用那个断言包已经让测试变得更容易,更可接近。

但我发现一个问题---可能不止我一个人有这种感觉---当我刚开始学习 Go 时,我几乎想把其他语言中的习惯带过来。如果你在写 Ruby---我所认识的没有人在写 Ruby on Rails 时不使用某种框架测试。你总是会用 RSpec,或其他工具。我当时有这种倾向,觉得“我可以找点什么东西带进这个 Go 程序,让它更像我以前做的事情?”但这些年来,我逐渐倒退了。

基本上,我现在的想法是,默认情况下我能不能用标准库来做呢?如果我引入某个依赖,会有什么权衡?我尽量不引入依赖,如果只是几行代码,我会直接复制粘贴,而不是引入整个依赖包。或者即使标准库稍微不那么方便或者有点冗长,我也可能还是会选择它。因为个人来说,如果我不需要引入依赖,我真的、真的、真的会尽量避免。也许是因为我被第三方包坑过很多次,所以我真的是尽量用标准库,即使有点不舒服,或者有点痛苦。

Jon Calhoun: 我觉得这回到了 Mat 之前提到的那点,如果你让代码放置足够长的时间,你过去认为它可以正常工作,但当你再次运行时发现它不行了,很多时候是因为某个第三方包发生了变化,而你的包管理器---当时可能还没有这个东西,或者发生了什么事,你就会想:“我不想再经历这种情况了。”

Mat Ryer: 是的,Johnny 你之前提到人们从其他语言带来的惯性,这正是像 testify 这样的包存在的原因之一。我认为 testify 仍然是 Go 语言中被引入最多的包之一。坦白讲---我和一个朋友一起创建了 testify,因为我当时在其他语言中工作,那是我思考如何检查等价性的方式:只需要把它们扔进一个东西里,然后让它负责检查并输出漂亮的信息等。

你认为 Go 是否应该有某种方式来实现 “assert”(断言)?至少对于常见的场景,比如断言错误为 nil 之类的检查。

Johnny Boursiquot: 如果你真的想要的话,你可以自己写。

Jaana Dogan: 几行代码而已,非常简单。

Johnny Boursiquot: 是的……你不需要一个完整的包来实现这些。

Jon Calhoun: 这几乎就像我们现在在讨论这个东西是应该成为第三方库,还是应该进入标准库了……因为它显然在第三方库中已经存在了,所以唯一的区别就是你是否愿意引入某个东西。

Mat Ryer: 是的,我的意思是---它是否值得被推广?因为我觉得 testify 为这种测试风格做出了证明。老实说,如果你看看 testify 的 API,它确实发展了很多。它是一个非常开放的项目,任何人的贡献都非常受欢迎。结果就是,它现在成了一个很大的包,甚至有了自己的依赖,数量还不少……但实际上,正如你所说,人们通常只需要 assert equal 之类的几个常见功能。我们能不能把这些功能集成到 t 中,比如 t.assert,还是说这不值得?

Jon Calhoun: 我觉得 testify 更适合拆分成几个包,或者有一个包把所有的东西都引入。至少对我来说,它是一个很好的过渡工具……如果你是刚开始学习 Go 并且想要一些让你感觉更熟悉的东西---我很喜欢这样的工具。但有足够多的人会对此感到不满,我认为这会引发太多的争论,不值得去尝试。

Jaana Dogan: 我认为为了支持所有的失败情况,可能会出现很多不同的 API。比如 assert,如果不是 nil,输出这个错误信息;或者是直接 log fatal,还是应该只是 log printf?你可能会添加很多类似的东西到标准库中。如果你问我的意见,我认为增加这种类型的工具会要求一个非常庞大的 API 服务。而在你自己的项目中,你可以更加果断地决定,比如“如果有错误,就用这种格式记录致命错误,输出这个错误信息。”但我觉得要把这些工具添加到标准库中,你可能需要涵盖很多不同的情况,我觉得这样做不值得。

Jon Calhoun: 我也不喜欢标准库中有太多实现同一件事的方式……

Jaana Dogan: 是的……是的。

Jon Calhoun: 这正是让我在做 Rails 项目时快要疯掉的原因之一---每个人都有自己定义的遍历数组并处理它们的方式……

Jaana Dogan: 是的…… [笑]

Jon Calhoun: ……我不需要学习 17 种遍历方式。

Jaana Dogan: [笑] 我认为如果 Go 语言中没有某个功能,或者标准库中没有某个功能,那是因为有太多不同的实现方式,所以他们无法放入一个带有个人倾向的 API 或者功能。他们想保持 Go 语言的正交性,并且会远离增加不必要的噪音。

Jon Calhoun: 我还认为,如果你要引入某个东西,做决定其实蛮简单的。如果你在写一个非常小的包---就像你说的,如果你只是写一个编码器/解码器之类的东西,那确实不值得引入其他东西;但如果你的包已经引入了 50 个其他依赖,再加一个 testify 也没什么大不了的。

Jaana Dogan: 对。

Mat Ryer: 在 Go 语言的测试中,还有什么我们特别喜欢的吗?其中一个我总是很喜欢的就是我们可以用表格测试(table tests)。你可以有一个匿名结构体的切片,包含任何你想要的字段---这取决于具体的情况,这非常合适,因为它提供了更多讲述故事的机会。然后你可以立即用一些测试数据实例化它们,接着循环遍历这些数据,运行一些目标方法或函数,使用这些输入,表格测试还包含了输出。这个模式我觉得非常好。还有其他的模式吗?

Johnny Boursiquot: 我从来不离开它。 [笑] 真的。即使是单个案例的表格测试,我也会用。因为我一次又一次地发现,即使我一开始觉得“这里我可能只需要测试一个情况”,但最终我总会找到一个边缘情况,或者从业务中发现某些输入,或者在找解决方案的过程中,我会意识到“哦,原来这里还有其他情况……”所以我最终还是会创建表格驱动的测试。所以,现在我一开始就直接用它,把我现在知道的情况放进去,然后随着时间的推移,我可以很容易地添加新的测试,复制粘贴下一行,或者一行行注释来单独测试。我几乎所有的测试都用它。

Jaana Dogan: 我很喜欢它鼓励大家添加更多的测试用例……这是我发现的,如果我一开始用表格驱动测试,大家会随着时间的推移添加更多的用例。因为创建一个新的测试函数的样板代码变少了,也更容易添加新的测试。

Mat Ryer: 是的。这对刚接触 Go 编程的人来说也很棒。如果你想为一个开源项目做贡献,你可以去看看测试代码,因为你可能知道一些原始包编写者不知道的东西。

Jaana Dogan: 是的。

Mat Ryer: 我同意,它确实让事情变得更简单。

Jaana Dogan: 是的。我还喜欢的一点就是,一些编辑器实际上可以自动生成表格驱动的测试。所以你可以自动生成样板代码,甚至只为一个用例保留它,之后其他人会补充新的用例。

Jon Calhoun: 你有没有遇到过某些情况---不是说不做表格驱动测试,而是一开始从更简单的方式开始,然后再回来看看怎么把它转换成表格驱动测试?

Johnny Boursiquot: 没有。 [笑]

Jaana Dogan: 我觉得我对表格驱动测试有一个问题---很难只为某个特定的输入运行测试。它会运行整个表格的所有用例。

Mat Ryer: 是的。你的唯一选择要么是注释掉……

Jaana Dogan: 是的……

Mat Ryer: 要么你可以覆盖下面的结构体,我猜是这样……

Jaana Dogan: 是的,但这些通常是单元测试,所以它们运行得很快……但有时我只想针对某个特定的输入运行测试,因为输出是变量或其他东西,比如我正在打印一些额外的信息,而日志非常难读……这是我唯一遇到的问题。

Jon Calhoun: 我见过几种不同的做法,我自己也谈过这个问题。有人用的一个方法是不用切片而用 map,这样他们可以命名每个测试用例。我不总是喜欢这种方式,但它的一个好处是,它让你思考每个测试用例的目的,而不是“让我随便扔 1000 个随机的用例进去。”

Jaana Dogan: 是的,我也是这样做的,以便让日志更好读,你可以记录测试的名字,通常是自描述的……这有助于你阅读日志的时候。

Johnny Boursiquot: 你可以在表格驱动测试中做到这一点,把测试场景作为结构体的一个属性。我通常称之为场景,第一个字段就是“这是我想要测试的场景”。当我调用 t.run 时,第一个参数就是场景的名字,这样输出就会清楚地告诉你这是你在测试的场景,以及哪个测试失败了。非常直观。

Jaana Dogan: 确实。

Jon Calhoun: 所以你做的其实和 map 是一样的,只是测试名字是结构体的第一个字段。

Johnny Boursiquot: 完全正确。

Jon Calhoun: 我一般不喜欢这种方式是因为当你在写 for 循环遍历时,数据和测试名字不是很清楚地分开,如果你明白我的意思……

Jaana Dogan: 是的,确实。

Jon Calhoun: 但这只是个很小的挑剔。

Mat Ryer: 对,Chris James 也在 Slack 上提到过这一点。他说“如果你用 t.run,当然是子测试,你可以通过运行标志来指定特定的测试。”他讲的是同一个点。

Jaana Dogan: 顺便说一下,在有 t.run 之前,表格驱动测试真的很难……之前真的很难。

Johnny Boursiquot: 没错!现在没有借口了。

Jon Calhoun: 我无法想起具体的例子,但你们提到了你们总是从表格测试开始,我记得有几次写的东西,一开始要不涉及表格测试就很难,但我现在想不起来具体是什么情况。我觉得是跟错误处理有关,有时我想忽略错误,有时又想做其他事情,但我不记得具体是什么了。

Jaana Dogan: 哦,是的,我完全能理解。如果对于每个输入,断言的方式不一样,我通常会分开写测试。

Jon Calhoun: 我想我遇到的情况是,我的测试实际上是在测试一个真实的 API,我想验证某些错误信息;那些错误包含了我想要断言的更多内容,所以我不得不做更多的处理……

Jaana Dogan: 嗯。

Jon Calhoun: 最后我写了检查函数,但当我一开始写表格测试时,我不想考虑检查函数,我只是想先写测试,然后再回来完善。

Mat Ryer: 有人还记得吗?抱歉,Johnny,我要换话题了,如果你还想继续这个话题,请继续。

Johnny Boursiquot: 没事,请继续。

Mat Ryer: 我想问,有人还记得在 Go 中 error 变成接口之前,它曾经是一个指针类型吗?它当时是 os.error 类型。

Jaana Dogan: 嗯……

Mat Ryer: 我刚才突然想到了这点,因为我记得曾经和错误较劲,当时是 os.error,后来当然改成了接口,应该是在 v1 之前改的。嗯!幸好我没记错。

还有一件我喜欢做的事……对不起,再说一件事。我喜欢在有一个 setup 函数做些工作的时候,从 setup 调用中返回一个 teardown 函数。通常我会调用 setup,传入 T,并且把 T 传给所有的帮助函数,这样如果出了问题,它们可以直接失败,我不需要返回任何错误……是的,setup 可能会返回一些东西,但它也可能返回一个类似 Context.WithCancel 的清理函数,你可以立即 defer 调用它来做 teardown。

Jaana Dogan: 是的,我喜欢 defer 清理函数。

Jon Calhoun: 我唯一不喜欢的就是当人们只是用双括号调用它……如果他们这么做也没问题,我只是觉得这种方式很容易被忽略。

Jaana Dogan: 确实。像是评估与……

Jon Calhoun: 如果你 defer 调用 setup,然后后面紧跟另一对括号,人们会想“这是什么意思……?”

Jaana Dogan: 是的,我不喜欢那样。

Mat Ryer: 这就是我们过度追求减少代码行数,结果把它压缩得太厉害了。

Jaana Dogan: [笑]

Mat Ryer: 不,真的,我完全同意。这大大减少了可读性……看起来很奇怪,像是魔法,但通常我喜欢魔法。

Johnny Boursiquot: 你会把它和 TestMain 提供的 setup 和 teardown 机制结合使用吗,还是你会选择其中一个?你怎么处理这个?

Mat Ryer: 只是针对某一组测试。如果是表格测试,我想做统一的 setup 和 teardown。我会在这种情况下使用它。

Johnny Boursiquot: 哦,明白。所以你只在特定的测试组中这样做,但不是所有测试。

Mat Ryer: 是的,不是每次都合适。但比如说,如果我在测试一个 web 服务器---基本上我总是在测试这个---我会在测试助手中创建这个服务器并返回它,然后 teardown 函数就是关闭服务器,这样每次运行后都能干净地清理。

Johnny Boursiquot: 我明白了,挺好的。

Jaana Dogan: 我希望有一种全局的方式为一个包设置 setup 函数和 teardown 函数。有时 [音频不清晰 01:04:43.21] 变量,我有时会忘记调用 setup,或者忘记调用 teardown,导致测试行为异常,或者测试一直表现异常,但没人意识到它没有清理一些资源……我希望在测试包中有更简单的方式来全局设置一些东西:在开始时总是运行这个,在结束时总是清理这些东西。

Jon Calhoun: 我对此有些矛盾的看法,因为我觉得那样会导致全局状态被滥用。

Jaana Dogan: 是的,确实。

Johnny Boursiquot: 全局状态是魔鬼。

Jaana Dogan: 是的。任何可能被滥用的东西,都会被滥用,我同意……[笑]

Mat Ryer: 是的,任何可能被滥用的东西,都会被滥用。好吧,我觉得我们今天的讨论差不多到这里了。非常感谢我的小组成员,Johnny Boursiquot、Jaana B. Dogan 和 Jon Calhoun。Jon,你的名字是这么发音的吧?Jon。

Jon Calhoun: 差不多了,是的。 [笑]

Mat Ryer: 感谢网络延迟,我成功叫对了你的名字,Jon。 [笑] 是的,我们今天讨论了很多……我觉得对我来说一个关键的收获是,做好测试---一切都取决于你编写测试代码的上下文:你希望从编写测试代码中得到什么?如果你在写一个简单的小东西,也许可以完全跳过测试。如果你在写一个庞大复杂的系统,测试的需求就会与小应用程序或你自己的小项目,甚至是中等项目非常不同。

所以在测试方面可能没有银弹,只有一些好的理念……如果你关心你的测试代码,好好维护它,不要一味地增加测试代码。照顾好它,保持它的整洁,我想一切都会好起来的。你会做得很好。是的,就是这样。

Jaana Dogan: 我想今天到这里就结束了。感谢你们的参与和收听,希望下周再见。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/19856.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

Excel常用技巧分享

excel单元格内换行 直接按回车会退出当前单元格的编辑,如果需要在单元格中换行,需要按下AltEnter。 excel插入多行或多列 WPS 在WPS中想要插入多行,只需在右键菜单中输入对应的数字即可。 Office Excel excel中相对麻烦一些,比…

C# .NET环境下调用ONNX格式YOLOV8模型问题总结

我的环境是: Visual Studio: 2019 显卡: 一、遇到问题 1、EntryPointNotFoundException:无法在DLL“onnxruntime”中找到名为“OrtGetApiBase”的入口点。差了下原因,入口点是启动项中的问题。 原因:之前用yolov7时安装的版本在C…

【PTA】【数据库】【SQL命令】编程题1

数据库SQL命令测试题1 10-1 显示教工编号以02开头的教师信息 作者 冰冰 单位 广东东软学院 显示教工编号以02开头的教师信息 提示:请使用SELECT语句作答。 表结构: CREATE TABLE teacher ( TId CHAR(5) NOT NULL, -- 教师工号,主键 DId CHAR(2) …

VSCode快速生成vue组件模版

1&#xff0c;点击设置&#xff0c;找到代码片段 2&#xff0c;搜索vue&#xff0c;打开vue.json 3&#xff0c;添加模版 vue2模板 "vue2": {"prefix": "vue2","body": ["<template>"," <div>$0</di…

理解DOM:前端开发的基础

理解DOM 在前端开发中&#xff0c;DOM&#xff08;文档对象模型&#xff09;是一个至关重要的概念。它不仅定义了如何通过编程方式访问和修改网页内容&#xff0c;还为我们提供了一种结构化的方式来与页面交互。本文将带你了解DOM的基本概念、不同节点的操作以及何时可以进行更…

如何将几个音频合成一个音频?非常简单的几种合成方法

如何将几个音频合成一个音频&#xff1f;音频合成不仅仅是将不同的音频文件按顺序排列&#xff0c;它还可能涉及到音量调节、剪辑、淡入淡出、音效调整等多个方面。对于一些专业的音频制作人员来说&#xff0c;音频的每一部分细节都可能需要精心打磨&#xff0c;以确保最终合成…

虚拟化表格(Virtualized Table)性能优化

文章目录 功能介绍一开始的代码领导让我们分析一下开始优化如何监听事件和传参&#xff1f;定位操作栏更加优化 功能介绍 菜鸟最近做的一个功能如下&#xff1a; 后端返回两个很大的数组&#xff0c;例如&#xff1a;数组a 1w条&#xff0c;数组b 2w条&#xff0c;然后要操作b…

Orcad 输出有链接属性的PDF

安装adobe pdf安装Ghostscript修改C:\Cadence\SPB_16.6\tools\capture\tclscripts\capUtils\capPdfUtil.tcl ​ 设置默认打印机为 Adobe PDF ​ 将Ghostscript的路径修改正确 打开cadence Orcad &#xff0c;accessories->candece Tcl/Tk Utilities-> Utilities->PD…

从源头保障电力安全:输电线路动态增容与温度监测技术详解

在电力系统中&#xff0c;输电线路是电能传输的关键环节。然而&#xff0c;当导线温度过高时&#xff0c;会加速导线老化&#xff0c;降低绝缘性能&#xff0c;甚至引发短路、火灾等严重事故&#xff0c;对电网安全运行构成巨大威胁。近日&#xff0c;某地区因持续高温和用电负…

递归系列 简单(倒序输出一个正整数)

倒序输出一个正整数 时间限制: 1s 类别: 递归->简单 问题描述 例如给出正整数 n12345&#xff0c;希望以各位数的逆序形式输出&#xff0c;即输出54321。 递归思想&#xff1a;首先输出这个数的个位数&#xff0c;然后将个位丢掉&#xff0c;得到新的数&#xff0c;继续…

矩阵论在图像算法中的应用

摘要&#xff1a; 本文详细阐述了矩阵论在图像算法中的广泛应用。首先介绍了图像在计算机中的矩阵表示形式&#xff0c;然后从图像压缩、图像变换、图像特征提取与识别、图像恢复与重建等多个方面深入分析了矩阵论相关技术的作用原理和优势。通过对这些应用的探讨&#xff0c;展…

鸿蒙改变状态栏和安全区域颜色之 expandSafeArea

改变状态栏和安全区域颜色之 expandSafeArea 基于API12。 参考文档 直接设置build里边根元素的背景色之后&#xff0c;本想着是整个页面的颜色全变成相应的颜色&#xff0c;不过实际上状态栏跟地步安全区域是不受影响的。这个时候一般可能都会各种地方找API来设置状态栏跟安全…

Ubuntu Linux使用前准备动作_使用root登录图形化界面

Ubuntu默认是不允许使用 root 登录图形化界面的。这是出于安全考虑的设置。但如果有需要&#xff0c;可以通过以下步骤来实现使用 root 登录&#xff1a; 1、设置 root 密码 打开终端&#xff0c;使用当前的管理员账户登录系统。在终端中输入命令sudo passwd root&#xff0c…

交换排序——快速排序3 针对LeetCode某OJ的优化

交换排序——快速排序3 针对LeetCode某OJ的优化 快速排序的优化小区间优化三数取中三路划分优化 快速排序的优化 这篇优化围绕这个测试OJ展开。 912. 排序数组 - 力扣&#xff08;LeetCode&#xff09; 这个测试OJ在早期用快排还能过。但现在用快排不能过了。 因为这个OJ针…

【Vue笔记】基于vue3 + element-plus + el-dialog封装一个自定义的dialog弹出窗口组件

这篇文章,介绍一下如何使用vue3+element-plus中的el-dialog组件,自己封装一个通用的弹出窗口组件。运行效果如下所示: 目录 1.1、父子组件通信 1.2、自定义VDialog组件(【v-model】模式) 1.2.1、编写VDialog组件代码 1.2.2、使用VDialog组件 1.2.3、运行效果 1.3、自…

【支持向量机(SVM)】:算法原理及核函数

文章目录 1 SVM算法原理1.1 目标函数确定1.2 约束条件优化问题转换1.3 对偶问题转换1.4 确定超平面1.5 计算举例1.6 SVM原理小节 2 SVM核函数2.1 核函数的作用2.2 核函数分类2.3 高斯核函数2.3 高斯核函数API2.4 超参数 γ \gamma γ 1 SVM算法原理 1.1 目标函数确定 SVM思想…

【数据结构】树——链式存储二叉树的基础

写在前面 书接上文&#xff1a;【数据结构】树——顺序存储二叉树 本篇笔记主要讲解链式存储二叉树的主要思想、如何访问每个结点、结点之间的关联、如何递归查找每个结点&#xff0c;为后续更高级的树形结构打下基础。不了解树的小伙伴可以查看上文 文章目录 写在前面 一、链…

Java基于微信小程序+SSM的校园失物招领小程序

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

IDEA 2024.3 版本更新主要功能介绍

IDEA 2024.3 版本提供的新特性 IntelliJ IDEA 2024.3 的主要新特性&#xff1a; AI Assistant 增强 改进的代码补全和建议更智能的代码分析和重构建议Java 支持改进 支持 Java 21 的所有新特性改进的模式匹配和记录模式支持更好的虚拟线程调试体验开发工具改进 更新的 UI/UX 设…

Unity类银河战士恶魔城学习总结(P132 Merge skill tree with skill Manager 把技能树和冲刺技能相组合)

【Unity教程】从0编程制作类银河恶魔城游戏_哔哩哔哩_bilibili 教程源地址&#xff1a;https://www.udemy.com/course/2d-rpg-alexdev/ 本章节实现了解锁技能后才可以使用技能&#xff0c;先完成了冲刺技能的锁定解锁 Dash_Skill.cs using System.Collections; using System…