加入任何一家新公司——拥有既定的文化和编程实践——都可能是一次令人畏惧的经历。 当我加入 Ansible 团队时,我决定写下我多年来学到的软件工程实践和原则,并努力实践这些原则。 这是一个非决定性的、非详尽的原则列表,应明智且灵活地应用。
我对测试充满热情,因为我相信良好的测试实践既可以确保最低的质量标准(可悲的是许多软件产品都缺乏这一点),又可以指导和塑造开发本身。 这些原则中的许多都与测试实践和理想有关。 其中一些原则是 Python 特有的,但大多数不是。 (对于 Python 开发人员,PEP 8 应该是您编程风格和指南的第一站。)
总的来说,我们程序员都是一群固执己见的人,而强烈的意见往往是极大热情的标志。 考虑到这一点,请随意不同意这些观点,我们可以在评论中讨论和辩论它们。
开发和测试最佳实践
1. YAGNI:“你不需要它”。 不要编写您认为将来可能需要但现在不需要的代码。 这是为想象中的未来用例编写代码,而且不可避免地,代码将变成死代码或需要重写,因为未来的用例总是与您想象的工作方式略有不同。
如果您为未来的用例添加代码,我将在代码审查中质疑它。 (例如,您可以而且必须设计 API 以允许未来的用例,但那是另一个问题。)
注释掉的代码也是如此; 如果一段注释掉的代码要发布,则不应该存在。 如果它是可能恢复的代码,请创建一个工单并引用代码删除的提交哈希值。 YAGNI 是敏捷编程的核心要素。 关于这方面的最佳参考是肯特·贝克 (Kent Beck) 的极限编程释义。
2. 测试不需要测试。 测试的基础设施、框架和库需要测试。 除非您真的需要,否则不要测试浏览器或外部库。 测试您编写的代码,而不是其他人的代码。
3. 第三次编写同一段代码是将其提取到通用帮助程序(并为其编写测试)的正确时机。 测试中的帮助程序函数不需要测试; 当您将它们分解并重用时,它们确实需要测试。 到您第三次编写类似代码时,您往往会对您正在解决的通用问题有一个清晰的了解。
4. 在 API 设计(面向外部和对象 API)方面:简单的事情应该简单;复杂的事情应该有可能。 首先为简单的情况设计,如果可能的话,最好是零配置或参数化。 为更复杂和灵活的用例添加选项或其他 API 方法(根据需要)。
5. 快速失败。 尽早检查输入并在无意义的输入或无效状态下失败,最好使用异常或错误响应,这将使您的调用者清楚地了解确切的问题。 但允许对您的代码进行“创新”用例(即,除非您真的需要,否则不要对输入验证进行类型检查)。
6. 单元测试测试行为单元,而不是实现单元。 在不更改行为或不必更改任何测试的情况下更改实现是目标,尽管并非总是可行。 因此,在可能的情况下,将您的测试对象视为黑盒,通过公共 API 进行测试,而不调用私有方法或修改状态。
对于某些复杂场景(例如,测试特定复杂状态下的行为以查找晦涩的错误),这可能是不可能的。 首先编写测试确实对此有所帮助,因为它迫使您在编写代码之前考虑代码的行为以及如何测试它。 首先进行测试鼓励更小、更模块化的代码单元,这通常意味着更好的代码。 关于开始采用“先测试”方法的良好参考是肯特·贝克 (Kent Beck) 的测试驱动开发。
7. 对于单元测试(包括测试基础设施测试),应测试所有代码路径。 100% 覆盖率是一个好的开始。 您无法覆盖所有可能的状态排列/组合(组合爆炸),因此这需要考虑。 只有在有非常好的理由的情况下,才应将代码路径保持未测试状态。 缺乏时间不是一个好理由,最终会花费更多时间。 可能的好理由包括:真正无法测试(以任何有意义的方式)、在实践中不可能命中,或在其他地方的测试中覆盖。 没有测试的代码是一种负担。 衡量覆盖率并拒绝降低覆盖率百分比的 PR 是确保您朝着正确的方向逐步前进的一种方法。
8. 代码是敌人:它可能会出错,并且需要维护。 少写代码。 删除代码。 不要编写您不需要的代码。
9. 不可避免地,代码注释会随着时间的推移而变成谎言。 实际上,当事情发生变化时,很少有人更新注释。 努力通过良好的命名实践和已知的编程风格使您的代码可读且自文档化。
无法变得明显的代码——解决一个晦涩的错误或不太可能的条件,或必要的优化——确实需要注释。 注释代码的意图,以及它为什么这样做,而不是它在做什么。 (顺便说一句,关于注释是谎言的这个特殊观点是有争议的。 我仍然认为它是正确的,并且编程实践的作者 Kernighan 和 Pike 也同意我的观点。)
10. 防御性地编写。 始终考虑什么可能出错,无效输入会发生什么,以及什么可能失败,这将帮助您在错误发生之前捕获许多错误。
11. 如果逻辑是无状态且无副作用的,则逻辑很容易进行单元测试。 将逻辑分解为单独的函数,而不是将逻辑混合到有状态和充满副作用的代码中。 将有状态代码和具有副作用的代码分离到更小的函数中,使它们更容易模拟和单元测试,而不会产生副作用。 (测试的开销越少,意味着测试速度越快。) 副作用确实需要测试,但测试一次并在其他任何地方模拟它们通常是一个好的模式。
12. 全局变量很糟糕。 函数比类型更好。 对象可能比复杂的数据结构更好。
13. 使用 Python 内置类型及其方法将比编写自己的类型更快(除非您是用 C 编写)。 如果性能是一个考虑因素,请尝试找出如何使用标准内置类型而不是自定义对象。
14. 依赖注入是一种有用的编码模式,可以清楚地了解您的依赖项是什么以及它们来自哪里。 (让对象、方法等将其依赖项作为参数接收,而不是实例化新对象本身。) 这确实使 API 签名更加复杂,因此这是一种权衡。 最终得到一个需要 10 个参数的方法来处理其所有依赖项,这是一个很好的迹象,表明您的代码无论如何都做得太多了。 关于依赖注入的权威文章是 Martin Fowler 的“控制反转容器和依赖注入模式”。
15. 您必须模拟才能测试代码的次数越多,您的代码就越糟糕。 您必须实例化和放置才能测试特定行为的代码越多,您的代码就越糟糕。 目标是小的可测试单元,以及更高级别的集成和功能测试,以测试单元是否正确协作。
16. 面向外部的 API 是“预先设计”和考虑未来用例真正重要的地方。 更改 API 对我们和我们的用户来说都是一种痛苦,并且创建向后不兼容性是很可怕的(尽管有时无法避免)。 仔细设计面向外部的 API,仍然坚持“简单的事情应该简单”的原则。
17. 如果函数或方法超过 30 行代码,请考虑将其分解。 一个好的最大模块大小约为 500 行。 测试文件往往比这更长。
18. 不要在对象构造函数中执行工作,这很难测试且令人惊讶。 不要将代码放在 __init__.py 中(导入命名空间除外)。 __init__.py 不是程序员通常期望找到代码的地方,因此它“令人惊讶”。
19. DRY(不要重复自己)在测试中比在生产代码中重要得多。 单个测试文件的可读性比可维护性(分解可重用块)更重要。 这是因为测试是单独执行和读取的,而不是它们本身是更大系统的一部分。 显然,过度重复意味着可以为方便起见创建可重用组件,但它远不如生产那么重要。
20. 只要您看到需要并有机会,就进行重构。 编程是关于抽象的,您的抽象越接近问题域,您的代码就越容易理解和维护。 随着系统有机地增长,它们需要改变结构以适应其不断扩展的用例。 系统超出了它们的抽象和结构,而不更改它们会变成技术债务,围绕它工作会更加痛苦(并且更慢且更易出错)。 将清理技术债务(重构)的成本包含在功能工作的估算中。 您让债务存在的时间越长,它积累的利息就越高。 关于重构和测试的一本伟大的书是迈克尔·费瑟斯 (Michael Feathers) 的高效处理遗留代码。
21. 首先使代码正确,其次才使其快速。 在处理性能问题时,始终在进行修复之前进行性能分析。 通常瓶颈与您想象的略有不同。 编写晦涩的代码因为它更快,只有在您进行性能分析并证明它实际上值得时才值得。 编写一个测试,对您正在分析的代码进行计时,可以更容易地知道您何时完成,并且可以将其留在测试套件中以防止性能回归。 (通常需要注意的是,添加计时代码总是会改变代码的性能特征,使性能工作成为更令人沮丧的任务之一。)
22. 更小、范围更紧密的单元测试在失败时提供更有价值的信息——它们会告诉您具体哪里出了问题。 一个建立半个系统来测试行为的测试需要更多的调查来确定哪里出了问题。 通常,运行时间超过 0.1 秒的测试不是单元测试。 没有所谓的慢速单元测试。 通过范围紧密的单元测试测试行为,您的测试充当代码的事实规范。 理想情况下,如果有人想理解您的代码,他们应该能够转向测试套件作为行为的“文档”。 关于单元测试实践的精彩演示是加里·伯恩哈特 (Gary Bernhardt) 的 快速测试,慢速测试。
23. “非我发明”并不像人们说的那么糟糕。 如果我们编写代码,那么我们就知道它做什么,我们知道如何维护它,并且我们可以自由地扩展和修改它以适应我们的需求。 这遵循 YAGNI 原则:我们有针对我们需要的用例的特定代码,而不是针对我们不需要的事物具有复杂性的通用代码。 另一方面,代码是敌人,拥有超过必要的代码是不好的。 在引入新的依赖项时,请考虑权衡。
24. 共享代码所有权是目标; 孤立的知识是不好的。 至少,这意味着讨论或记录设计决策和重要的实现决策。 代码审查是开始讨论设计决策的最糟糕时机,因为在编写代码后进行大刀阔斧的更改的惯性很难克服。 (当然,在审查时指出和更改设计错误仍然比从不更改要好。)
25. 生成器很棒! 对于迭代或重复执行,它们通常比有状态对象更短且更容易理解。 关于生成器的良好介绍是 David Beazley 的“系统程序员的生成器技巧”。
26. 让我们成为工程师! 让我们考虑设计和构建健壮且良好实现的系统,而不是发展有机的怪物。 然而,编程是一种平衡行为。 我们并不总是在建造火箭飞船。 过度工程(洋葱架构)与设计不足的代码一样难以使用。 几乎所有罗伯特·马丁 (Robert Martin) 的著作都值得一读,而整洁架构:软件结构和设计的工匠指南是关于这个主题的好资源。 设计模式是一本经典的编程书籍,每个工程师都应该阅读。
27. 间歇性失败的测试会侵蚀您的测试套件的价值,以至于最终每个人都忽略测试运行结果,因为总是有一些失败。 修复或删除间歇性失败的测试很痛苦,但值得付出努力。
28. 通常,尤其是在测试中,等待特定的更改,而不是任意休眠一段时间。 巫毒睡眠很难理解,并且会减慢您的测试套件的速度。
29. 始终至少看到您的测试失败一次。 故意放入一个错误并确保它失败,或者在被测行为完成之前运行测试。 否则,您不知道您是否真的在测试任何东西。 意外编写实际上不测试任何东西或永远不会失败的测试很容易。
30. 最后,给管理层提一点建议:持续不断的功能驱动是开发软件的可怕方式。 不让开发人员为他们的工作感到自豪,就无法确保您能从他们身上获得最好的成果。 不解决技术债务会减慢开发速度,并导致产品更差、错误更多。
感谢 Ansible 团队,特别是 Wayne Witzel,感谢他们对改进此列表中建议的原则提出的评论和建议。
想要摆脱阻碍您发挥最佳性能的 IT 流程和复杂性吗? 下载这本免费电子书:教大象跳舞。
7 条评论