如果您掌握了编写单元测试时发生的典型错误的小清单,您甚至可以编写它们。 今天,Android Konstantin
kzaikin Zaikin的Yandex.Browser开发团队的
负责人将与Habr读者分享他的经验。
-我有一份实用的报告。 我希望它对所有人有益-那些已经在编写单元测试的人,那些只是在考虑编写的人,正在尝试的人以及没有成功的人。
我们有一个很大的项目。 俄罗斯最大的移动项目之一。 我们有很多代码,很多测试。 在每个池请求中都跟踪测试,它们不会同时下降。
谁知道他在该项目中拥有什么测试范围? 零,好。 谁在项目中进行单元测试? 谁认为不需要单元测试? 我认为这没什么不对,有些人对此深信不疑,我的故事应该可以帮助他们相信这一点。
幸运的是,有成千上万的绿色测试-我们不是马上就来了。 没有灵丹妙药,屏幕上我的报告的主要思想是:

中国谚语用象形文字写成,一千个旅程始于一步。 似乎有一个类似的说法。
很久以前,我们就做出了一项决定,我们需要改进我们的产品,我们的代码,并且我们正在有目的地朝着这一目标迈进。 这样,我们遇到了许多颠簸,水下耙子,并带着一些信念聚集在一起。

我们为什么需要测试?
因此,当我们引入新功能时,旧功能不会下降。 在GitHub上具有徽章。 要重构现有功能-深入思考,需要向不编写测试的人透露这些功能。 为了使现有功能在重构期间不会掉落,我们将通过测试来保护自己。 要老板发送合并请求,是的。
我的意见-请不要将其与我的团队的意见联系在一起-测试对我们有帮助。 它们使您可以在不将其投入生产的情况下运行代码,而无需将其安装在设备上,则可以非常快速地启动和运行它。 您可以避免设备和生产中无法使用的所有极端情况,而测试人员也不会提出这些建议。 但是,作为开发人员,您会发明它们,对其进行检查并在早期阶段修复错误。
非常重要:测试告诉开发人员,代码应该如何工作,开发人员认为您的方法应该做什么。 这些评论不会消失,过一会儿有用的评论就会变得有害。 碰巧在注释中写了一件事,而在代码中则完全不同。 从这个意义上讲,单元测试不能说谎。 如果测试为绿色,则说明那里发生了什么。 测试失败-您违反了开发人员的主要意图。
签订合同。 这些不是已签署和盖章的合同,而是针对类行为的软件合同。 如果您重构,在这种情况下,合同将被违反,如果您违反合同,则测试将失败。 如果保存了合同,则测试将保持绿色,您将更有信心重构正确。

这是我整个报告的总体思路。 您可以显示第一行然后离开。
许多人认为测试代码是马马虎虎的代码,不是用于生产的,因此您可以马马虎虎编写。 我对此表示强烈反对,我认为应该首先对测试以及生产代码负责。 如果您以相同的方式使用它们,那么测试将使您受益。 否则,将是一个污迹。
更具体地说,似乎下面的两行引用了任何代码。
吻-保持简单,愚蠢。 无需复杂化。 测试应该很简单。 生产代码应该很简单,但是测试尤其如此。 如果您具有易于阅读的测试,那么这些测试很可能是编写得很好,表达良好的测试,也很容易测试。 即使在合并请求期间,看着您全新的测试的人也会明白您想说什么。 如果发生故障,您可以轻松了解发生了什么。
干-不要重复自己。 在测试中,开发人员通常倾向于使用似乎没有人在生产中使用的禁止技术-复制粘贴。 在开发人员积极复制粘贴的过程中,他们根本不会理解。 不幸的是,在测试中,这是正常的做法。 不需要这样做,因为-第一行。 如果您像真实的好代码一样诚实地编写测试,那么这些测试对您将很有用。
当我们开发成千上万的代码行,编写成千上万的测试,收集佣金时,我积累了关于测试的典型注释。 我很懒惰,当我去池请求并观察到相同的错误时,基于DRY原理,我决定写下这些典型的问题,然后我首先在内部Wiki上做了,然后在GitHub上发布了实用的测试气味,您可以关注当您编写测试时。

我将按要点列出。 如果您想起这种测试气味,请增加一个计数器。 如果数到五,您可以举手并尖叫“宾果!” 最后,我想知道谁计数了多少。 我的计数器等于点数,我自己收集了所有点数。

