討論交流
xiaotong 2022-09-16 132 0 0 0 0
程序员,程序开发,Craftsmanship的基础纪律TDD重构简单设计结对/协同编程验收测试关于TDDTDD必须遵循以下纪律:创建测试集,方便重构,并且其可信程度达到系统可部署水平,意即,若测试集通过,系统就可以部署…

Craftsmanship的基础纪律

  • TDD
  • 重构
  • 简单设计
  • 结对/协同编程
  • 验收测试

关于TDD


TDD必须遵循以下纪律:

  1. 创建测试集,方便重构,并且其可信程度达到系统可部署水平,意即,若测试集通过,系统就可以部署
  2. 创建足够解耦、可测试、可重复的代码
  3. 创建极短的反馈循环周期,保障代码编写工作以稳定的节奏和产出运行
  4. 创建相互解耦的测试代码和生产代码,以便分别维护,无需在两者之间重复改动

TDD三法则:

  • 三法则是什么
    1. 在编写因为缺少生产代码而必然会失败的测试前,绝不编写生产代码
    2. 只写刚好导致失败或通不过编译的测试,编写生产代码来解决失败问题
    3. 只写刚好能解决当前测试失败问题的生产代码。测试通过后,立即写其他测试代码
  • 三法则的好处
    • 你将花更多时间写能正常运行的代码,花更少的时间调试不能正常运行的代码
    • 你将产出一套几乎完美的低层级文档
    • 好玩 -- 起码有动力
    • 你将产出一套测试集,领你有信心部署系统
    • 你将创建较少耦合的设计

红灯 -> 绿灯 -> 重构 -> 红灯

  • 让测试通过专注于行为,重构专注于结构
  • 为什么重构是一个单独的环节?专注于行为就不能专注于结构
  • 重构是一种持续行为,每走一圈TDD循环,你都会做清理工作
  • 重构就类似上完洗手间后洗手,那就是一种你始终会做的体面行为

TDD实操规则

  1. 先编写测试,能逼着你写出已知道自己要写的代码
  2. 让测试失败,让测试通过,清理代码
  3. 别挖金子。刚开始尝试TDD时,你会倾向先解决比较难或比较有趣的问题,不要这样,先专注于测试周边行为,比如判空、大小等等,为什么?因为太早去挖金子,就有可能忽略周边细节,也就会失去这些周边细节带来的简化机会
  4. 先写最简单、最具体、最基本、且易失败的测试
  5. 能泛化生产代码的时候就泛化
  6. 如果生产代码让人感觉不对,在继续之前,先修正设计问题
  7. 在开始下一个更复杂情况的测试前,先穷尽简单情况测试
  8. 如果你必须写很多实现代码才能让测试通过,删掉测试,写一个更容易通过的简单测试。TDD新手常发现他们能写出相当不错的测试,然后意识到,让测试通过的唯一方法是写出本该由测试推动实现的全套算法,这种情况往往意味你的步子迈的太大了,你需要先写更简单的测试
  9. 从容不迫,循序渐进地完成所有测试
  10. 不要在测试中添加测试不需要的东西,你可以借助一下5种工具:
  • Dummy: 它创建一个什么也不做的接口,当被测试的函数需要接受对象作为参数,但测试本身的逻辑不需要那个对象时,使用Dummy
  • Stub:Stub是一种Dummy, 它也什么不做,不过Stub函数并不返回0或者null,而是返回能推动测试沿预定被测试的值
  • Spy:Spy是一种Stub,它在Stub的基础上,还能记住对它所做的事,并允许询问(比如记住登录失败了几次)。Spy的功用是保证被测试的算法行为正确。但是,使用Spy有风险,因为它耦合了测试本身与被测试函数的实现
  • Mock:Mock是一种Spy,它在Spy的基础上,还知道我们的预期,基于这些预期,判断测试是否通过,Mock和生产代码的耦合比Spy更加严重
  • Fake: Fake不同于Dummy, Sub, Spy, Mock, 它是一种模拟器,它模拟实现基础业务规则。Fake的问题是,总有新的测试条件出现,Fake代码随着测试条件不断增加变得又大又复杂,以至于需要专门为它写测试
  1. 别在测试中使用生产环境的数据
  2. 将测试的结构与生产代码的结构解耦
  • 将测试代码的模块与生产代码的模块一一对应特别有害
  • 如果生产代码有A->B->C这种依赖关系,为了测试A,测试代码可能需要B,C模块中的类来实例化A,或者在A的方法调用中需要使用B和C中的类,有经验的程序员会通过在A中声明多态接口来屏蔽B和C,从而让测试更加健壮(这是一个通过测试改进设计的好例子)
  • 不必担心B和C没有得到测试,如果B,C执行某个A的重要功能,那么该功能必定能通过对A的测试而得到覆盖
  • 在大型系统中,这种模式会一再重复,会有很多模块,每一个都被自己的表层模块或接口所保护,不被测试代码直接调用
  1. 测试越具体,生产代码越通用
  2. 泛化代码的通用顺序如下,按这个顺序常常能得到好的代码实现
  • {} -> Nil
  • Nil -> Constant
  • Constant -> Variable
  • Unconditional -> Selection
  • Value -> List
  • Selection -> Iteration (if可以看做是while的特例)
  • Statement -> Recurrsion
  • Value -> Mutated Value
  1. 尽量不使用调试器
  2. 两分钱:强烈推荐观看随书所带的视频,亲眼围观Bob大叔具体是如何用TDD的方式写码的,看一次视频比看100遍实操规则都有启发

