软件开发和测试的30个最佳实践

源自:转载 伯乐在线时间:2017-11-08次数:1

      加入一个企业文化和编程实践已经定型的新公司,可能会是一种令人沮丧的经历。当我加入Ansible 团队后,我决定整理我多年以来所学并为之奋斗的软件工程实践和准则。这是一个不明确的也不够详尽的准则列表,使用它们时需要智慧和灵活性。

      我对测试充满热情,因为我相信良好的测试实践既能确保满足最低质量标准(可悲的是许多软件产品做不到),并能指导和塑造开发本身。本文提到的这些准则,很多是与测试实践和理念相关的。其中一些准则针对 Python 的,但大多数不是。(对于 Python 开发者,PEP 8 应该是编程风格和指南的首先。)

 

开发和测试的最佳实践

1. YAGNI 原则:“You Aint Gonna Need It”。不要写你认为将来可能需要但现在不需要的代码。这是为假想的未来用例编码,这些代码将不可避免地变成死代码,或需要重写,因为未来结果总是与想象的稍有不同。

如果你写代码用于将来的用例,我将在代码评审中对其质疑。(你可能而且必须设计 API,并确保未来的用例可用,但这是不同的问题。)

这条原则也适用于被注释掉的代码;如果一个被注释掉代码块即将进入一个发布版本,那么它就不应该存在。如果代码可能要还原,请为代码删除创建一个问题单并引用提交对象的哈希字符串。YAGNI 原则是敏捷编程的核心要素,这个话题最好的参考书是 Kent Beck 写的《解析极限编程》(《Extreme Programming Explained》)。

2 . 测试不需要测试。用于测试需要的基础设施、框架和库需要测试。除非你真的需要不要测试浏览器或外部库。测试你写的代码,而不是别人的代码。

3.当第三次编写相同的代码时,也是将其提取成通用的辅助函数(并为其编写测试)的正确时机。测试中的辅助函数不需要测试;但当你将它们剔除出去然后再重用它们时,它们需要测试。到你第三次编写类似代码的时候,你通常会清楚地认识到你正在解决的通用问题的模型是什么。

4. 现在来谈谈 API 设计(面向外部的对象API):把简单的事情做简单了,复杂的事情自然成为可能。首先设计简单的用例,如果有可能最好是零配置或参数化。为更复杂和灵活的用例(如需要)增加选项或额外的 API 方法。

5. 快速失败。检查输入,如果遇到无意义输入或非法状态则尽早失败,最好是通过异常或错误响应使问题对调用者变得清晰。允许你的代码处理“有创意”的用例(例如,除非真的需要,否则在做输入验证的时候不要做类型检查)。

6.单元测试测的是行为单元,而不是实现单元。我们的目标是在改动实现的情况下不改动行为,也不必更新测试,尽管这个目标不总是能实现。因此在可能的情况下将测试对象视为黑盒,通过公共 API 测试,而不调用私有方法或玩弄状态位。

       在一些复杂的情况可能做不到,如在特定的复杂状态下测试行为,以找到一个偶发的错误。这一点对于写测试非常有帮助,因为它迫使你在写测试代码之前思考你代码的行为以及你将如何测试它。测试首先鼓励更小,更模块化的代码单元,这通常意味着好代码。关于“测试优先”方法有一本很好的入门参考书,就是 Kent Beck 写的《测试驱动开发》(《Test Driven Development by Example》)

7. 对于单元测试(包括测试基础设施测试),所有代码路径都应该被测到。100% 覆盖是一个好的开始。你不能覆盖所有可能状态的排列/组合(组合性爆炸),因此这个问题需要考虑。只有当有很好理由的情况下才允许有代码路径未经测试。没有时间不是一个好理由,最终会花费更多的时间。可能的好理由包括:真正的不可测(以任何有意义的方式),现实中不可能发生,或被其它测试覆盖。没有测试的代码是一种债。测量覆盖率和拒绝减少覆盖率 PR(拉取请求) 是确保你在正确的方向演进的一种方式。

