没有单元测试的验证在学习编程和业务开发的工程中,我们讨论了一段时间:单元测试有用吗?这样讨论的主要原因是我们好像在使用单元测试的时候,项目也可以很好的跑起来。小到毕业设计的内容,大到十几个人的团队。我们设计项目,分析需求,然后根据设计结果写代码,然后进行接口或者业务执行上面的测试,让我们知道写的代码可以完美的完成计划的内容,然后问测试学生帮助我们进行代码测试,以确保他们确实按照计划进行。终于代码上线了,可喜可贺。在最终问题发生之前,似乎没有任何问题。当然我们在开发的时候会测试项目的功能,比如使用main来验证指定的代码块,或者使用postman来测试验证我们设计的界面。可能中间有一些数据库修改,比如模拟订单数据,或者模拟用户注册。这些方法一定程度上可以满足现阶段的功能需求,不然也不会有那么多“单元测试真的有用吗?”的声音。所以有什么问题?问题是你不能总保证“当前业务测试”能覆盖你这一时期提供的功能点,而且即使测试同学保存了之前所有测试用例的自动化测试内容,也不能真正保证您的系统完好无损,因为业务功能和软件功能之间存在差距。虽然是一张搞笑图,但是准确的击中了我想说的:为用例设计的功能测试并不能保证你的“系统”是正常的。测试驱动开发我们在项目开发中一般会进行功能测试,可以保证业务流程在当期的进行。但是即使功能测试流程包含了过去所有的功能,也只能保证业务流程是正确的,并不能保证你的设计在以后的扩展中也是正确的(比如业务可能只需要正常的流程,但是有无异常流程需要)。因此,如果代码能够实现所有计划的功能,则由开发者编写相应的测试模块。因为你是开发人员,你知道你所有的逻辑组合是什么,按照这个要求写的测试代码可以长期测试你的系统。就是这样:TDD(TestDrivenDevelopment)测试驱动开发中最重要的原则是:先写单元测试再写业务代码。这条规则的目的是:不要编写没有单元测试的代码。其实我们在写功能业务的时候,一般都会假设一个入口,然后经过一段时间的逻辑处理,最后返回一个结果。先写单元测试的目的是先把你的假设直接放到代码中,然后在后面的编程过程中就可以忽略这部分假设,专注于逻辑编写,即使你最后忘记了前面的也没关系如果你假设它,因为你已经将它写入代码。进一步分解这个标准可以细化为三个原则:在编写失败的单元测试之前不要编写生产代码。您只能编写碰巧失败的单元测试,即使它们无法编译。只编写足以通过当前失败测试的生产代码。根据这三个原则细分,我们可以把我们写一个逻辑的步骤变成:写一个刚好失败的单元测试,然后用刚好满足逻辑的生产代码来满足它。这么小的周期可能只会在一两分钟内发生一次。借助IDEA等现代IDE,可以在测试包下的相同包路径下创建相应的测试方法,大大加快了单元测试的编写时间。并且通过恰到好处的异常,让每一个业务逻辑都得到控制,恰到好处满足,这样每一个生产代码的写法就不会发散过大。因为如果你过度设计你的生产代码,你还需要相应的单元测试代码来确保你设计的适当性。如果按照这个周期来写,我们在写业务代码的同时,只需要十多秒就可以完成单元测试的编写。单元测试可以完全覆盖业务单元。但是,随着业务代码的增多,测试代码的数量也会急剧增加,其相应的管理也是一个挑战。系统演进的保障回到最开始的例子,我们说在一些团队中,我们总觉得单元测试效率低下,会影响业务上线的速度。还可以说,在最终问题出现之前,这种方法看起来还不错。这就是最终的问题:重构。这里所说的重构不一定是大规模的整体系统重构。我们在上一篇文章《如何阻止软件退化》中提到:要让软件设计的质量不下降,需要在每次需求变化时根据变化点调整原程序的设计结构。而当我们相对于原有的程序结构进行调整时,我们不能保证对代码的修改一定会按预期工作,也不能保证系统中的某个修改点是否会影响到系统的其他部分。比如:当你修改支付路由的时候,如果发生意外,其他支付方式会失效,但是如果你保证功能正常,你需要对所有的支付逻辑进行功能测试,仍然可能存在遗漏功能点(这是个人经验)。因为我们怕新增加的功能带来更多的bug,导致加班,最后的结论是,我们可能会抗拒功能结构的调整,成为所谓的“狗屎上的雕刻”。所以从这个角度来说,如果没有单元测试,软件必然会线性退化。相反,如果我们的系统包含单元测试。我们不用担心代码的修改。每一次调整都能通过那些“恰到好处”的单元测试,所以无论你如何重构设计模式,你都不用担心引入新的不可预知的缺陷。所以,当有单元测试的时候,我们的系统可以有进一步的可维护性和可扩展性,也有系统演进的可能。应该重视的单元测试我们需要单元测试来保证系统功能的可扩展性和可维护性。但这并不意味着只要有单元测试。事实上,我们应该像关注生产代码一样关注单元测试。原因很简单:单元测试代码也可能因功能调整而损坏。如果它损坏且难以维护,则没有人会想要修改它。最终的结果是我们没有使用单元测试,然后失去了代码的可扩展性。因此,测试代码必须和业务代码的修改同时修改,单元测试的编写也不能小看,因为单元测试只运行在测试环境中。我们还需要让单元测试的代码足够干净,以便于维护。在测试测试的逻辑单元时,重要的是要反映当前的测试内容,而让别人理解测试内容最重要的是测试“可读性”。如果单元测试代码中塞满了一长串业务逻辑或断言内容,阅读起来会非常困难。为了避免开发者被淹没在代码的细节中,有一种比较认可的单元测试构建方式:BUILD-OPERATE-CHECK(BUILD-OPERATE-CHECK),使用give-when-then命名方式进行命名。举个例子(这里直接用CLEANCODE的例子):givenPages(xxx);whenRequestIsIssued(xxx);thenResponseShouldBeXML();其中,第一部分将构建测试数据的内容划分为给定开头的方法;第二部分将操作测试数据的内容封装到以when开头的方法中;第三部分将检查是否得到预期结果的操作封装到then开头的方法中。这样就屏蔽了大部分代码细节,通过方法名直接描述了测试的前置条件、处理过程、判断结果。同时,当我们涉及到一些复杂流程的判断时,可以单独编写一些额外的单元测试方法来支持单元测试。这可以让人改变,让人们可以快速理解单元测试的逻辑。可以放松的部分是我们需要单元测试来保持代码干净,并且需要给予它与生产代码相同的关注。但这并不意味着我们的测试代码与生产代码完全相同。因为单元测试的准则就是要有可读的代码,准确地描述所关注的测试函数的边界。所以有些东西不需要和生产代码保持一致。其中最明显的是性能要求。我们需要在各种在线代码中优化系统性能,但是单元测试代码在测试环境中运行,单个逻辑一次只执行一次。对于单元测试,0.1ms和1ms之间的逻辑差距可能并不明显。在这种情况下,我们可能会选择一种表达能力更强的方法来编写项目。例如,使用“+”号拼接字符串。我们通常使用StringBuilder,但是不得不说,我们可以直接使用“+”来拼接实现可读性更高一点。另外还有一些异步函数可以使用序列化来验证每一步的结果。单一概念为了保证每个单元测试中逻辑的可读性,我们希望每个单元测试只测试一个概念,这样就可以用一套give-when-then方法来描述这个测试概念。当我们发现单元测试中有多个概念时,我们会把它们拆开单独测试。这样,当多个概念在一个单元测试方法中聚合时,复合概念会产生犹豫,并掩盖一些缺失的测试点。它还确保了单元测试的可读性。除了其他原则,单元测试还必须保证:快速性:单元测试可以快速执行,支持频繁测试。独立性:单元测试之间互不依赖,随时按任何顺序执行。可重复性:单元测试可以重复执行并得到统一的结果,否则总会有功能失败的借口。可验证:单元测试应该通过布尔值明确表达检测结果,而不是通过日志等其他辅助手段。时效性:先写再开始写业务代码,让业务代码覆盖测试。最后,本文讨论了单元测试的必要性以及单元测试中的一些要点。有人认为单元测试会影响开发效率,但从项目经理的角度来看,只有单元测试的项目才有可能不断进步。