您所知道的编程中最困难的事情。 在测试中,这真的很重要。 如果您对测试的命名不正确,则很可能无法制定测试要检查的内容。
人类是相当简单的生物,很容易被困在名字中。 因此,我请您打电话给测试。 制定测试以验证并遵循简单规则。
no_action_or_assertion
如果测试名称不包含测试检查内容的描述,例如,您拥有Controller类,并且编写了testController测试,那么您要检查什么? 该测试应该做什么? 最有可能的是,什么都不做或要检查的东西太多。 一个都不适合我们。 因此,有必要在测试名称中写下我们要检查的内容。
long_name
您不能走到另一个极端。 测试的名称应足够简短,以便一个人可以轻松地对其进行分析。 从这个意义上说,Kotlin很棒,因为它允许您用引号将测试名称写在普通英语的空格中。 它们更容易阅读。 但是,长名字还是有味道的。
如果您的测试名称太长,则很可能您在一个测试类中放置了太多的测试方法,因此您需要澄清要检查的内容。 在这种情况下,您需要将测试类分为几个类。 无需担心。 您将拥有一个测试类名称,该名称将检查您的生产代码的名称,并且会有简短的测试名称。
old_prefix
这是愚昧主义。 以前,在Java中,每个人都使用JUnit进行测试,直到第四个版本为止,人们都同意测试方法应以单词test开头。 真是这样,每个人都这么称呼它。 但是有一个问题,英语单词test是动词“ check”。 人们很容易陷入这种陷阱,不再写任何其他动词。 编写testController。 检查自己很容易:如果您没有写动词测试班应该做什么,那么很可能您没有检查过某些东西,那么您就写得不够好。 因此,我总是要求您从测试方法的名称中删除单词test。
我讲的很简单,但奇怪的是,它们有帮助。 如果测试通过得当,最有可能在引擎盖下看起来很好。 这很简单。

我实际上在GitHub上读取了测试气味ID。 链接在下面,您可以步行和使用。
multiple_asserts
在测试方法中,有很多断言。 也许吧? 也许吧 是好是坏? 我认为这很糟糕。 如果您在测试方法中编写了多个断言,那么您将检查多个语句。 如果您测试您的测试而第一个断言落空,那么测试会到达第二个断言吗? 不会达到。 在CI上某个程序集崩溃之后,您已经知道测试失败了,去修复一些问题,再次填充它,它将落在下一个断言上。 很有可能。
在这种情况下,如果您将此测试方法分为几种,并且具有多个断言的所有方法都同时落入,那会更酷,因为它们将彼此独立地启动。
其他一些断言可以掩盖测试类执行的不同操作。 我建议编写一个测试-一个断言。 同时,断言可能非常复杂。 我的同事在第一份报告中演示了一段代码,其中他使用了出色的assertThat结构和匹配器。 我真的很喜欢JUnit的对决,所以您也可以使用它。 对于测试读者来说,这只是一个简短的声明。 GitHub上有所有这些气味及其修复方法的示例。 有一个错误代码示例和一些良好代码。 所有这些都以项目的形式完成,您可以下载,打开,编译和运行所有测试。
many_tests_in_one
下一个气味与上一个气味密切相关。 您在系统上执行某些操作-声明。 对系统做其他事情,一些长时间的操作-做一个断言-做其他事情。 实际上,您只是简单地研究了几种方法,就获得了可靠的,良好的测试方法。
repeating_setup
这是指冗长。 如果您有一个测试类,并且每个测试方法在开始时都运行相同的方法。
在开始时执行相同方法的测试类。 这似乎有点,但是在每种测试方法中都存在这种垃圾。 而且,如果所有测试方法都通用,那么为什么不将其拖到JUnit 5中的构造函数,
Before块或
Before Each块中。如果执行此操作,则每种方法的可读性都会提高,而且您将摆脱DRY sin。 这样的测试更易于维护和阅读。

