21

我继承了一个大型且相当复杂的状态机。它有 31 种可能的状态,都是真正需要的(大业务流程)。它有以下输入:

  • 枚举:当前状态(所以 0 -> 30)
  • 枚举:来源(目前只有 2 个条目)
  • 布尔值:请求
  • 布尔值:类型
  • 枚举:状态(3 个状态)
  • 枚举:处理(3 个状态)
  • 布尔值:已完成

将其分解为单独的状态机似乎不可行,因为每个状态都是不同的。我为最常见的输入编写了测试,每个输入一个测试,所有输入都保持不变,除了状态。

[Subject("Application Process States")]
public class When_state_is_meeting2Requested : AppProcessBase
{
    Establish context = () =>
    {
        //Setup....
    };

    Because of = () => process.Load(jas, vac);

    It Current_node_should_be_meeting2Requested = () => process.CurrentNode.ShouldBeOfType<meetingRequestedNode>();
    It Can_move_to_clientDeclined = () => Check(process, process.clientDeclined);
    It Can_move_to_meeting1Arranged = () => Check(process, process.meeting1Arranged);
    It Can_move_to_meeting2Arranged = () => Check(process, process.meeting2Arranged);
    It Can_move_to_Reject = () => Check(process, process.Reject);
    It Cannot_move_to_any_other_state = () => AllOthersFalse(process);
}

没有人完全确定每个状态和一组输入的输出应该是什么。我已经开始为它编写测试。但是,我需要编写类似4320次测试(30 * 2 * 2 * 2 * 3 * 3 * 2)的东西。

您对测试状态机有什么建议?


编辑:我正在考虑所有建议,当我找到一个最有效的答案时会标记答案。

4

9 回答 9

6

我看到了问题,但我肯定会尝试将逻辑分开。

我眼中的大问题领域是:

  • 它有 31 种可能的状态。
  • 它有以下输入:
    • 枚举:当前状态(所以 0 -> 30)
    • 枚举:来源(目前只有 2 个条目)
    • 布尔值:请求
    • 布尔值:类型
    • 枚举:状态(3 个状态)
    • 枚举:处理(3 个状态)
    • 布尔值:已完成

发生的事情太多了。输入使代码难以测试。您已经说过将其拆分为更易于管理的区域会很痛苦,但是在运行中测试这么多逻辑同样如果不是更痛苦的话。在您的情况下,每个单元测试都涵盖了太多内容。

我问的关于测试大型方法的这个问题本质上是相似的,我发现我的单位太大了。你仍然会得到很多测试,但它们会更小,更易于管理,覆盖更少的领域。但这只能是一件好事。

测试遗留代码

查看Pex。您声称您继承了此代码,因此这实际上不是测试驱动开发。您只是希望单元测试涵盖各个方面。这是一件好事,因为任何进一步的工作都将得到验证。我个人还没有正确使用 Pex,但是我被我看到的视频所震撼。本质上,它将根据输入生成单元测试,在这种情况下,输入是有限状态机本身。它将生成您没有充分考虑的测试用例。当然这不是 TDD,但在这种情况下,测试遗留代码,它应该是理想的。

一旦你有了测试覆盖,你就可以开始重构,或者添加具有良好测试覆盖安全的新特性,以确保你不会破坏任何现有的功能。

于 2010-04-22T13:23:00.100 回答
4

全对测试

为了限制要测试的组合数量并合理地确保您涵盖了最重要的组合,您应该查看所有对测试。

全对测试背后的原因是:程序中最简单的错误通常由单个输入参数触发。下一个最简单的错误类别包括那些依赖于参数对之间交互的错误,这可以通过所有对测试来捕获。1涉及三个或更多参数之间交互的错误越来越少2,同时越来越昂贵通过详尽的测试来找到,它的限制是对所有可能的输入进行详尽的测试。

还可以在此处查看先前的答案(无耻插件)以获取更多信息以及指向所有配对和图片作为工具的链接。

示例 Pict 模型文件

