使用测试驱动开发开发状态机
由于状态机模型广泛用于嵌入式系统,本文探讨了在测试驱动开发 (TDD) 方法下开发状态机 (SM) 软件的几种策略。本出版物首先解释了基本的状态机概念和 TDD 技术。最后,介绍了使用TDD方法开发C语言状态机软件的简单有序的方法。
SM 模型由状态、转换和动作组成。虽然状态是系统或元素的条件,但转换是从一种状态到另一种状态的路径,通常由将前驱(源)状态与后续(目标)状态连接起来的相关事件发起。元素执行的实际行为以动作表示。
在 UML 状态机中,动作可能与进入状态、退出状态、转换本身或所谓的“内部转换”或“反应”相关联。所有状态机的形式(包括 UML 状态机)都普遍假设状态机在开始处理下一个事件之前完成了每个事件的处理。这种执行模型称为运行至完成 (RTC)。在这个模型中,动作可能需要时间,但任何挂起的事件都必须等到状态机完成——包括整个退出动作、转换动作和进入动作序列。
在讨论使用TDD开发状态机的策略之前,值得一提的是它的定义、重要性和应用。
首先,TDD 是一种增量式构建软件的技术。简而言之,没有首先编写失败的单元测试,就不会编写生产代码。测试很小。测试是自动化的。测试驱动是合乎逻辑的,即 TDD 实践者不是深入研究生产代码(将测试留待稍后),而是在测试中表达代码的期望行为。一旦测试失败,TDD 从业者就会编写代码,使测试通过。在 TDD 过程的核心,有一个由称为“TDD 微循环”的短步骤组成的重复循环。
以下列表中的 TDD 循环步骤基于 James Grenning 的“嵌入式 C 测试驱动开发”一书:
- 添加一个小测试。
- 运行所有测试,如果新测试失败,它甚至可能无法编译。
- 进行必要的小改动以通过测试。
- 运行所有测试并证明新测试是否通过。
- 重构以消除重复并提高表现力。
让我们使用图 1 中的图表找到一种更简单的方法来使用 TDD 开发状态机。状态机初始化时,从StateA开始 状态。一旦它收到 Alpha 事件,状态机转换到 StateB 通过依次执行 xStateA()、effect() 和 nStateB() 动作来改变状态。那么,如何测试图 1 的 SM 以确定它的行为是否适当?
图 1. 基本状态机(来源:VortexMakes)
测试如图 1 所示的 SM 的最传统和最简单的方法主要包括验证 SMUT(被测状态机)的状态转换表。这使得每个状态一个测试用例 ,其中 SMUT 由感兴趣的事件激发,以验证触发了哪些转换。同时,这意味着检查每个触发的转换的目标状态和执行的动作。如果动作足够复杂,则更适合为此制作特定的测试用例。 (使用单元测试测试状态机一文深入解释了这种策略)。
每个测试用例根据 xUnit 模式分为四个不同的阶段:
- 设置 建立测试的先决条件,例如 SM 的当前状态 (StateA ),要处理的事件 (Alpha ),以及预期的测试结果,即转换目标状态 (StateB ) 以及要执行的已排序操作列表(xStateA()、effect() 和 nStateB())。
- 锻炼 用 Alpha 刺激状态机 活动。
- 验证 检查获得的结果。
- 清理 测试后将被测状态机返回到其初始状态。它是可选的。
上面提到的策略足以使用 TDD 开发 SM。然而,在某些情况下,需要不止一个转换来检查功能。这是因为效果仅由于后续转换的一系列动作而变得可见,这意味着功能涉及 SMUT 的一组状态、事件和转换。在这些情况下,测试完整和功能性的场景比孤立的状态转换更合适。因此,测试用例比前面提到的策略更实用,更抽象。
让我们使用图 2 中的状态机来探索这个概念。
图 2. DoWhile 状态机(来源:VortexMakes)
图 2 显示了一个名为 DoWhile 的状态机,它模拟了一个类似于“do-while”的执行循环。 DoWhile 是使用 Yakindu 状态图工具绘制的。 “x =0”和“output =0”动作在创建 DoWhile 时被调用,这些动作设置了所有 DoWhile 属性的默认值。循环迭代次数必须通过‘x++’或‘x =(x> 0)设置? x-:x'动作。 ‘i =0’动作为循环建立初始条件。循环体由“i++”动作执行,它保持循环迭代,然后通过“i ==x”保护由选择伪状态评估终止条件。如果为真,循环体将被再次评估,依此类推。当终止条件变为假时,循环终止执行‘output =i’动作。
在开发新功能之前创建一个测试列表很有帮助。测试列表源自规范,它定义了应该做什么的最佳愿景。由于它不需要完美,以前的列表只是一个临时文件,以后可以修改。 DoWhile 的初始测试列表如下所示:
- SM初始化后所有数据默认设置
- 增加 X 属性
- 递减 X 属性
- 可以执行单个迭代循环
- 可以执行多次迭代循环
- 可以执行非迭代循环
- 检查越界值
为了开发 DoWhile 状态机,Ceedling 和 Unity 将与最简单但清晰的编程技术一起使用:使用“switch-case”语句。 Ceedling 是一个构建系统,用于为 C 项目生成完整的测试和构建环境; Unity 是用于 C 项目的轻量级便携式表达 C 语言测试工具。
两个文件代表这个状态机,DoWhile.h 和 DoWhile.c,所以它们是被测源代码。代码清单 1 显示了 test_DoWhile.c 文件的一个片段,它通过应用前面提到的策略实现了上面的测试列表。为了使本文保持简单,代码清单 1 仅显示了测试用例:“可以执行单个迭代循环”,它由 test_SingleIteration() 实现。代码和模型都可以在 https://github.com/leanfrancucci/sm-tdd.git 存储库中找到。
代码清单 1:单次迭代测试(来源:VortexMakes)
此测试验证 DoWhile 只能正确执行一次迭代。为此,test_SingleIteration() 通过调用 DoWhile_init()(第 96 行)初始化 DoWhile 状态机。它将 DoWhile 循环执行的迭代次数设置为零。之后,DoWhile 准备通过调用 DoWhile_dispatch() 来处理事件。要执行单次迭代,test_SingleIteration() 发送 Up 事件到 DoWhile(第 97 行)。此事件将迭代次数增加到 1。测试通过发送 Start 开始循环 事件(第 98 行),然后它发送 Alpha 事件,所以 DoWhile 执行一次迭代(第 99 行)。这是通过验证 out 属性的值是否等于执行的迭代次数来检查的(第 101 行)。最后,DoWhile 必须保持在 StateC 状态(第 102 行)。
为了证明 DoWhile 可以执行多次迭代,test_SingleIteration() 进行了扩展,如代码清单 2 所示。
代码清单 2:多次迭代测试(来源:VortexMakes)
代码清单 3 中显示的 test_NoneIteration() 测试检查 DoWhile 在收到 Alpha 时不执行任何迭代 没有事先通过Up设置迭代次数的事件 事件。
代码清单 3:非迭代测试(来源:VortexMakes)
尽管 DoWhile 的实现细节不是本文的目的,但代码清单 4 和代码清单 5 显示了 DoWhile.c 和 DoWhile.h 文件的一部分。这些文件实际上代表了使用 C 语言中的“switch-case”语句对 DoWhile 的演示实现。
代码清单 4:DoWhile 实现的片段(来源:VortexMakes)
代码清单 5:DoWhile 规范的片段(来源:VortexMakes)
上面介绍的两种策略都提供了使用 TDD 开发状态机软件的简单有序的方法,这是提高软件质量的最重要方法之一。
第一种策略主要包括验证 SMUT 的状态转换表。此方法使每个状态的测试用例 .另一种策略建议实现一个完整和功能性场景的测试用例 ,这经常涉及 SMUT 的一组状态、事件和动作。与第一种策略相比,第二种策略使测试更具功能性且不那么抽象。尽管这些策略独立于特定类型的系统、编程语言或工具,但它们在嵌入式系统中非常有用,因为它们中的许多都具有通常在一个或多个状态机中定义的基于状态的行为。
选择 C 语言是因为它是最流行的嵌入式软件开发语言之一。因此,为了在该语言中应用 TDD,选择了 Ceedling 和 Unity。总而言之,与传统方法相比,这些方法绝对允许开发人员以更简单有序的方式构建更灵活、可维护和可重用的软件。
嵌入式