基于模型的测试(MBT)和 Spec Explorer

发表于:2017-08-25来源:MSDN作者:Yiming Cao Sergio M点击数: 标签:MBT
十多年来,Microsoft 在其内部开发流程中成功应用了基于模型的测试 (MBT)。 事实证明,对于各种内部和外部软件产品而言,MBT 是非常成功的方法。 这些年来,这种方法采用得越来越多。

要生成高质量的软件,需要在测试阶段进行大量的工作,这可能是软件开发过程中成本最高、工作量最大的部分。 从最简单的功能黑盒测试到重量级的方法,包括定理证明程序以及形式化需求说明,有很多方法可以提高测试可靠性和效率。 但是,测试并不总是能达到必要的细致程度,经常缺乏规范和方法体系。

十多年来,Microsoft 在其内部开发流程中成功应用了基于模型的测试 (MBT)。 事实证明,对于各种内部和外部软件产品而言,MBT 是非常成功的方法。 这些年来,这种方法采用得越来越多。 相对来说,它在测试界已广为接受(尤其是与测试方法中的其他“形式化”方法相比时)。

Spec Explorer 是 Microsoft 的 MBT 工具,它扩展了 Visual Studio,提供高度集成的开发环境,可以创建行为模型,它也是图形分析工具,可用于检查这些模型的有效性以及基于这些模型生成测试用例。 我们认为,这个工具是一个分界点,它促进了 MBT 这项高效方法在 IT 行业中的应用,缓和自然学习曲线,提供最先进的开发环境。

本文概要介绍 MBT 和 Spec Explorer 背后的主要概念,通过一个案例研究说明 Spec Explorer 的主要功能。 我们也希望本文提供的实际经验规则,可以帮助您了解,何时应考虑使用 MBT 这种质量保证方法体系来解决特定测试问题。 您不应在一切测试方案中盲目使用 MBT。  很多时候,其他方法(如传统测试)可能是更好的选择。

什么是 Spec Explorer 中的可测试模型?

尽管不同的 MBT 工具提供不同的功能,有时在概念上稍有差异,但对于“执行 MBT”的含义,这些工具是一致的。基于模型的测试自动基于模型生成测试过程。

模型通常是手动创建的,包括系统需求和预期行为。 具体到 Spec Explorer,是基于面向状态的模型自动生成测试用例。 测试用例包括测试序列和测试预期。 测试序列是从模型推断的,负责推动待测系统 (SUT) 达到不同状态。 测试预期跟踪 SUT 的演变过程,确定它是否符合模型指定的行为,并且作出判定。

