加入任何一家新公司——拥有既定的文化和编程实践——都可能是一次令人畏惧的经历。当我加入 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 条评论