您如何编写难以测试的错误代码?
对于不可测试的代码 ,您应该避免编写很少的反模式。 这些是:
我曾经对尝试证明包含所有这些反模式的代码的正确性感到不满(或高兴,因为它教会了我糟糕的代码有多糟糕 )。 它是用C ++编写的,不适合用于特定的嵌入式系统应用程序。 但是我重新创建了代码在JavaScript中的作用的要点,如下所示:
错误,笨拙的代码
首先让我们了解一下这段代码正在尝试做什么。
valIncrementer
函数采用名为val
的参数并返回val+1
,但有一些警告:
- 如果
val
是0到10之间的数字,则该函数返回val+1
。 - 如果
val
为0和10的范围之外,则该函数将返回0,如果val
小于0或10,如果val
大于10。 - 如果设置了
disabled
标志,则该函数仅返回val
而不更改它。
以上代码不仅难以阅读,而且难以测试。 为了证明代码的正确性,您必须运行一组测试以查看valIncrementer
函数为不同的输入提供的内容。 至少,我们需要测试val
是何时在有效范围(即0和10)之间的数字,以及边缘情况(小于0,小于等于0,等于10和小于等于10)大于10
res
等于:
![](https://i-blog.csdnimg.cn/blog_migrate/40930c80e6ae5571ead7ae9987db979e.png)
糟糕,笨拙的代码为我们提供了正确的答案,但是无法对处理整个逻辑一个方面的代码的不同部分进行单元测试。 例如,当您希望val
递增时,如何测试val
是否递增? 好吧,在开始时将nextVal
分配为val+1
,但不能保证保持这种状态。 您必须逐步遍历firstStageSetter1
, firstStageSetter2
和secondStageSetter
,以确保nextVal
不会被重新分配给其他对象。
更好,更可测试的代码
这就是我重写不良,笨拙的代码以提高代码的可维护性和可测试性的方式。
请注意,我的重写代码与糟糕,笨拙的代码相比要短得多。 我的重构有两个指导原则:
- 纯函数:纯函数时,它不会使用未作为计算参数提供的任何内容(无全局状态),也不会修改给定的任何内容。 纯函数总是只返回一个新副本(无突变)。 考虑纯函数的一种好方法是: 如果将此输入赋给一个函数,无论如何我总是会得到相同的输出吗? 如果答案为否,那么它可能不是纯函数。
- 解耦设计:开始编码之前,请考虑如何将要做的事情分解为一组简单的构建块,每个构建块都做一件非常简单的事情。 我更好的
valIncrementer
将其职责的各个方面委托给助手功能。 它分开了我应该做的代码在做它的代码,它可以是独立单元测试。
每个助手功能负责一个简单的任务,并且可能很短。 它只关心自己的论点,并且不受外部任何情况的影响。 valIncrementer
的唯一责任是将所有这些功能与一个if语句绑定在一起。
为什么我要使incrementedVal
成为一个单独的函数? 我不需要像这样的简单功能。 但是,假设这不只是增加一个数字那么简单。 假设此操作需要访问数据库,并且可能会有延迟和其他副作用。 我们想将不确定的代码与确定性的代码分开。
人们为什么要编写笨拙,无法测试的代码?
我看到两个原因:
- 技术债务:大多数人不打算编写错误的代码。 随着时间的推移,随着更多功能的添加,它会发生。 更改后重构代码通常是不可行的,尤其是当您的代码是API并且代码重写可能会破坏用户代码时。
- 不良文化:有时还有管理压力和计划/财务约束,以最大程度地减少对“工作”代码的其他部分的更改(其中包括许多常见的借口 , 请不要解决未中断的论点)。 本文指出:
事实证明,更高的灵活性导致开发人员编写了其他人实际上难以理解的代码。 很难决定是否应该因不足够聪明地掌握逻辑而感到羞愧,还是对不必要的复杂性感到恼火。 另一方面,在某些情况下,人们会觉得“特殊”于理解和应用对他人来说很难的概念。 在开发人员之间保持如此聪明的差异确实不利于团队动力,而复杂性总是导致这种情况。
如果您已经处在这种情况下,那么当您继续忽略代码重构和代码重写时,您会认识到您正在增加技术负担,并且有一个观点认为,对其进行更改并确保它仍然有效超过了成本/时间不重构的好处。 有可用的资源可以帮助您识别代码不可测试的症状以及如何重构的建议。 可能还有许多其他人也在为您遇到的同一问题而苦苦挣扎,因此有机会在开放源代码解决方案上与同一生态系统中的其他开发人员进行创意和协作。
如果您只是开始一个项目,请遵循以下准则:
- 确保设计好代码以考虑将来的修订。 这意味着将代码的可维护性和可测试性作为设计目标,并在设计代码时期望您的代码将来必须进行大量更改才能添加更多功能。
- 注意诸如面向对象编程之类的设计模式。 虽然面向对象编程的力量设计师要经过广泛的规划阶段,使得基线设计,用更少的缺陷对象,就已经下了很大的火近来对其“高度结构化的物理环境中后-其实变化是过于昂贵,如果不是不可能”。 面向对象编程的问题在《 编码员在工作》中的乔·阿姆斯特朗很好地总结了 彼得·西贝尔(Peter Siebel)表示:“面向对象语言的问题在于,它们具有随身携带的所有隐式环境。 您想要香蕉,但是得到的是一只大猩猩,里面盛着香蕉和整个丛林。”
- 尝试避免复杂的依赖项注入。 依赖注入是一种通过允许客户端在编译时配置其对服务的使用,从而将服务的客户端与服务的实现分离的技术 。 实际上,复杂的依赖项注入意味着在可以为客户端完全配置服务之前,需要在构造函数中指定许多内容。 依赖项注入越复杂,行为与环境的耦合就越紧密,从而使对服务实现的独立测试变得更加困难。 如果您的依赖项注入变得太复杂,则始终可以使用自己的依赖项注入将大型,复杂的服务拆分为较小,较简单的服务。
复杂的依赖项注入的问题在于,它可能导致“它对我的机器现象起作用”,如下面的这个可爱的卡通插图所示。
![](https://i-blog.csdnimg.cn/blog_migrate/1bd8d64e51f7c74ee131dbebddf59570.png)
谢谢阅读!
如果您觉得这个故事有趣,请随时鼓掌👏
对于那些有兴趣学习更多有关此主题的人,以下是一些后续阅读:
- Mishko的指南:编写可测试的代码 (也为PDF版本 )
- 单元测试,如何编写可测试的代码及其重要性
- 为什么大多数单元测试都是浪费的回应
- C ++中依赖注入的功能替代
- 面向对象的程序设计和依赖注入
- 面向对象编程是一场代价高昂的灾难,必须结束
- 再见面向对象编程
From: https://hackernoon.com/how-to-refactor-unwieldy-untestable-code-4a73d75cb80a