TDD实操难题

  • 数据库测试
    • 数据库测试第一原则:不要测试数据库。我们真正要测试的是发给数据库的命令是否正确
    • 数据库测试第二原则:将数据库从业务规则解耦。用一个DBGateway接口把数据库和业务规则隔离,这样就可以用一个DBGateway的Spy来测试业务规则了
    • 不要使用生产数据库做测试,创建刚好够用的测试数据库,证明测试有效。备份这个数据库,在运行测试前,恢复数据库,保证测试总是操作同样的数据
  • 测试GUI
    • 不要测试GUI,测试GUI之外的其它部分,GUI比你想象的要小。
    • 使用自动化测试,唯一不易测试的就是GUI本身,所以我们要吧它做小
    • 用一个Presenter接口来隔离GUI和业务规则。Presenter创建数据(View Model)给GUI,GUI负责把View Model绘制在屏幕上。Presenter决定每个按钮和菜单项的状态。如果要在屏幕上显示一张数字表格,是Presenter创建出容纳字符串的表格,并正确地格式化和安放这些字符串
    • 可以用一个Presenter的Spy来测试业务规则,而Presenter则可以通过检查其输出的ViewModel来测试
  • 测试硬件
    • 解决方案类似GUI测试,让涉及硬件的部分尽量简单,只是机械执行硬件指令。用一个Presenter对业务隔离硬件细节,用另一个通讯接口隔离对硬件指令的具体操作,Presenter按约定的数据格式把业务数据压扁为基本的字符串和标识,然后由通讯接口操作具体硬件进行通讯

TDD不确定性原理

  • 不确定性:无论测试过多少值,都无法排除有漏网之鱼的可能
  • 如果你要求测试确定性足够高,那么测试与实现就不可避免地相互耦合,而且测试将会变得脆弱,也即生产代码的微小改动就会破坏大量的测试
  • 测试确定性越高,测试就越脆弱;测试越健壮,测试的确定性就越低
  • 如何平衡是一个取舍,如果你更注重确定性(伦敦派),大概会在测试中大量使用Spy,也会容忍躲不开的测试脆弱问题。如果你注重灵活性(芝加哥派),比起Spy,你会更喜欢用值测试和属性测试,也会容忍测试的不确定性
    • 伦敦派遵循由外而内(outside-in)实践,他们往往从用户界面着手,一次只做一个用例,逐渐靠近业务规则,他们在每个边界使用Mock和Spy,证明用以与内层沟通的算法。最终,他们遇到业务规则,实现它,连接到数据库,转身,使用Mock和Spy做测试,反向回到用户界面
    • 芝加哥派遵循由内而外(inside-out)实践,他们倾向从业务规则着手,再向着用户界面推进。他们不会从头到尾在满足整个用例之后才开始下一个用例。他们会使用值和属性测试实现几个业务规则,与用户界面完全无涉。用户界面,UI与业务规则之间的层,都会在必要的时候再实现。芝加哥派人士不会将业务规则直接连接到数据库
  • Bob大叔的选择:当测试跨越边界时是伦敦派,没有跨越边界时是芝加哥派

其他:

  • 调试器用得越舒服,你就越知道自己可能做错了点什么
  • 对于描述系统动机,测试并不特别出色,他们并非高层次文档。但是,在最低层级上,它们比其他任何形式的文档都好。它们本身就是代码,你明白,代码会说真话
  • 如果测试集通过了,但你还是没有信心部署系统,那么这种测试集有何用?
  • 你写的每个测试都是描述系统行为的有限状态机

关于重构


  • 重构不改变行为
  • 每次重构都应该很小,小到不需要调试的程度
  • 最最常用的重构技巧
    • 要一直抽取到没有什么可抽为止,每个函数只应该干一件事
    • 函数的名字与其涵盖范围成反比,公有函数的名称应该相对短,私有函数的名称应该更长一些
    • 函数中的每一行都应该处于相同的抽象层级,而且刚好比函数名所在的抽象层级低一层
    • 重命名
    • 方法抽取
    • 变量抽取
    • 类成员字段抽取
  • 更多的重构技巧,强烈建议参考马丁.福勒的《重构》一书
  • 妥当组织测试集,以便能在重构时快速运行相关部分的测试子集,避免测试花太多时间打断节奏,然后每1小时左右运行一次完整测试集
  • 如果重构需要几个小时或者几天来完成,那么就将重构切分成小块,一边持续推进,一边保持测试通过
  • 在开始一系列重构时,确保在代码仓库中打好标签,以便在需要时回滚

