深入理解TDD模式、原则与三角验证:编程开发的核心理论

5.2 TDD模式、原则和三角验证

深入理解TDD模式、原则与三角验证:编程开发的核心理论

本节我们继续讨论TDD的一些理论知识,然后下一节我们开始进入单元测试的实战部分。理论知识是最佳实践的充分且不必要的条件。即使没有理论知识,您仍然可以进行最佳实践;但掌握理论知识会让你事半功倍。所以,不要轻易跳过这一部分,它的内容也很有趣。

5.2.1 构造-操作-验证模式

不知道大家注意到没有,编程开发有一个简单的原则。这个道理非常简单、通俗,也符合我们传统文化中所说的“万变不离其宗”的理念。

大家应该有印象,刚学编程的时候,用C++写一段main函数代码,其实分为三个部分:输入、处理和输出。后来进入实际的项目开发后,随着系统变得越来越复杂、越来越庞大,你可能会对这个概念变得麻木。其实,无论是程序、模块、框架还是系统,本质上都可以分为三个部分:输入、输出、处理。这是最大的简单。

我们以PHP开源框架为例。框架需要解决参数输入的问题。这些参数可能是通过php-fpm运行方式传入的GET、POST、多媒体数据等,也可能是通过php-cli运行方式传入的命令行。参数、流、输入参数等;输出可以是普通页面渲染返回,也可以是JSON接口数据结果,也可以是异常失败时的4xx、5xx页面;处理环节就是前面介绍的动态调用,以及前后穿插的事件监听、钩子函数、回调函数等。

那么输入、处理和输出以及构造-操作-测试模式之间的关系是怎样的呢?或许细心的读者已经发现了其中的奥秘。这里我暂时不给出答案,留给大家一个思考。因为你从思考中获得的东西与别人告诉你的东西在深度上是不同的。

构建-操作-检查模式可以理解为:“当...做...时,您应该...”。其中,建设包括测试环境的搭建和测试数据的前期准备;操作是指对测试对象的调用,以及测试对象之间的通信和协助交互;最终测试是指业务规则的断言和需求的功能验证。

例如1+2=3,简单实现代码和对应的单元测试代码如下,其中单元测试用例可以按照构造-操作-检查的模式分三步编写。第一步,构造必要的参数,即上下文场景信息;第二步,执行运算,调用计数器的加法方法;第三步,自断言加法结果,检查1加2的结果是否等于3。

add($left, $right);
        // Step 3. 检验
        $this->assertSame(3, $sum);
    }
}
class Calculator {
    public function add($left, $right) {
        return $left + $right;
    }
}

这是编写测试用例的基本模式。如果您不知道如何编写单元测试代码,可以参考此模式。

5.2.2 第一原则

编写单元测试代码时,除了遵循组合-操作-检查模式外,还必须遵循FIRST原则。这五项原则是:

首先,单元测试的执行速度一定要快,能够快速运行并给出快速反馈。如果需要等待几分钟或者很长时间才能运行一个测试用例,对于开发人员本身来说是不友好的,更不用说蜗牛般的测试速度了。最好的速度是“闪杀”,即一个单元测试用例可以在1秒内完成。对于整个测试套件,时间限制可以稍微放宽,总的运行时间一般不要超过从饮水间取一杯水的时间。在我们曾经实现的一键测试中,就是这样的时间要求。它允许开发人员运行一键测试,然后端着一杯水回来查看哪里存在问题或错误。速度这个特性非常重要。

其次是独立性。在编写代码、设计系统时,我们都要求“高内聚、低耦合”,简化类之间的依赖关系,避免不必要的认知关系。同样,在编写单元测试代码时,也需要遵循独立性原则,即每个测试用例相互独立。如果你失败了,也不影响我的考试;如果我有异常,也不会影响你的测试。虽然xUnit家族可以支持指定测试用例之间的顺序和依赖关系,但那是一种有意识的设计和有意的行为,与这里提到的情况不同。

可重复性意味着相同的单元测试用例应该重复运行并得到相同的结果。这很容易理解。意思是对于一个特定的函数,如果我每次提供的参数都是一样的,那么这个函数每次返回给我的结果也应该是一样的。既然这么简单,我们已经做到了,为什么还要再提呢?这是因为,与之前的独立原则类似,很多时候我们会在无意识的层面上犯错误。接触过命令查询职责分离(CQRS)模型的同学都知道,接口可以分为两种,即命令操作和查询操作。命令操作有副作用,而查询操作是幂等的。当待验证的操作是命令操作时,内部状态或数据值将被改变。当重复执行单元测试时,可能会得到不同的结果。因为对象的方法通常不是纯函数。遵循这一原则很重要,以便反复执行并不断验证。

