抽象的不同层次
一个关于抽象的故事 抽象能力应该是程序员所需要具备的、再如何强调都不为过的、最重要的能力了。通过抽象,我们可以从纷纭错乱的、独立的、看似互不干涉的大量问题中解脱出来,找到一个方案,然后一举解决一类问题。 由于抽象是提取了众多具体事物的某种特征并进行了简化的结果,抽象的概念有时候反而会阻碍我们的思考(大概人的大脑更喜欢具体的事物吧)。这时候我们与需要具体的实例来帮助理解。也就是说,在思维活动中,不论是代码编写,还是方案设计时需要不断的从实例中提取概念并形成抽象,又需要不时的将抽象具体化成实例来验证理论。Bret Victor曾将这个过程比作抽象的梯子是非常有见地的 – 你需要不断的爬上爬下,从高处看一看概览,然后回到地上做一些实际而具体的动作。 另一个有趣的事实是,抽象可以有多个层次,当我们终于脱离了众多细节,建立起更高层的概念时,这些高级(high-level)的概念又变成了新的细节,而我们又可以基于这些细节作出新的抽象。就好比我们现在使用JavaScript或者Python来写程序的时候,基本上不需要考虑诸如寄存器的编码含义,机器指令长度等等细节,也无需关注内存的分配和释放等等细节,而只需关注业务功能即可。 这么说起来太过于抽象,我们来看一个具体的例子吧。 背景介绍 在这篇文章中,我希望通过对一个模拟项目的自动化测试(使用cypress)的重构来描述一些代码编写/重构时的模式。其中涉及到的重构方法比较常规,并没有高深的技巧,不过我觉得通过详细解析一个从可以工作的代码演进为较为整洁且便于理解/修改的测试用例,整个过程还是比较有趣的,希望你也可以有所收获。 所谓模拟项目,是对我从2019年1月到10月所经历的一个项目的模拟。纯粹从业务来说,该项目的业务规则/逻辑并不是十分复杂,不过技术细节上有很多值得反思的地方。我们的自动化测试也是经历了N个版本的迭代,最后我也成功将这篇文章中描述的模式应用到了该项目中,效果也是比较令人满意的。 这里的模拟应用Questionnaire是这样一个服务:通过类似调查问卷的方式来输入用户技能,角色,技术水平,项目偏好等等,根据后台的规则引擎来返回一些最适合用户的offer列表。后台的计算逻辑我们这里刻意将其淡化,这里只关注信息的采集部分。 应该注意的是,这里的调查问卷是动态的,比如如果对问题Q1选择了答案A,则下一个问题会变成Q303,如果选B的话则需要回答问题Q1024等。也就是说,问卷的路线有可能是多条的,而且每一条都是同等重要的(至少在测试里需要完整走通一遍)。 从界面上来看,我将整个问卷简化成了3步,实际场景里则可能是10+步,而且每一步中需要回答的问题长短不等,有些问题展现/隐藏还会依赖于前面某一步/某几步用户的答案等。用户只有完成了当前所在的步骤的所有必选项之后,才可以进入下一步(Next按钮才会可用)。 抽象101 - 函数 从功能测试编写的角度来看,将每个步骤视为一个独立的单元是一个合理的做法。每一个步骤需要做的事情也非常类似: 验证该步骤的标题是正确的 填写该步骤的所有必选项 点击下一步按钮 在写一个section的测试时,直观的通过cypress提供的API的结果大致为(假设这里的所有data-test标记我们已经在应用代码中打好了桩): it('Verify the basic information section', () => { cy.get('[data-test="step-title"]').contains('Basic information'); cy.get('[data-test="email-address"] input').type('juntao.qiu@gmail.com'); cy.get('[data-test="assignment"] input[value="assigned"]').check(); cy.get('button[data-test="next-button"]').click(); }); 第二个section的流程大同小异,都是先用selector找到页面元素,如果可以找到的话,通过cypress的API来模拟用户的实际操作: it('Verify the details section', () => { cy.get('[data-test="step-title"]').contains('More details'); cy.get('[data-test="ps-role"]').click(); cy.get('[data-value="dev"]').click(); cy.get('[data-test="developer"] input[value="frontend"]').check(); cy.get('[data-test="rating"] [for="rating-4"]').click(); cy.get('button[data-test="next-button"]').click(); }); 如果逐字对比的话,每行代码几乎都不一样,但是如果仔细看又会发现很多重复。消除这些重复显然可以让代码干净一些,可读性也可以得到提高。一个立即可以想到的重构方法是抽取函数,将验证标题和点击下一步抽取如下: const checkStepTitle = (title) => { cy.get("[data-test="step-title"]").contains(title); } const goToNextStep = () => { cy....