关于简单设计


  • 最佳的系统设计就是满足所有的需求特性,同时提供最大修改灵活性的最简设计
  • 简单不代表容易,简单意味着非联接,而非联接并不简单
  • 简单设计是基于高层策略不关心低层级细节实现的设计,实现这种设计的基本手段是抽象,抽象放大本质因素,消除无关因素
  • 简单设计4大法则:
    • 通过测试
      • 把100%测试覆盖率作为目标,即使做不到,也要努力不断逼近,因为可测试的代码就是解耦的代码
      • 测试不但能推动你做出解耦和坚固的设计,还能让你持续改进这些设计
    • 揭示意图 
    • 没有重复
      • 并非所有的重复都应该消除,有些情况下,两段代码可能一模一样,但将会处于截然不同的原因而被修改,这种重复应该被允许存在,他们将各自演化,最终变得完全不同
    • 最少元素

关于结对/协同编程


  • 它保证知识在整个团队传播
  • 它通常是短时间、非正式、间歇性的行为,大体占总开发时间的20% ~ 70%
  • 每次持续时间10分钟到1个小时,更短或更长都没有什么帮助
  • 研究表明,结对编程中,整体生产力下降大约15%,而不是人们担心的50%,同时,结对编程制造的Bug会减少15%,而且更重要的是,每个功能的代码量会减少15%

Craftsmanship的标准和操守节选

  • Software是个合成词,意思是『灵活的产品』。软件存在的全部理由是为了让我们能快速和容易地改变机器的行为。如果我们创建的软件难以改变,我们就破坏了软件存在的根本理由
  • Alpha和Beta测试可能适宜用来确定与用户的功能兼容性,但不应该用来消除编码缺陷
  • 想让软件随时间的推移变得更好,需要什么?需要意志,需要态度,需要遵守我们确知有效的纪律
  • 软件不仅应该没有行为上的bug,还应该没有结构(设计)上的Bug
  • 只要使代码难以阅读、难以理解、难以修改或难以重用,就是结构性的Bug
  • QA不适合放在研发过程的末端,QA应该放在过程的开始处。QA的工作不是找到缺陷,那是程序员的工作。QA的工作是用测试来指定系统的行为。给出足够细化的测试,方便排除最终系统中的Bug,这些测试应该由程序员而不是QA来执行
  • 自动化测试不应该通过用户界面来测试业务规则
  • 应该考虑向用户界面提供预设值的占位符来替换业务规则,以保持用户界面和业务规则的隔离,这样也保证了用户界面的测试是快速和明确的
  • 排期中的预估不应该是一个确定值,而应该是一个概率分布,比如5%的可能在周5完成,50%可能在下周5前完成,95%可能在下下周5前完成
  • 程序员能说的话里最重要的一句就是:『不!』,在适当的时候,适当的环境下,这个答案可以为你的雇主节省下大量的金钱,并可以防止可怕的失败和尴尬
  • 混乱的软件就是有害的软件,在软件中制造混乱,降低了你了解软件会做什么的能力,以及规避危害的能力
  • 在初创企业中,有一件事是绝对肯定的,那就是你正在创造错误的产品
  • 需求最可能变更的时候就是在项目的开始阶段,所以在一开始系统就要保持整洁,这样才能保证对系统快速修改,快速发布
  • 是什么让一个系统逐渐腐化成了无价值的系统?源代码的依赖性!如何解决?管理依赖性!如何管理?使用SOLID原则。结构的价值大于行为的价值,结构的价值取决于良好的依赖管理,良好的依赖管理来自SOLID原则,因此整个系统的价值就取决于对SOLID原则的正确应用
  • 问题有两种,紧急的和重要的。紧急的不重要,而重要的永远不紧急。紧迫性越高,关键性越小
  • 尽量减少分支,如果一个功能开发好了,但是公司还不决定上线,也应该把该功能合并进主干,然后通过开关在线上屏蔽这个功能
  • 不要把测试覆盖率作为一个考核指标,因为很容易用作弊手段来满足考核,我们应该把它作为一个帮助我们改进的衡量标准
  • 保持代码灵活性的最好的方法之一,就是定期对其进行修改
  • 保持高生产力:保持开发环境的效率 + 注意力管理 + 时间管理
  • 能不开的会不开,必须要参加,只参加相关的部分,提前告诉组织者你会提前离开,坐在靠门口的地方
  • 大多数排期都是谎言,他们都是由一个已知的截止日倒推而来的
  • 如果你给出了一个精确日期的排期,你实际不是在预估,而是在承诺,承诺了,就必须做到
  • 永远保持学习

Tag: 程序员 程序开发
歡迎評論
未登錄,
請先 [ 註冊 ] or [ 登錄 ]
(一分鍾即可完成註冊!)
返回首頁     ·   返回[討論交流]   ·   返回頂部