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