测试的可靠性非常重要。 有迹象表明可以确定测试将哭泣,呈绿色或红色。 当开发人员编写它时,他确定它是绿色的,然后由于某种原因测试变成绿色或红色,这给我们带来了痛苦和不确定性,总的来说这些测试是有用的。 我们不确定测试,这意味着我们不确定测试是否有用。
随机的
我本人曾经编写过测试,其中包含Math.random(),并进行随机数处理,并对它们进行了处理。 不需要这样做。 我们希望测试系统以相同的配置进入测试系统,并且其输出也必须相同。 因此,例如,在单元测试中,您无需对网络进行任何操作。 由于服务器可能没有响应,因此可能会有不同的时间安排。
如果您需要适用于网络的测试,请执行本地代理,无论如何,但绝不要使用真实网络。 这是相同的随机性。 当然,您不能使用随机数据。 如果必须执行某些操作,请针对边界条件,条件较差的情况进行一些示例,但应对其进行硬编码。
睡觉
开发人员在尝试测试某种异步代码时面临的一个经典问题。 就是我在测试中做了什么,然后我需要等到它完成。 怎么做? Thread.sleep(),当然。
有问题。 例如,在开发测试时,您是在某些打字机上进行测试的,因此可以一定的速度运行。 您在另一台计算机上运行测试。 如果您的系统在Thread.sleep()时间内无法正常工作,会发生什么? 测试变成红色。 这是意外的。 因此,这里的建议是,如果要执行异步操作,则根本不要测试它们。 几乎所有异步操作都可以部署,因此您具有某种提供异步操作的条件机制以及一个同步执行的代码块。 例如,内部的AsyncTask有一个同步执行的代码块。 您可以轻松地同步测试它,而无需任何异步。 无需测试AsyncTask本身,它是一个框架类,为什么要测试它? 用括号括起来,您的生活会更轻松。
Thread.sleep()很痛苦。 除了会降低测试的可靠性之外,这还因为由于设备的时序不同而使测试人员哭泣,这也减慢了测试的执行速度。 谁希望他的单元测试(应该在几毫秒内执行)将运行五秒钟,因为我设置了“睡眠”状态?
Modify_global
典型的气味是,我们在测试开始时更改了一些全局静态变量,以检查系统是否正常运行,但最后没有返回。 然后我们得到一个很酷的情况:在机器上,开发人员按一个顺序运行测试,首先检查具有默认值的全局变量,然后在另一个测试中对其进行更改,然后执行其他操作。 两项测试均为绿色。 在CI上,情况恰好发生,测试以相反的顺序开始。 尽管这两个测试都是绿色的,但其中一个或两个测试都将是红色的。
您需要自己清理一下。 侦察兵规则从这个意义上说:更改了全局变量-返回到原始状态。 更好的是,确保不使用全局状态。 但这是一个更深刻的想法。 关于测试有时会突出体系结构中的缺陷这一事实。 如果我们必须更改全局状态并将其返回到原始状态以编写测试,那么我们的体系结构是否都做得很好? 例如,我们真的需要全局变量吗? 通常,您可以通过注入一些类别的上下文或某种东西来消除它们,以便每次都可以在测试中重新初始化,注入和重新初始化它们。
@VisibleForTesting
测试气味是否高级。 通常,第一天就不需要使用这种东西。 您已经测试了某些东西,然后需要将类转换为某种特定状态。 而您却使自己成为后门。 您有一个生产类,并且创建了一个在生产中永远不会调用的特定方法,并通过它向类中注入了一些东西或更改了其状态。 因此,恶意破坏封装。 在生产中,您的类以某种方式工作,但是在测试中,实际上,它是一个不同的类,您可以通过其他输入和输出与它进行通信。 在这里,您可能会遇到一种情况,即您更改了生产,但是测试没有注意到它。 测试继续经过后门,并且没有注意到,例如,异常开始在构造函数中发出,因为它们经过了另一个构造函数。
通常,您应该通过与生产中相同的输入和输出来测试您的类。 不应仅使用测试方法。

在我们的15,000个测试中,有多少个被执行? 在Team City上,每次请求池大约需要20分钟,开发人员被迫等待。 仅仅因为有1万5千个测试。 在本节中,我编译了使测试变慢的气味。 尽管thread_sleep已经存在。
不必要的_android_test
Android进行了仪器测试,它们很漂亮,可以在设备或模拟器上运行。 确实,这将完全提升您的项目,但速度非常慢。 对于他们来说,您甚至需要提升整个模拟器。 即使您想象自己在CI上有一个提升的仿真器(恰好恰好有一个),然后在仿真器上运行测试也要比在主机上运行时间长得多,例如使用Robolectric。 虽然还有其他方法。 这种框架使您可以使用纯Java在主机上使用Android框架中的类。 我们非常积极地使用它。 以前,Google对它有些酷,但是现在,谷歌本身已经在各种报告中谈论它,建议使用它。
不必要的robolectric
Robolectric的Android框架被仿真。 尽管实现的距离越远,功能越丰富,但它并不完整。 它几乎是真正的Android,只能在台式机,笔记本电脑或CI上运行。 但是它也不需要在任何地方使用。 Robolectric不是免费的。 如果您有一个将您从Android仪器英勇地转移到Robolectric的测试,您应该考虑-也许走得更远,摆脱Robolectric,把它变成最简单的JUnit测试? 机器人测试需要时间来初始化,尝试加载资源,初始化您的活动,应用程序以及其他所有内容。 需要一些时间。 这不是一秒,而是毫秒,有时是数十秒。 但是,当有许多测试时,那也很重要。
有一些技术可以摆脱Robolectric。 您可以通过使用接口包装整个平台部分来通过接口隔离代码。 然后将只有一个JUnit主机测试。 JUnit在主机上的速度非常快,只有极少量的开销,这样的测试可以在成千上万的运行中运行,它们将运行一分钟,几分钟。 不幸的是,我们的测试需要很长时间才能完成,因为我们有很多Android工具测试,因为我们在浏览器中具有本机部分,因此我们不得不在真实的模拟器或设备上运行它们。 为什么这么久。
我不会再烦你了。 你有多少气味? 到目前为止,最多七个。 订阅
频道 ,放星星。