自我验证是单元测试的一大特点。基本上我们为此原则编写单元测试。与自我验证单元测试相结合,就是自动化单元测试。传统的开发方式基本上是编写代码、手动测试和调试。调试过程需要花费大量的时间,并且时间占比也非常高。至于手动测试,你可以想象的是,开发者写完代码后,打开浏览器,点击页面,输入一系列表单数据,或者在链接后手动添加必要的参数,然后进入页面他刚刚写道。开发模块并进行测试和验证。俗话说,机制有多种。想要拿到待测试的代码,就得翻越千山万水,重重困难,历经四面埋伏。更何况,你还要重复测试,一个月后回来重复验证。可想而知,手动验证会有多痛苦。代码自验证则不同。可以自动化,一旦自动化,离效率就不远了。因为开发者可以从重复、繁琐的手工操作中解脱出来,去做更有价值的事情。

最后一点是时效性。这很容易理解,但也非常重要,尤其是在进行测试驱动开发时。如果出现测试失败,应及时发现并报告,以便及时纠正,形成快速反馈的闭环机制。

下面简单介绍一下这五个原则。

5.2.3 三角形验证

接下来是三角测量验证。熟练掌握这个思维工具对于中型项目,甚至是核心底层模块、大型系统来说都是非常有启发的。一般来说,在辩论某种观点时,都会讨论正反两方面。进行单元测试时,一定要从多个维度进行验证、判断、检测,不要轻易放过任何可疑点。

测试路径也可以分为两类,一类是正常的Happy Path,另一类是Happy Path的反面,比如Sad Path、Bad Path或Exception Path。因此,在构建测试用例时,需要将预期的输入参数划分为等价类,并为这些不同类型的路径准备相应的测试数据。我们可以通过例子来加深对这方面的理解。

回到上面两个数字相加的单元测试,我们只是测试了正常的快乐路径,1加2等于3。但是如果客户端(这里的客户端指的是使用计算器计数器的客户端代码)通过的不是一个,会发生什么?数字而是字符串,或者我需要如何验证它?通过主动想象可能的场景并提前设计和开发,而不是等到最后出现问题然后被动地弥补,前者会具有更大的优势。字符串加法会发生什么取决于我们对计数器的定位和定位,以及它所处的上下文约束。如果我们要实现的是小学生使用的数字计算器,则字符串加法是不允许的,应该禁止。最后呢?我是否应该断言抛出了异常,是否应该断言返回了默认值,或者代码是否应该因参数类型错误而结束?通过这样一系列的自我质疑或集体智慧讨论,从不同维度出发,到提出问题,到如何断言,最后得出具体的实施方法。

还记得我们之前说过的话吗?在面向过程的世界中,如果函数无法处理,惯例是返回默认值 FALSE,并附有错误代码。在面向对象的世界中,如果失败,通常会抛出特定的异常。

因此,为了使计数器代码更健壮地处理非法参数,可以添加错误路径的测试用例。

class CalculatorTest extends TestCase {
    /**
     * @expectedException IllegalParameterException
     */
    public function testAddString() {
        // Step 1. 构造
        $left = "逆流而上";
        $right = "PHP企业级系统开发";
        // Step 2. 操作
        $calculator = new Calculator();
        $sum = $calculator->add($left, $right);
    }
}

这里使用了PHPUnit的expectedException注解,表示期望抛出的异常是IllegalParameterException。为了让这个测试通过,很容易根据约束来实现代码。以下是参考实现版本。

class Calculator {
    public function add($left, $right) {
        if (!is_numeric($left) || !is_numeric($right)) {
            throw new IllegalParameterException('参数非法');
        }
        return $left + $right;
    }
}
class IllegalParameterException extends Exception { }

现在,我们已经验证了利弊,但我们这里说的是三角验证,至少有第三个维度的测试。那么三维的测试是什么呢?三一直是一个普遍的数字。三角形验证不仅仅是三个不同维度的验证,还可以是四角、五角、八角的验证。我们要懂得举一反三,灵活变通。第三维度可以说是一个开放的验证。您可以根据当前情况、关注点和项目需求进行补充。一个方向是测试可预见的未来可扩展性。

比如这里的计数器,如果我们想要复数相加怎么办?现在不支持。如果将来需要支持,能否在不影响原有设计和原有提供接口的情况下,完美过渡、平滑升级、完全兼容?这是一个值得思考的问题。提前考虑可预见的可扩展性并不需要我们过度设计,而是要求我们编写和交付的代码具有一定的生存能力,能够在复杂多变的场景中快速迭代和演进。

简单回顾一下,为了使编写的测试代码更有条理,针对单个测试用例提出了构造-操作-检查模式。 FIRST 的这五个原则不仅适用于单个测试用例,也适用于大多数测试套件,并且作为一个整体具有指导意义。最后,三角测量验证从断言级别提出了更多启发式测试要求。

这三部分理论知识之前已经被别人总结分享过了。这里我只是分享一下我的一点经验。

你可能想看:
« 上一篇
下一篇 »

相关推荐

php实时推送,php实地培训

2026年10月05日

46阅读

php乱码出现问号的原因,php乱码出现问号的原因有哪些

2026年10月05日

38阅读

php一句话木马,一句话木马ctf

2026年10月05日

41阅读

php获取数组长度,php中如何获取数组的长度

2026年10月05日

43阅读

php面试题库及答案,php程序员面试真题库

2026年10月05日

41阅读

phpcontent-type修改,php源码二次修改需要多少钱

2026年10月05日

38阅读