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