给定模型生成 93 个测试用例,覆盖所有输入参数对。

#
# This is a PICT  model for testing a complex state machine at work 
#

CurrentState  :0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30
Source        :1,2
Request       :True, False
Type          :True, False
Status        :State1, State2, State3
Handling      :State1, State2, State3
Completed     :True,False

#
# One can add constraints to the model to exclude impossible 
# combinations if needed.
#
# For example:
# IF [Completed]="True" THEN CurrentState>15;
#

#
# This is the PICT output of "pict ComplexStateMachine.pict /s /r1"
#
# Combinations:    515
# Generated tests: 93
于 2010-04-30T06:56:12.433 回答
3

我为一台医疗设备构建了一个有限状态机。FSM 可以通过我定义的 XML 格式进行配置。

要定义状态机,必须依靠使用状态图的数字电路设计经验,

你必须使用我所说的收费公路过渡地图。在美国东海岸,大多数高速公路都被称为收费公路。收费公路当局发布收费公路收费图。如果收费区有 50 个出口,定价图将有一个 50rows x 50cols 表,将出口详尽地列出为行和列。要找出进入 20 号出口和离开 30 号出口的通行费,您只需查找第 20 行和第 30 列的交叉点。

对于 30 个状态的状态机,收费公路转换图将是一个 30 x 30 的矩阵,按行和列列出所有 30 个可能的状态。让我们将行决定为 CURRENT 状态,将列决定为 NEXT 状态。

每个相交的单元格将列出从当前状态(行)转换到下一个状态(列)的“价格”。但是,该单元格将引用 Inputs 表中的一行,而不是单个 $ 值,我们可以将其称为转换 id。

在我开发的医疗设备 FSM 中,有字符串、枚举、int 等输入。输入表按列列出了这些输入刺激。

要构建 Inputs 表,您将编写一个简单的例程来列出所有可能的输入组合。但是桌子会很大。在您的情况下,该表将有 4320 行,因此有 4320 个转换 ID。但它不是一个乏味的表,因为您以编程方式生成了该表。就我而言,我编写了一个简单的 JSP 来在浏览器上列出转换输入表(和收费表),或者下载为 csv 以在 MS Excel 中显示。

构建这两个表有两个方向。

  1. 设计方向,您可以在其中构建收费公路表所有可能的转换,将不可到达的转换灰显。然后只为每个可达转换构建所有预期输入的输入表,行号作为转换 id。每个转换 id 都被转录到收费公路转换图的相应单元格上。然而,由于 FSM 是一个稀疏矩阵,并非所有的转换 id 都会在收费公路转换图的单元格中使用。此外,一个转换 id 可以多次使用,因为相同的转换条件可以应用于一对以上的状态更改。

  2. 测试方向是反向的,您可以在其中构建 Inputs 表。您必须为详尽的转换测试编写一个通用例程。
    该例程将首先读取转换排序表以将状态机带入入口点状态以开始测试周期。在每个 CURRENT 状态,它准备运行所有 4320 个转换 id。在 Turnpike 转换图中的每一行 CURRENT 状态,将有有限数量的列有效 NEXT 状态。

您可能希望例程循环遍历它从 Inputs 表中读取的所有 4320 行输入,以确保未使用的转换 ID 对当前状态没有影响。您想测试所有有效的转换 id 是否都是有效的转换。

但是你不能——因为一旦注入了有效的转换,它会将机器的状态更改为 NEXT 状态,并阻止你完成对先前 CURRENT 状态的其余转换 id 的测试。一旦机器改变状态,你必须再次从转换 id 0 开始测试。

过渡路径可以是周期性的或不可逆的,或者具有沿路径的周期性和不可逆部分的组合。

在您的测试例程中,您需要为每个状态创建一个寄存器,以记住最后进入该状态的转换 id。每次测试达到有效的转换 id 时,该转换 id 都会留在该寄存器中。因此,当您完成一个循环并返回到已经遍历的状态时,您开始迭代下一个转换 id 大于存储在寄存器中的转换 id。