8. 代码是敌人:它可能出错,并且需要维护。少写代码,删除代码,不要写你不需要的代码。

9. 随着时间的推移,代码注释不可避免地成为谎言。在现实中,很少有人在事情变化的时候更新注释。通过良好的命名法和已知的编程风格,努力使你的代码可读和自文档化。

对于那些晦涩的代码,一定要写注释,例如偶发错误或意外情况的变通方案,或者必要的优化。解释代码的意图和及其原因,而不是解释代码在做什么。(顺便说一句,有些观点认为注释变谎言是有争议的。我仍然认为这是正确的,《程序设计实践》(《The Practice of Programming》)的作者 Kernighan 和 Pike 同意我的观点。)

10. 防守思维。总是考虑什么会出错,无效的输入会引发什么,什么可能会失败,这将有助于你在许多错误发生之前发现他们。

11.无状态和无副作用的单元测试,其逻辑应简单。将逻辑分解成单独的函数,而不是将逻辑混合到有状态和充满副作用代码中。将有状态代码和有副作用代码,分为较小的更容易模拟的函数和无副作用地单元测试。(测试的开销越小意味着更快的测试)副作用确实需要测试,但是测试一次然后处处模拟它们通常是一个好的模式。

12. 全局变量不好。函数优于类型。对象可能比复杂的数据结构更好。

13.使用 Python 内置类型及其方法将比自己编写的类型运行快(除非你用C语言编写)。如果性能是一个考虑因素,请尝试弄懂如何使用标准的内置类型,而不是自定义对象。

14. 依赖注入是一个实用的编程模式,明确你的依赖是什么和它们来自哪里。(对象,方法等以参数的形式接收它们的依赖,而不是实例化新对象本身。)这确实让 API 签名更复杂,所以这里需要权衡。如果一个方法最后为所有的依赖设置了10个参数,那这是一个不错的信号,不管为什么你的代码做得太多。关于依赖注入的权威文章是 Martin Fowler 的《控制反转容器&依赖注入模式》(《Inversion of Control Containers and the Dependency Injection Pattern》)。

15. 需要模拟测试的代码越多,你的代码就越糟糕。为了测试一个特定的行为,需要实例化和牵扯的代码越多,代码越糟糕。我们的目标是小型可测试的单元,以及更高级别的集成和功能测试,以测试各单元是否配合正确。

16. 面向外部的 API 是“预先设计”——同时要考虑未来的用例——真正重要的地方。改变 API 对我们和用户来说是一种痛苦,造成向后不兼容是可怕的(尽管有时无法避免的)。设计面向外部的 API 要细心,仍然要坚持“把简单的事情做简单”的原则。

17.如果函数或方法超过 30 行代码,考虑分解它。一个良好的模块最大约 500 行。测试文件往往比这个要大。

18 .不要在对象构造函数中工作,这里很难测试而且经常发生意外。不要在__init__ .py中添加代码(导入命名空间除外)。__init__ .py不是程序员通常期望找代码的地方,所以它是个“惊喜”。

19. DRY(不要重复自己)在测试中没有在生产代码中要紧。单个测试文件的可读性比可维护性更重要(跳出模块复用的限制)。这是因为测试是单独被执行和阅读的,而且它们自己不是大系统的一部分。虽然在很多重复的时候创建可重用的组件更方便,但相较于产品代码,测试代码较少考虑。

20.每当你看到有需要有机会就重构。编程是关于抽象的,你的抽象映射越接近问题域,代码就越容易理解和维护。随着系统的有机增长,需要改变结构以扩大它们的用例。系统越来越多的抽象和结构,如果不改变它们就成为技术性的债务。它会是更加痛苦的工作(更慢,越来越多的错误)。在特性开发估计中请考虑清除技术债务(重构)的成本。你遗留债务的时间越长,积累的利息就越高。关于重构和和测试的一本很棒的书是 Michael Feathers 的《修改代码的艺术》(《Working Effectively with Legacy Code》)。

