软件工程规则和测试积累的最佳实践,能帮助企业显著节省时间,减少痛点。良好的测试实践既可以确保质量标准,也可以指导和塑造每个程序员的发展。
小数今天推送的这篇文章,文中的许多原则都与测试实践和理想有关,有一些还是Python特有的。正如本文作者所说,程序员的那些固执己见,强烈的意见表达和坚持,往往是激情的最好表现。
- YAGNI:不要编写现在尚不需要,而将来可能需要的代码。现在的代码不可避免地会变成死代码或需要重写,因为未来的用例总是与设想的不同。如果把代码放在未来的用例中,在代码审查中要提出质疑。(例如,可以并且必须设计API来允许将来的用例,不过,这是另外一个问题。)
注解代码也是如此。如果一个注释代码块进入发行版,它就不应该存在。如果是可以恢复的代码,创建一个ticket,并引用代码删除的提交散列。YAGNI 是敏捷编程的核心元素。Kent Beck提供的极限编程解释(Extreme Programming Explained)就是最好的参考。
2.测试本身不需要测试。用于测试的基础架构,框架和库需要测试。除非确实需要,通常不要测试浏览器或外部库。测试你写的代码,而并非其他人的。
3.第三次编写相同代码时,是将其抽取到通用帮助器(并为其编写测试)的正确时机。测试中的辅助功能不需要测试; 当把它们分解出来并进行重用时,才需要测试。到第三次编写类似的代码时,会清楚地知道正在解决什么样的问题。
4.涉及到API设计:简单的事情简单化; 复杂的事情可能如果可能也简单化。首先设计一个简单的情况,如果可能,最好使用零配置或参数化。为更复杂和更灵活的用例添加选项或其他API方法。
5.快速失败。尽可能早地检查输入,并在无意义的输入或无效状态上失败,使用异常或错误响应,来确定问题。允许代码“创新”,也就是说,通常情况下,不要对输入验证进行类型检查。
- 单元测试是对行为单元的测试,而不是对实现单元。改变实现,而不是改变行为或被迫改变任何测试,当然这点并不是总能实现。在可能的情况下,将测试对象视为黑盒子,通过公共 API 进行测试,而无需调用私有方法或修改状态。
对于一些复杂的场景,比如一些特定复杂状态下,测试行为找到一个难以理解的错误。编写测试确实有助于解决这个问题,因为它会迫使你在编写代码之前,考虑清楚代码的行为以及如何测试。测试首先鼓励更小,更模块化的代码单元。
7.对于单元测试(包括测试基础结构测试),应该测试所有的代码路径。100%的覆盖率是一个很好的起点。你不能涵盖所有可能的排列/组合状态。在有很好的理由时,代码路径才能被测试。缺少时间不是一个好的理由,这往往导致最终花费更多时间。好的理由包括:真正无法测试,在实践中不可能实现,或者在测试中其他地方被覆盖。没有测试的代码要承担责任。衡量覆盖率,并拒绝降低覆盖率的百分比是朝正确方向前进的一种方法。
8.代码是敌人:可能出错,需要维护。少写代码。删除代码。不要编写你不需要的代码。
9.随着时间的推移,代码注释不可避免地成为谎言。实际上,很少有人在事情发生变化时更新评论。通过良好的命名实践和已知的编程风格,努力使代码有可读性和自我记录。
不明确的代码确实需要注释,比如围绕一个模糊的错误或不可能的条件,或者必要的优化。
10.防守地写。要总是想可能出现什么问题,无效输入会发生什么,什么可能导致失败,这有助于在错误发生前发现错误。
11.如果逻辑无状态且无副作用,则易于进行单元测试。将逻辑分解成独立的函数,而不是混合成有状态和副作用的代码。将有状态和带副作用的代码分离成更小的函数,可以更容易地模拟和单元测试。副作用确实需要测试,测试一次并在其他地方进行模拟,是比较好的做法。
12.Globals are bad。功能优于类型,对象比复杂的数据结构更好。
13.使用 Python 内置类型和方法比编写自己的类型更快(除非用C编写)。如果考虑性能,尝试解决如何使用标准内置类型而不是自定义对象。
依赖注入是一种有用的编程模式,可以清楚了解依赖关系。(让对象,方法等接收依赖关系作为参数,而不是自己实例化新的对象。)这是一种交换,使API签名更加复杂。如果完成依赖项需要使用10个参数,说明代码太多了。
越需要模拟测试代码,它就会越糟糕。需要实例化和放置的代码越多,用来测试特定的行为,代码就越糟糕。测试的目标是小型可测试单元,和更高级别的集成和功能测试,来测试这些单元是否正确地协作。
16.面向外部的 API 是要“预先设计”的,它关系到未来用例。改变 API 对自己和用户来说都是一种痛苦,而创造向后兼容性是非常可怕的,尽管有时不可避免。精心设计面向外部的API,仍然坚持“简单的事情简单化”的原则。
17.如果一个函数或方法超过30行代码,就要考虑分解。好的最大模块大小约为500线。测试文件往往比这个更长。
18.不要在难以测试的对象构造函数中工作。不要把代码放在init.py中。程序员不希望在init.py找到代码。
- DRY(Don’t Repeat Yourself不要重复自己)在测试中比在生产中重要得多。单个测试文件的可读性比可维护性更重要(突破可重用的块)。这是因为测试是单独执行和读取的,而不是作为大系统的一部分。过多的重复意味着可重用的组件可以方便地创建,但是与生产相比,测试的重要性低得多。
20.当看到需要并有机会时进行重构。编程是关于抽象的,抽象映射越接近问题域,代码就越容易理解和维护。随着系统有机地增长,需要为扩展用例而改变结构。系统超出抽象和结构,而不改变它们成为技术债务,这会更痛苦,更缓慢,更多bug。包括重构成本,包括对功能的预估。你把债务拖得越久,它积累的利息就越高。Michael Feathers 撰写了一本关于重构和测试的书籍,其中着重介绍了遗留代码。
21.正确的编写代码第一,速度第二。处理性能问题时,请务必在修复之前进行配置。瓶颈通常出乎你的意料。编写难懂的代码如果是为了速度更快,为此你进行了配置并证明这么做是值得的,那么也仅仅是速度上值得而已。编写一个测试,训练正在配置的代码,让它知道什么情况下会变得更简单,并且留在测试套件中,来防止性能下降。(通常情况下,添加计时代码总是会改变代码的性能特点,使性能变得经常差强人意。)
22.更小范围的单元测试在失败时会提供更有价值的信息。要判定错误,有一半的系统测试行为的测试需要依据更多的调查。一般来说,运行时间超过0.1秒的测试不是单元测试。没有所谓的慢单元测试。通过严格范围的单元测试测试行为,你的测试可以作为代码实际规范。理想情况下,如果有人想了解你的代码,应该能够将测试套件作为行为的“文档”。
- “这里没有发明”并不像人们所说的那么糟糕。如果我们编写代码,我们知道它的作用,知道如何维护它,可以自由地扩展和修改它。这是遵循了YAGNI的原则:我们有需要用例的特定代码,而并非通用代码,通用代码会给我们不需要的部分带来复杂性。另一方面,代码是敌人,拥有更多的代码没有好处,当引入新的依赖关系时需要考虑权衡。
24.共享代码所有权是目标。孤立的知识没什么好,这意味着至少要讨论、记录贯彻设计决策和实施决策。代码审查时开始讨论设计决策是最糟糕,因为在编写代码之后进行彻底的更改太难克服了。当然,在Review时指出和修改设计错误,比永远不总结不思考要好得多。
- Generators rock! 与有状态的对象相比,它们用于迭代或重复执行通常更短,更容易理解。
26.让我们成为工程师!设计、建立强大和良好实施的系统,而不是增加有机怪物。编程是一个平衡的行为,我们并不总是建造火箭飞船,过度设计(onion architecture洋葱架构)与设计不足的代码同样令人痛苦。罗伯特•马丁(Robert Martin)所做的几乎任何事情都值得一读,“ 干净架构:软件结构和设计指南”( Clean Architecture: A Craftsman’s Guide to Software Structure and Design )是这方面的好资源。设计模式( Design Patterns)是每个工程师应该阅读的经典编程书籍。
27.间歇性地失败的测试会侵蚀测试套件的价值,以至于最终每个人都忽略测试运行的结果。修复或删除间歇性失败的测试是痛苦的,但值得努力。
28.一般来说,尤其是在测试中,等待一个具体的改变,而不是任意时间内sleeping。Voodoo sleeps 很难理解,而且会放慢测试套件。
29.至少要看到你的测试失败过一次。将一个蓄意的 bug 放入让它失败,或在测试行为完成之前运行测试。否则,你不知道你真的在测试。偶尔写写测试代码实际并不测试任何东西,或写永远不会失败的测试,都是很容易的。
30.最后,管理要点:持续的功能研发是开发软件的一个可怕方法。不让开发人员在工作感到骄傲, 就不会从中获得最大的利益。不解决技术债务,会拖慢开发速度,并导致更糟糕的、更多bug的产品。
原文作者:Michael Foord
原文链接:
https://opensource.com/article/17/5/30-best-practices-software-development-and-testing