您的例程必须处理转换路径的不可逆部分,当机器进入最终状态时,它会从入口点状态重新开始测试,重申来自下一个转换 id 的 4320 输入大于存储的为一个状态。通过这种方式,您将能够详尽地发现机器所有可能的过渡路径。

幸运的是,FSM 是有效转换的稀疏矩阵,因为详尽的测试不会消耗转换 id 数 x 可能状态平方数的完整组合。但是,如果您处理的是无法将视觉或温度状态反馈到测试系统的传统 FSM,那么就会出现困难,您必须直观地监控每个状态。那会很难看,但我们仍然花了两周时间额外测试设备,只通过有效的转换。

如果您的 FSM 允许您通过简单的重置到达一个入口点并且应用一个转换 id到入口点状态。但是让您的例程能够读取转换排序表很有用,因为您经常需要进入状态网络的中间并从那里开始您的测试。

您应该熟悉转换和状态图的使用,因为检测机器的所有可能和未记录的状态并采访用户是否真的希望它们变灰(转换无效和状态无法访问)非常有用。

我的优势是它是一种新设备,我可以选择设计状态机控制器来读取 xml 文件,这意味着我可以随意更改状态机的行为,实际上是客户想要的任何方式,并且我能够确保未使用的转换 ID 确实无效。

对于有限状态机控制器的 java 列表http://code.google.com/p/synthfuljava/source/browse/#svn/trunk/xml/org/synthful。不包括测试例程。

于 2010-04-23T11:58:05.757 回答
3

我想不出任何简单的方法来测试这样的 FSM,而不需要真正迂腐和使用证明,使用机器学习技术或蛮力。

蛮力: 编写一个以某种声明性方式生成所有 4320 个测试用例的东西,其中大部分数据不正确。我建议将它放在一个 CSV 文件中,然后使用 NUnits 参数测试之类的东西来加载所有测试用例。现在这些测试用例中的大多数都将失败,因此您必须手动更新声明性文件以使其正确,并随机抽取测试用例的样本进行修复。

机器学习技术: 您可以使用一些向量机或 MDA 算法/启发式方法来尝试从我们上面提到的样本中学习,并教您的 ML 程序您的 FSM。然后在所有 4320 个输入上运行算法,看看两者在哪里不一致。

于 2010-04-22T14:00:02.533 回答
3

使用SpecExplorerNModel

于 2010-04-23T09:25:37.700 回答
3

您认为“完全”测试函数 sum(int a, int b) 需要多少测试?在 c# 中,它类似于 18446744056529682436 测试......比你的情况要糟糕得多。

我建议如下:

  1. 测试大多数可能的情况,边界条件。
  2. 单独测试 SUT 的一些关键部分。
  3. 当 QA 或生产中发现错误时添加测试用例。

在这种特殊情况下,最好的方法是测试系统如何从一种状态切换到另一种状态。创建 DSL 来测试状态机并使用它实现最常见的用例。例如:

Start
  .UploadDocument("name1")
  .SendDocumentOnReviewTo("user1")
  .Review()
  .RejectWithComment("Not enough details")
  .AssertIsCompleted()

为流创建简单测试的示例如下:http: //slmoloch.blogspot.com/2009/12/design-of-selenium-tests-for-aspnet_09.html

于 2010-04-23T05:12:29.890 回答
1

根据要求进行测试。如果需要在 Completed 为真时将某个状态转移到某个其他状态,则编写一个测试,自动循环遍历其他输入的所有组合(这应该只是一对 for 循环)以证明其他输入是正确忽略。您最终应该为每个过渡弧进行一次测试,我估计这将是大约 100 或 150 次测试,而不是 4000 次。

于 2010-04-23T05:22:11.633 回答
1

您可以考虑研究基于模型的测试。在这种情况下,有一些工具可以帮助生成测试。我通常推荐MBT

于 2010-04-23T12:38:38.660 回答
0

覆盖测试的蛮力似乎只是一个开始。

于 2010-04-22T16:12:56.073 回答