21. 代码正确为第一位,速度快第二位。在处理性能问题时,在修复错误之前先要做性能剖析。通常瓶颈不是你认为的那样。如果写晦涩的代码的唯一价值就是更快而且你已经做过性能剖析并证明了,那么它实际就是值得的。编写测试定期检测你要做性能剖析的代码,这样可以很容易让你知道你什么时候测试过。测试可以留在测试套件中,以防止性能退化。(通常情况下,添加定时代码总会改变代码的性能特性,使性能工作成为令人沮丧的任务之一。)

22. 当更小、范围有限的单元测试失败的时候,可以给出更多有价值的信息,告诉你具体是什么错误。如果一个测试牵涉了半个系统来测试行为,那么它需要更多的调查以确定什么是错误的。一般来说,运行超过 0.1 秒的测试不是单元测试。没有所谓的慢单元测试。用限定范围的单元测试测试行为,你的测试行为扮演了事实上的代码规范。理想情况下如果有人想了解你的代码,他们应该能够把测试套件转换为行为的“文档”。Gary Bernhardt 的《快测,慢测》(《Fast Test, Slow Test》)是关于单元测试实践的一篇很棒的演讲。

23. ”非我所创“不像人们说的那么坏。如果代码是我们写的,那么我们知道它是什么,我们知道如何维护它,在我们可以在适当的时候自由地扩展和修改它。这遵循了 YAGNI 原则:我们用那些适合我们需要用例的特定代码,而不用我们不需要的可以做复杂事情的通用代码。另一方面,代码是敌人,拥有必要的代码比拥有更多的代码更好。引入新的依赖关系时要权衡。

24. 共享代码所有权是我们目标;沉默的知识不是好知识。这意味着最低限度要讨论或记录设计决策和重要的实施决策。代码评审(Code Review)是开始讨论设计决策的最坏时刻,因为在代码编写后,很难彻底更改。(当然在评审时指出并修改设计错误比没有好。)

25. 生成器很棒!它们通常比迭代或重复执行的状态对象更短更容易理解。David Beazley的《系统程序员的生成器诀窍》(《Generator Tricks for Systems Programmers》)是关于生成器一个很好的介绍。

26.让我们成为工程师!让我们考虑设计、构建健壮并实现良好的系统,而不是做膨胀的有机怪物。然而编程是一种平衡。我们并不总是建造火箭。过度设计(洋葱架构)同设计不完善的代码一样,处理起来非常痛苦。Robert Martin 的作品几乎都值得一读,《架构之洁:一个工匠的软件结构和设计指南》(《Clean Architecture: A Craftsman’s Guide to Software Structure and Design》)是这个话题一个很好的资源。《设计模式》(《Design Patterns》)是每一位工程师都应该阅读的经典编程书。

27. 间歇失败的测试会侵蚀测试套件的价值,以至于最终每个人都忽略测试运行结果,因为总有一些失败的事情。修复或删除间歇性失败测试是痛苦的,但这些努力是值得的。

28. 一般来说,特别是在测试中,在需要等待一个特定的变化时候不要采用休眠随机时间的方式。Voodoo(Python 库) 的 sleeps 很难理解而且使你的测试套件变慢。

29. 至少让你的测试失败一次。故意加入一个错误,并确保它失败,或在测试的行为不完整的情况下运行测试。否则你不知道你真的在测试什么。瞎写的测试实际上不能测试任何东西或它很可能永远不会失败。

30. 最后一点:只关注特性改进是开发软件的一种可怕方式。如果开发者的工作不能确保最好的结果,那么不要让他们为自己的工作感到自豪。不处理技术债务会使开发变慢并最终导致产品更糟,问题更多。

感谢 Ansible 团队,尤其是 Wayne Witzel,为改善这个列表中的准则而提出的意见和建议。