模型是 Spec Explorer 项目中的主要部分之一。 它在称为模型程序的构造中指定。 您可以采用任何 .NET 语言(如 C#)来编写模型程序。 这种程序由一组与已定义状态交互的规则构成。 模型程序与称为 Cord 的脚本编写语言组合,Cord 是 Spec Explorer 项目中第二重要的部分。 这样,可以指定行为描述来配置浏览和测试模型的方式。 模型程序与 Cord 脚本结合后,可针对 SUT 创建可测试的模型。

当然,Spec Explorer 项目中第三重要的部分是 SUT。 不是一定要向 Spec Explorer 提供 SUT 才能生成测试代码(这是 Spec Explorer 的默认模式),因为生成的代码是直接从可测试模型推断的,并不与 SUT 进行任何交互。 您可以独立于模型评估和测试用例生成阶段“离线”执行测试用例。 但是,如果提供 SUT,则 Spec Explorer 可以验证是否已明确定义从模型到实现的绑定。

案例研究:聊天系统

我们来看一个示例,它介绍如何在 Spec Explorer 中构建可测试的模型。 此示例中的 SUT 是一个简单的聊天系统,只有一个聊天室,用户可以登录和注销。 用户登录后,可以请求已登录用户的列表,可以向所有用户发送广播消息。 聊天服务器总是确认这些请求。 请求和响应是异步的,这意味着它们可以混在一起。 就像一般的聊天系统,一位用户发送的多条消息会按顺序接收。

使用 MBT 的优点之一是,由于必须形式化行为模型,您可以获得大量反馈来了解需求。 在早期阶段,就能发现歧义、矛盾和缺少上下文的情况。 因此,系统需求务必是准确而形式化的,如:

 

 
R1. Users must receive a response for a logon request.
R2. Users must receive a response for a logoff request.
R3. Users must receive a response for a list request.
R4. List response must contain the list of logged-on users.
R5. All logged-on users must receive a broadcast message.
R6. Messages from one sender must be received in order.

Spec Explorer 项目从测试系统的角度使用操作来描述与 SUT 的交互。 这些操作可以是表示从测试系统到 SUT 的触发动作的调用操作,可以是捕获来自 SUT 的响应(如果有)的返回操作,还可以是表示从 SUT 发送的自治消息的事件操作。 调用/返回操作是阻塞操作,因此它们在 SUT 中由单个方法来表示。 这些都是默认操作声明,而“event”关键字用于声明事件操作。 图 1 是聊天系统中的操作声明。

图 1 操作声明

 

 
// Cord code
config ChatConfig
{
  action void LogonRequest(int user);
  action event void LogonResponse(int user);
  action void LogoffRequest(int user);
  action event void LogoffResponse(int user);
  action void ListRequest(int user);
  action event void ListResponse(int user, Set<int> userList);
  action void BroadcastRequest( int senderUser, string message);
  action void BroadcastAck(int receiverUser, 
    int senderUser, string message);
  // ...
}

声明操作之后,下一步是定义系统行为。 在此示例中,用 C# 描述模型。 用类字段建模系统状态,用规则方法建模状态转换。 规则方法确定在模型程序中可基于当前状态采取的步骤,以及执行每一步后状态如何更新。

因为此聊天系统实际上是由用户与系统之间的交互组成,模型的状态即是用户及其状态的集合(请参阅图 2)。

图 2 模型的状态

 

 
/// <summary>
/// A model of the MS-CHAT sample.
/// </summary>
public static class Model
{
  /// <summary>
  /// State of the user.
  /// </summary>
  enum UserState
  {
    WaitingForLogon,
    LoggedOn,
    WaitingForList,
    WatingForLogoff,
  }
  /// <summary>
  /// A class representing a user
  /// </summary>
  partial class User
  {
    ///  <summary>
    /// The state in which the user currently is.
    /// </summary>
    internal UserState state;
    /// <summary>
    /// The broadcast messages that are waiting for delivery to this user.
    /// This is a map indexed by the user who broadcasted the message,
    /// mapping into a sequence of broadcast messages from this same user.
    /// </summary>
    internal MapContainer<int, Sequence<string>> waitingForDelivery =
      new MapContainer<int,Sequence<string>>();
  }             
    /// <summary>
    /// A mapping from logged-on users to their associated data.
    /// </summary>
    static  MapContainer<int, User> users = new MapContainer<int,User>();
      // ...   
  }

可以看到,定义模型的状态与定义一般的 C# 类没有太大区别。 规则方法是用于描述在何种状态下可以激活操作的 C# 方法。 规则方法还描述在操作激活时,对模型的状态应用何种更新。 下面的“LogonRequest”示例说明如何编写规则方法:

 

 
[Rule]
static void LogonRequest(int userId)
{
  Condition.IsTrue(!users.ContainsKey(userId));
  User user =  new User();
  user.state = UserState.WaitingForLogon;
  user.waitingForDelivery = new MapContainer<int, Sequence<string>>();
  users[userId] = user;
}

此方法描述操作“LogonRequest”的激活条件和更新规则,该操作先前已在 Cord 代码中声明。 此规则主要说明:

  • 在当前用户集中还不存在输入用户 ID 时,可以执行 LogonRequest 操作。 “Condition.Is­True”是 Spec Explorer 提供的 API,用于指定启用条件。
  • 满足此条件时,将创建已正确初始化状态的新用户对象。 然后将它添加到全局用户集合。 这是规则的“更新”部分。

此时,大部分建模工作已完成。 现在,我们定义一些“机器”,以便浏览系统的行为和获得一些相关信息。 在 Spec Explorer 中,机器指浏览单元。  机器有一个名称以及一个用 Cord 语言定义的关联行为。 您也可以将一个机器与其他机器组合以形成更复杂的行为。 我们看看聊天模型的几个示例机器:

 

 
machine ModelProgram() : Actions
{
  construct model program from Actions where scope = "Chat.Model"
}

我们定义的第一个机器是所谓的“模型程序”机器。 它使用“construct model program”指令告诉 Spec Explorer 基于 Chat.Model 命名空间中的规则方法浏览模型的全部行为。

 

 
machine BroadcastOrderedScenario() : Actions
{
  (LogonRequest({1..2}); LogonResponse){2};
  BroadcastRequest(1, "1a");
  BroadcastRequest(1, "1b");
  (BroadcastAck)*
}

第二个机器是一个“方案”,是用类似正则表达式的方式来定义的操作模式。 方案通常由“模型程序”机器组成,用以分割完整行为,如下所示:

 

 
machine BroadcastOrderedSlice() : Actions
{
  BroadcastOrderedScenario || ModelProgram
}
 

“||”操作符用于在两个参与的机器之间创建“同步并行组合”。 结果行为将仅包含可在两个机器上同步的步骤(我们说的“同步”,是指使用相同的参数列表执行相同的操作)。 图 3 所示的图中浏览此机器结果。


图 3 组合两个机器

图 3 中的图中可以看到,组合的行为与方案机器和模型程序机器一致。 这是非常有效的方法,可以获取复杂行为的较简单子集。 另外,当系统具有无限状态空间(如聊天系统中那样)时,分割完整行为可以生成有限的子集,更适合进行测试。

我们来分析此图中的不同实体。 圆形状态是可控制的状态。 它们是为 SUT 提供触发操作的状态。 棱形状态是可观察的状态。  这些状态预期 SUT 会发生一个或多个事件。 测试预期(预期的测试结果)已经在图中编码,包括事件步骤及其参数。 具有多个传出事件步骤的状态称为非确定性状态,因为 SUT 在执行时提供的事件不是在建模时确定的。 观察图 3 中的浏览图包含几个非确定性状态:S19、S20、S22 等。

浏览图对于理解系统很有用,但它仍然不适合用于测试,因为它不是“测试正常”形式。 如果一个行为不包含任何具有多个传出调用-返回步骤的状态,我们就说该行为是测试正常形式。 图 3 中的图中,可以看到 S0 明显不符合此规则。 要将此类行为转换为测试正常形式,可以使用测试用例构造简单创建新机器。

 

 
machine TestSuite() : Actions  where TestEnabled = true
{
  construct test cases where AllowUndeterminedCoverage = true
  for BroadcastOrderedSlice
}

此构造生成新行为,方法是遍历初始行为并以测试正常形式生成跟踪。 遍历标准是覆盖到边缘。 初始行为中的每个步骤都会遍历至少一次。 图 4 中的图形显示了这样遍历后的行为。


图 4 生成新行为

要实现测试正常形式,具有多个调用-返回步骤的状态将按每个步骤进行分割。 不会分割事件步骤,这些步骤始终完全保留,因为事件是 SUT 在运行时可以进行的选择。 必须准备好测试用例才能处理任何可能的选择。

Spec Explorer 可以基于测试正常形式行为生成测试套件代码。  所生成的测试代码的默认形式是 Visual Studio 单元测试。 您可以使用 Visual Studio 测试工具或 mstest.exe 命令行工具直接执行此类测试套件。 生成的测试代码是用户可读的,可以很方便地调试:

 

 
#region Test Starting in S0
[Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute()]
public void TestSuiteS0() {
  this.Manager.BeginTest("TestSuiteS0");
  this.Manager.Comment("reaching state \'S0\'");
  this.Manager.Comment("executing step \'call LogonRequest(2)\'");
  Chat.Adapter.ChatAdapter.LogonRequest(2);
  this.Manager.Comment("reaching state \'S1\'");
   this.Manager.Comment("checking step \'return LogonRequest\'");
  this.Manager.Comment("reaching state \'S4\'");
  // ...
    }

 

测试代码生成器是高度可自定义的,可配置为生成面向不同测试框架(如 NUnit)的测试用例。

Spec Explorer 安装程序包括这个完整的聊天模型。

MBT 适合哪种情况?

使用基于模型的测试有利有弊。 最明显的好处是,在完成可测试的模型后,按一下按钮就能生成测试用例。 此外,模型必须预先形式化,这样才能实现对需求不一致的早期检测,帮助团队在预期行为方面保持正确。 请注意,编写手动测试用例时,已经有“模型”,但它没有形式化,只是存在于测试者的脑海中。 MBT 迫使测试团队清晰地传达出其有关系统行为的预期,并使用清楚的结构将这些预期编写出来。

另一个明显的优点是项目维护成本较低。  系统行为的更改或新增功能可通过更新模型反映出来,这通常比逐个更改手动测试用例简单得多。 有时,仅仅确定需要更改的测试用例就是一项非常耗时的任务。 请注意,模型编写也是独立于实现或实际测试的工作。 这就是说,团队中的不同成员可以同时进行不同的任务。

缺点是经常需要进行思维调整。 这可能是这个方法的重大挑战之一。 大家都知道的一个最重要的问题是,IT 从业者没有时间尝试新工具,使用这个方法的学习曲线不容忽视。 应用 MBT 可能还需要进行一些流程更改,这也可能造成一些阻碍,具体取决于团队。

另一个不利之处是,与手动编写的传统测试用例相比,必须提前进行更多工作,因此需要花更多时间才能生成第一个测试用例。 另外,测试项目需要有足够的复杂度,才值得进行投资。

幸运的是,我们认为有几条经验规则可帮助确定何时适合使用 MBT。 第一个特征是,系统状态集无限,可以用不同的方式满足需求。 系统是反应式或分布式,或具有异步或非确定性交互的系统,这是另一个特征。 另外,如果方法有很多复杂参数,也说明适合用 MBT。

如果符合这些条件,MBT 都有重大意义,可以节省大量测试工作。  Microsoft Blueline 是这方面的示例,在这个项目中,数百个协议验证为 Windows 协议遵从性计划的一部分。 在这个项目中,我们使用 Spec Explorer 来验证实际协议行为的协议文档的技术准确性。 这是繁重的工作,Microsoft 花费了 250 个人年进行测试。 Microsoft Research 验证了一项统计信息研究,该项研究表明,使用 MBT 为 Microsoft 节省了 50 个人年的测试工作,换句话说,与传统测试方法相比,省去了大约 40% 的工作。

基于模型的测试是非常强大的方法,在传统测试方法的基础上增加了一种系统的方法。 Spec Explorer 是成熟的工具,它在高度集成、最先进的开发环境中使用 MBT 概念,是免费的 Visual Studio Power Tool。


原文转自:https://msdn.microsoft.com/zh-cn/magazine/dn532205