本文是关于测试设计和可测试性的一些想法。 我们与我的儿子讨论了一些问题,他的儿子是Java的初级开发人员,目前在匈牙利的EPAM(我工作的同一家公司,但在另一家子公司)工作和学习。 本文中的所有内容都是不错的旧知识,但是,您仍然可以在其中找到一些有趣的东西。 如果您是初中生,那么就是这个原因。 如果您是大四学生,那么您将获得一些有关如何解释这些事情的想法。 如果都不是:对不起。
问题简介
他们要做的任务是一些轮盘赌程序或其他游戏模拟代码,他们必须编写。 代码的输出是损失或赢得的模拟钱数。 该模拟使用随机数生成器。 在进行测试时,该生成器引起了头痛。 (是的,您是对的:问题的根本原因是缺乏TDD。)代码的行为是随机的。 有时,模拟玩家赢得了比赛,而其他时候却输了。
使它可测试:注入模拟
如何使此代码可测试?
答案应该很明显:模拟随机数生成器。 利用注入的随机源,并在测试过程中注入不同的非随机源。 在测试期间,随机性并不重要,因此无需测试随机性。 我们必须相信随机数生成器是好的(不是,它永远不会好,也许足够好,但这是完全不同的故事),并且已经由其自己的开发人员进行了测试。
学习#1:不要测试依赖项的功能。
我们可以将Supplier
类型的字段初始化为() -> rnd()
lambda之类的字段,如果进行测试,则使用setter覆盖它。
可测试的好吗?
现在,我们更改了类的结构。 我们打开了一个新条目,以注入一个随机数生成器。 这个可以吗?
没有普遍的是或否的答案。 这取决于要求。 程序员喜欢使其代码可配置,并且比当前要求所绝对需要的代码更具通用性。 原因……好吧……我想,这是因为过去,程序员多次经历了需求的变化(开玩笑!),并且如果为变化做好了准备的代码,那么编码工作就变得容易了。 这是足够合理的推理,但其中存在一些基本缺陷。 程序员不知道将来会出现什么样的需求。 通常,没有人真正知道,每个人对此都有一些想法。
程序员通常知识最少。 他们怎么知道未来? 业务分析师了解得更好一些,并且在链的末端,用户和客户最了解它。 但是,即使他们也不知道自己无法控制的业务环境也可能需要程序的新功能。
另一个缺陷是,开发未来需求现在会产生很多开发人员无法理解的额外成本。
实践表明,这种“提前”思考的结果通常是几乎不需要的复杂代码和灵活性。 甚至有一个缩写词: YAGNI ,“您将不需要它”。
那么,实现该可注射性功能是否为YAGNI? 一点也不。
首先:代码有许多不同的用途。 执行只是一个。 同样重要的是代码的维护。 如果无法测试该代码,则无法可靠地使用它。 如果无法测试代码,则无法对其进行可靠的重构,扩展:维护。
仅用于测试的功能就像房子的屋顶桥。 您在房屋中时不会自己使用它,但是如果没有它们,检查烟囱将非常困难且昂贵。 没人质疑这些屋顶桥的必要性。 它们是必需的,它们是丑陋的,而且仍然存在。 没有他们,房子就无法测试。
学习#2:可测试的代码通常具有更好的结构。
但这不是唯一的原因。 通常,当您创建可测试的代码时,最终结构通常也将更有用。 也就是说,可能是因为测试模仿了代码的使用,而设计可测试的代码将促使您将可用性放在第一位,将实现放在第二位。 而且,说实话:没有人真正在乎实施。 可用性是目标,实现只是实现目标的工具。
责任
好的,我们做到了:可测试性很好。 但是,还有一个关于责任的问题。
随机性的来源应该硬连接到代码中。 代码和代码的开发者负责随机性。 不是因为这个开发者实现了它,而是因为这个开发者选择了随机数生成器库。 选择基础库是一项重要的任务,必须负责任地完成。 如果我们打开一扇门改变随机性的实现选择,那么我们将失去对我们责任的控制。 还是不是?
是的,没有。 如果您打开API并提供了注入依赖项的可能性,那么您就不必对注入的功能的运行负责。 尽管如此,用户(您的客户)仍会来找您寻求帮助和支持。
“有一个bug!” 他们抱怨。 是因为您的代码还是用户选择的特殊注入实现中的某些内容?
您基本上有三个选择:
- 您可以检查每种情况下的错误,并在错误不是您的错误时告诉他们,并帮助他们选择更好的(或只是默认的)函数实现。 这将花费您宝贵的时间,无论是已付还是未付。
- 同时,您也可以排除问题并说:您甚至不会检查使用标准的默认实现无法复制的任何错误。
- 从技术上讲,您只能使用仅用于可测试性的功能。
第一种方法需要良好的销售支持,否则您最终将花费个人时间解决客户问题,而不是花费您的付费客户时间。 不专业。
第二种方法是专业的,但客户不喜欢它。
第三是将用户从#1吸引到#2的技术解决方案。
学习#3:提前考虑用户的期望。
无论选择哪种解决方案,重要的事情都是有意识地做到,而不仅仅是偶然。 了解您的用户/客户可能会想到什么并做好准备。
防止生产注入
当您打开将随机性生成器注入代码的可能性时,如果确实需要,如何为生产环境关闭那扇门?
我首选的第一个解决方案是,首先不要将其打开。 通过具有lambda表达式(或其他方式)的初始化字段使用该表达式,使其可以注入,但不实现注入支持。 让该字段为私有字段(但不是最终字段,因为在这种情况下可能会导致其他问题),并在测试中进行一些反思以更改私有字段的内容。
另一个解决方案是提供一个包私有的setter,或者更好的方法是提供一个额外的构造函数来更改/初始化字段的值,并在生产环境中使用它时引发异常。 您可以检查很多不同的方式:
- 为生产环境中不在类路径上的测试类调用`Class.forName()`。
- 使用`StackWalker`并检查调用者是否为测试代码。
为什么我更喜欢第一个解决方案?
学习#4:不要仅仅因为可以就使用花哨的技术解决方案。 无聊通常会更好。
首先,因为这是最简单的方法,所以会将所有测试代码放入测试中。 应用程序代码中的设置程序或特殊构造函数本质上是测试代码,而生产代码中则包含它们的字节代码。 测试代码应在测试类中,生产代码应在生产类中。
第二个原因是设计功能在生产环境和测试环境中故意有所不同,这恰恰违背了测试的基本原理。 测试应在经济上尽可能模拟生产环境。 当测试环境不同时,您如何知道代码将在生产环境中正常工作? 你希望。 已经有许多环境因素可能会改变生产环境中的行为,并让bug仅在测试环境中表现出来而无声地保持休眠状态。 我们不需要额外的这类东西来使我们的测试更具风险。
摘要
编程和测试还有更多方面。 本文仅讨论讨论中出现的一小部分特定问题。 文章中还列出了一些重要的经验教训:
- 测试被测系统(SUT),而不是依赖项。 注意,实际上在测试某些依赖项的功能时,您可能会认为您正在测试SUT。 使用愚蠢而简单的模拟。
- 遵循TDD。 编写测试之前并与功能开发混在一起。 如果不只是因为您不这样做而已,那么至少在编写代码之前和同时考虑一下测试。 可测试的代码通常更好(不仅仅是测试)。
- 考虑一下其他程序员将如何使用您的代码。 想象一下,一个普通的程序员如何使用您的API并不仅为像您这样的天才产生代码的接口,他们比您更了解您的意图。
- 大三的时候,不要仅仅因为可以就去寻求理想的解决方案。 使用无聊且简单的解决方案。 您将知道您何时是大四学生:什么时候不再想用无聊的解决方案了。
翻译自: https://www.javacodegeeks.com/2019/07/inject-able-only-test.html