通过 Pex 自动进行旧代码单元测试
Nikhil Sachdeva
在我以前的生命我是一名顾问。我的客户前导的银行的一个希望使其贷款源过程自动化。银行已经有一个系统,在基于 Windows 的应用程序、 专有的后端和也是他们的解决方案的核心在大型机系统包含的地方。我的工作是将现有的系统与一组帐户除法为正在开发的应用程序集成在一起。
客户端生成的交流在大型机与一个 Web 服务。首次似乎相当简单。我必须做所有已挂钩到该服务、 获得的信息,并将它传递给新帐户应用程序。但 ’s 永远不会这么简单。
实施过程中,我发现新的系统要求一个贷款原属性,但 Web 服务 GetLoanDetails 方法未返回该信息。事实证明,服务由开发人员与公司不再 ’s 创建年前。银行已经被使用不做任何修改服务因为它有许多层,而且恐怕断裂事物的每个人。
该 Web 服务是旧式代码。
最终,我们内置轻量服务包装,因此新的系统可能调用它的任何新信息并继续使用旧的服务。它将已修改现有的服务,如果一致和可测试设计有后跟要容易得多。
使代码保持最新
旧式代码是在过去开发和仍使用但难以维护和更改的内容。理由一般保持这些系统都围绕成本和时间中构建一个类似的新系统涉及 — 尽管有时是缺乏有关当前编码的努力的含义,将来的安全意识。
这一事实是一段时间内的代码开始到 rot 中。这可以是因为要求更改、 不-非常-好的想法-通过设计、 已应用的 anti-patterns 或缺乏相应的测试。最终结果是难以维护且难以更改的代码。
有许多种方法可以防止您的代码 rotting,但最有效选项之一可以被编写为可测试的代码,然后生成足够的单元测试的代码。单元测试充当代理程序连续探测未路径为系统、 识别麻烦错误和更改引入到一个子系统有好或坏影响整体软件是否提供指示器。单元测试提供给开发人员的信心任何代码更改不会带来任何回归测试。
不必说,创建和维护良好的单元测试套件可以本身中的一项挑战。它 ’s 可能最后会编写用于测试套件比下测试代码的更多代码。另一个挑战是在代码内的依赖项。越复杂解决方案更多可能要查找之间的依存关系类。mocks 和存根 (stub) 是可识别的方式来删除这些依赖项和测试代码在隔离,但这些需要额外的知识,从创建有效的单元测试开发人员体验。
要在挽救 Pex
Pex ( research.microsoft.com/projects/pex/ ) 是一个由以自动并系统地产生的执行有限数量的有限的路径所需的测试输入最小集微软研究院开发的工具。Pex 自动生成一个小型测试套件,具有高度的代码和声明覆盖率。
Pex 查找您然后可以保存为一个小测试套件与高的代码覆盖率的方法的有趣的输入输出值。Pex 执行了系统化的分析求职边界条件、 例外项和可以立即调试的断言失败。Pex 还使参数化的单元测试 (PUT)、 的单元测试的扩展,减少了测试维护成本,和它利用动态符号执行探测通过下创建涵盖大多数分支的执行一个测试套件的测试代码。
一个 PUT 是只需采用参数,调用下测试代码,并指出断言的方法。样本 PUT 如下所示:
void AddItem(List<int> list, int item) {
list.Add(item);
Assert.True(list[list.Count - 1] == item);
}
PUT 概念派生一个更广泛的术语,称为数据驱动测试 (DDT),从其已使用传统的单元测试中很长时间使测试可重复。因为传统的单元测试都关闭在本质,提供给他们的输入的值唯一方法是通过外部源 (如 XML 文件、 电子表格,或数据库。DDT 方法的很好地工作时没有维护和更改该外部数据文件中所涉及的额外系统开销,并且输入都依赖于系统的开发人员已经掌握。
Pex 不依赖于外部源,而是通过将值传递给相应的 PUT 提供初始测试方法的信息。在 PUT 是开放方法,因为它可以排列接受任意数量的输入。此外,Pex 不生成该 PUT 为随机的值。它依赖于 introspection 的测试方法和生成有意义的值基于因素 (如边界条件、 可接受的类型的值和状态-的-在-画约束规划求解称为 Z3 ( research.microsoft.com/en-us/um/redmond/projects/z3/ )。这将确保介绍该方法在测试下的所有相关的路径。
Pex 的美容是它从该 PUT 生成传统的单元测试。这些单元测试可以直接在一个单元测试框架中像 MSTest Visual Studio 中不做任何修改中运行。Pex 提供要生成单元测试框架类似于 NUnit 或 xUnit.NET 的扩展名。您还可以创建自己的自定义扩展。一个 Pex 生成传统的单元测试看上去像下面这样:
[TestMethod]
[PexGeneratedBy(typeof(TestClass))]
void AddItem01() {
AddItem(new List<int>(), 0);
}
动态符号执行是 Pex 已经探索性测试的答案。使用这一技术 Pex 执行代码多次以了解程序行为。它监视控件和数据流并生成测试输入一个约束系统。
单元测试与 Pex
第一步是创建代码测试下一个 PUT。在 PUT 可以手动生成由开发人员或通过使用 Visual Studio 外接程序 Pex。您可以定制该 PUT 通过修改参数、 添加 Pex 工厂和存根 (stub)、 mocks 与集成、 添加断言,依此类推。本文中稍后介绍 Pex 工厂和 stub。
当前在 Visual Studio 外接程序对于 Pex 仅在 C# 创建将但下测试代码可以是任何.net 语言。
第二步之后设置该 PUT 是运行 Pex 探索。这是其中 Pex 不会与其幻。它分析来标识被测试 PUT。然后它启动下测试代码检查由通过每个分支和评估可能的输入的值。Pex 反复执行下测试代码。在每次运行后它将选取的未覆盖以前,到达该分支生成约束系统 (通过测试输入一个谓词),然后使用来确定新的测试输入约束规划求解,如果任何分支。与新的输入再次执行该测试,并重复此过程。
在每次运行 Pex 可能会发现新的代码并深入了解实现。以此方式 Pex 探讨了代码的行为。
同时也可以浏览该代码,Pex 生成包含测试涵盖可以行使 Pex 的所有分支的一个单元测试套件。这些测试是在 Visual Studio 测试编辑器中可以运行的标准 unit 测试。如果某些节找到较低的覆盖率您想象的重构,再应用相同的周期再次以获得更高的代码覆盖率和更全面的测试套件通过修改您的代码。
Pex 可以帮助减少的工作量,并处理旧式代码时所涉及的时间。因为 Pex 自动探讨了不同的分支和代码路径,don’t 必须理解整个代码基的所有详细信息。另一个好处是,开发人员工作在 PUT 级别。编写一个 PUT 是经常很大程度比书写关闭单元测试,由于集中问题方案而非所有可能的测试案例的功能,更简单。
将 Pex 放到工作
let’s Pex 使用上一段旧式代码,请参阅如何它可以帮助使代码更易于维护且易于测试。
Fabrikam、 basketballs 和 baseballs 的前导制造商提供的联机门户用户可以在此查看可用的产品和放置那些产品的订单。通过提供连接到数据存储区和操作 (如 HasInventory 和删除一个仓库组件访问自定义的数据存储区中是库存详细信息。订单组件提供了一种处理基于产品的顺序和传递由用户的数量的 Fill 方法。
订单和仓库组件是彼此紧密关联。这些组件开发年前,没有当前员工有系统的深入了解。在开发过程中创建了任何单元测试,并可能是作为一个结果则该组件是非常不稳定。的 图 1 显示了当前的设计。
图 1 的 A 旧订单履行系统
订单类的填充方法如下所示:
public class Order {
public bool Fill(Product product, int quantity) {
// Check if WareHouse has any inventory
Warehouse wareHouse = new Warehouse();
if (wareHouse.HasInventory(product, quantity)) {
// Subtract the quantity from the product in the warehouse
wareHouse.Remove(product, quantity);
return true;
}
return false;
}
}
有几个关键的项目需要引起注意此处。首先,订单和仓库都紧密关联。在类依赖于较低可扩展且难以使用模型或存根框架使它们的实现。不没有可用的任何单元测试,因此任何更改可能会引入导致不稳定系统的回归测试。
仓库组件写入多长时间以前,而且当前的开发团队不了解如何更改它或任何更改的含义。为了更复杂的问题顺序不能处理仓库的其他实现,不做修改。
let’s 尝试重构代码,然后使用 Pex 生成单元测试。我将重构仓库和订单对象,然后创建单元测试的订单类的填充方法。
重构旧式代码显然是一项挑战。在这些情况下一个方法可能至少使代码可测试,以便在生成足够的单元测试。我应用仅在没有什么内容最少的模式使代码更具可测试性。
第一个问题是顺序使用特定实施的仓库。这使得很难耦合从排序仓库实现。let’s 修改代码,使其更加灵活和可测试位。
我首先创建一个接口 IWareHouse 并修改该仓库对象来实现此接口。任何新仓库将要求来实现此接口。
因为订单上仓库具有直接依赖项,他们是紧密关联。我使用依赖项注入打开行为的扩展的类。使用这种方法 IWareHouse 实例将被传递到订单在运行时。的 图 2 显示了新的设计。
图 2 修订与 IWareHouse 的系统
的 图 3 显示了新的订单类。
图 3 修订订单类
public class Order {
readonly IWareHouse orderWareHouse;
// Use constructor injection to provide a wareHouse object
public Order(IWareHouse wareHouse) {
this.orderWareHouse = wareHouse;
}
public bool Fill(Product product, int quantity) {
// Check if WareHouse has any inventory
if (this.orderWareHouse.HasInventory(product, quantity)) {
// Update the quantity for the product
this.orderWareHouse.Remove(product, quantity);
return true;
}
return false;
}
}
创建参数化的单元测试
let’s 现在使用 Pex 生成重构代码的测试。Pex 提供一个 Visual Studio 外接程序,使正在生成将置于很容易。用鼠标右键单击项目、 类或方法,将需要生成,并单击 Pex | 创建参数化的单元测试 stub。通过选择填充方法的订单类开始。
Pex 允许您选择一个现有的单元测试项目,或创建一个新。它还为您提供要筛选测试基于方法或类型名称的选项 (请参阅 的 图 4)。
图 4 的 一个新的 Pex 项目设置
Pex 生成下面的 PUT 的 Fill 方法。
[PexClass(typeof(Order))]
[TestClass]
public partial class OrderTest {
[PexMethod]
public bool Fill([PexAssumeUnderTest] Order target,
Product product, int quantity) {
// Create product factory for Product
bool result = target.Fill(product, quantity);
return result;
}
}
在 OrderTest 不只是一个普通 TestClass ; 它已被批注具有指示它由 Pex 创建一个 PexClass 属性。目前尚没有 TestMethod 如您所期望的那样在标准的 Visual Studio 单元测试。而是,您有一个 PexMethod。此方法是参数化的单元测试。稍后时让 Pex 探讨下测试代码,它将创建另一个分部类,其中包含批注 TestMethod 属性与该标准的单元测试。这些生成的测试将可以访问通过 Visual Studio 测试编辑器。
请注意,对于 Fill 方法 PUT 采用三个参数。
[PexAssumeUnderTest] 目标顺序
这是测试本身下的类。PexAssumeUnderTest 属性告诉 Pex 它应该只传递的确切的指定类型的非空值。
产品的产品
这是产品的基类。Pex 将试图自动创建产品类的实例。对于更细致地控制您可以使用工厂方法提供 Pex。Pex 将使用这些工厂创建复杂的类的实例。
int 数量
Pex 将提供的基于下测试方法数量的值。它将尝试插入有意义的测试的值,而不是垃圾邮件的值。
Pex 工厂
如我提到之前 Pex 使用约束规划求解来确定新的测试输入的参数。输入可以是标准的.net 类型或自定义业务实体。期间研究,Pex 实际上创建这些类型的实例,以便下测试程序可以行为不同有趣的方式。如果类是可见的并且具有一个可见的默认构造函数,Pex 可以创建类的实例。可见的所有字段是否它可以同时为它们生成的值。但是,如果字段是用属性封装,或者不公开给外部世界,Pex 要求创建该对象以获得更好的代码覆盖率的帮助。
Pex 提供要创建并链接到 Pex 研究的必选的对象的两个挂钩。以便 Pex 可以浏览不同的对象状态,用户可以提供的复杂对象的工厂。这是通过 Pex 工厂方法实现的。可以通过此类工厂创建该类型称为 explorable 的类型。我们将使用这种方法在本文中。
其他方法就是定义对象的私有字段的固定条件列表,以便 Pex 可以直接制造不同对象状态。
来到回方案示例如果运行生成参数化的测试一个 Pex 探索,Pex 浏览结果窗口将显示一条消息 “ 2 对象创建 ”。这不是一个错误。期间研究,Pex 遇到复杂的类 (在本例中的顺序),并创建该类的一个默认工厂。此工厂通过 Pex 才能更好地理解程序行为。
Pex 由创建该默认工厂是类的香草必需的实现。您可以定制此工厂以提供您自己的自定义实现。单击接受/编辑工厂以将该代码插入您的项目 (请参阅 的 图 5)。或者,您可以创建静态类与批注 PexFactoryMethod 属性具有一个静态方法。同时探索,Pex 将在测试项目中具有与此属性的方法的任何静态类搜索,并相应地使用它们。
图 5 创建默认工厂
OrderFactory 类似于以下内容:
public static partial class OrderFactory {
[PexFactoryMethod(typeof(Order))]
public static Order Create(IWareHouse wareHouseIWareHouse) {
Order order = new Order(wareHouseIWareHouse);
return order;
}
}
如果其他程序集编写工厂方法,可以判断 Pex 例如使用程序集级别 PexExplorableFromFactoriesFromType 或 PexExplorableFromFactoriesFromAssembly 属性以声明性方式使用它们。
[assembly: PexExplorableFromFactoriesFromType(
typeof(MyTypeInAnotherAssemblyContainingFactories))]
如果 Pex 创建很少的测试,或者通过只应创建该对象上引发一个 NullReferenceException 创建有趣的测试失败,这是一个很好的指示 Pex 可能需要自定义工厂。否则,Pex 附带了一组启发方式它们在许多情况下创建工作的对象工厂。
Pex 存根 (stub) 框架
软件开发中的一个测试存根 (stub) 概念指的是一个可以替换可能是复杂的组件,以便于测试的哑元实现。简单存根 (stub) 的想法时,可以帮助创建和维护虚拟实现大多数现有框架是实际上相当复杂。Pex 团队已制定了新的轻量框架,他们只需调用存根 (stub)。存根 (stub) 生成.net 接口和非密封类的存根 (stub) 类型。
在此框架中类型 T 的存根提供默认实现的每个抽象成员的 T,并动态地指定自定义每个成员的实现机制。(可以选择,存根 (stub) 也可以生成为非抽象虚拟成员)。C# 代码作为生成存根 (stub) 类型。框架是仅依赖于委托来动态指定存根 (stub) 成员的行为。存根 (stub) 支持.net Framework 2.0 及更高,而且还集成了与 Visual Studio 2008 和更高。
在我的示例方案排序类型仓库对象上具有依赖项。记住我重构代码以实现依赖项注入,以便仓库访问可以提供从外到订单类型。派上用场时创建该存根 (stub)。
创建一个存根 (stub) 是相当简单。您所需要的全部工作就是.stubx 文件。如果创建了测试项目,通过 Pex 您应该已经有它。如果不是,此文件从创建 Visual Studio 内。右击测试项目并选择添加新项。存根 (stub) 模板是可用 (请参阅 的 图 6)。
图 6 创建新的存根
文件将显示为标准的 XML 文件,Visual Studio 中。程序集元素中指定该存根 (stub) 需要被创建并保存.stubx 文件程序集的名称:
<Stubs xmlns="https://schemas.microsoft.com/stubs/2008/">
<Assembly Name="FabrikamSports" />
</Stubs>
Pex 中程序集将自动创建必要的存根 (stub) 方法,对于所有类型。
生成的存根 (stub) 方法具有相应的存根实现提供挂钩的委托字段。 榛樿鎯呭喌涓嬶,Pex 将委托用于提供实现。 您也可以提供 lambda 表达式将行为附加到该委托,或使用 PexChoose 类型让 Pex 自动生成该方法的值。
渚嬪 HasInventory 方法提供选项,我可以有如下所示:
var wareHouse = new SIWareHouse() {
HasInventoryProductInt32 = (p, q) => {
Assert.IsNotNull(p);
Assert.IsTrue(q > 0);
return products.GetItem(p) >= q;
}
};
在事实的方式数据表使用 PexChoose 存根 (stub) 的默认行为时已使用 Pex,Pex 由创建该测试项目包含 following 程序集级属性:
[assembly: PexChooseAsStubFallbackBehavior]
SIWareHouse 类型由 Pex 存根 (stub) 框架生成。 它实现 IWareHouse 接口。 let’s 进一步了解一下 SIWareHouse 存根 (stub) 的 Pex 由生成代码。 名为 < StubsxFilename > 分部类中创建.stubx 文件的源代码 designer.cs 的 图 7 所示。
图 7 的 Pex 生成 IWareHouse 存根
/// <summary>Stub of method System.Boolean
/// FabrikamSports.IWareHouse.HasInventory(
/// FabrikamSports.Product product, System.Int32 quantity)
/// </summary>
[System.Diagnostics.DebuggerHidden]
bool FabrikamSports.IWareHouse.HasInventory(
FabrikamSports.Product product, int quantity) {
StubDelegates.Func<FabrikamSports.Product, int, bool> sh
= this.HasInventory;
if (sh != (StubDelegates.Func<FabrikamSports.Product,
int, bool>)null)
return sh.Invoke(product, quantity);
else {
var stub = base.FallbackBehavior;
return stub.Result<FabrikamSports.Stubs.SIWareHouse,
bool>(this);
}
}
/// <summary>Stub of method System.Boolean
/// FabrikamSports.IWareHouse.HasInventory(
/// FabrikamSports.Product product, System.Int32 quantity)
/// </summary>
public StubDelegates.Func<FabrikamSports.Product, int, bool> HasInventory;
存根 (stub) 创建 HasInventory 方法为一个公共委托字段和 HasInventory 实现中调用它。 Pex 可用没有实现是否调用将使用 PexChoose,如果该 FallBackBehaviour.Result 方法在 [程序集:PexChooseAsStubFallbackBehavior] 是存在,否则将引发一个 StubNotImplementedException。
若要用于 IWareHouse 的存根的实现我会有点调整参数单元测试。 已修改后都能够在其构造函数中采取 IWareHouse 实现订单类。 我现在创建一个 SIWareHouse 实例,然后将的传递到订单类,以便它使用 IWareHouse 方法的自定义实现。 修订后的 PUT 是如下所示:
[PexMethod]
public bool Fill(Product product, int quantity) {
// Customize the default implementation of SIWareHouse
var wareHouse = new SIWareHouse() {
HasInventoryProductInt32 = (p, q) =>
PexChoose.FromCall(this).ChooseValue<bool>(
"return value")
};
var target = new Order(wareHouse);
// act
bool result = target.Fill(product, quantity);
return result;
}
存根 (stub) 实际上自动提供默认实现的存根 (stub) 方法,因此您可能有只需运行不做任何修改也 PUT。
参数化的模型
对于存根实现一个更细致地控制 Pex 支持一个称为参数化的模型的概念。 这是一种方法编写不具有修复行为的一个特定的存根 (stub)。 通过这一概念提供 Pex 的抽象是开发人员不需要担心实现的变体。 Pex 将研究方法基于下测试代码如何使用它们的不同的返回值。 参数化的模型是一项强大的功能,使您可以采取完全控制如何在同一时间让 Pex 评估输入参数变量的值时处理该存根 (stub)。
IWareHouse 的参数化的模型可能类似于在 图 8 代码。
图 8 参数化的 IWareHouse 的模型
public sealed class PWareHouse : IWareHouse {
PexChosenIndexedValue<Product, int> products;
public PWareHouse() {
this.products =
new PexChosenIndexedValue<Product, int>(
this, "Products", quantity => quantity >= 0);
}
public bool HasInventory(Product product, int quantity) {
int availableQuantity = this.products.GetItem(product);
return quantity - availableQuantity > 0;
}
public void Remove(Product product, int quantity) {
int availableQuantity =
this.products.GetItem(product);
this.products.SetItem(product,
availableQuantity - quantity);
}
}
实质上是我创建了用于 IWareHouse,我自己存根的实现,但请注意,我没有提供值的数量和产品。 相反,我让 Pex 生成这些值。 PexChosenIndexedValue 自动提供值对于该对象允许只有一个存根的实现带有变量参数的值。
为了简单起见,我将让 Pex 提供 IWareHouse 类型的 HasInventory 实现。 我将添加到前面创建 OrderFactory 类的代码。 每次通过 Pex 创建订单实例时它将使用存根的仓库实例。
摩尔
到目前为止我专注于两个原则 — 重构代码,以使其更具可测试性,然后使用 Pex 生成单元测试。 这种方法可以让您清理您最终导致更多的可维护软件的代码。 但是,重构旧式代码可以在自身中的大挑战。 可以有许多可能阻止开发人员重构当前源代码的组织或技术约束。 您如何处理这?
旧式代码是难以重构的方案中该方法应至少创建足够的单元测试的业务逻辑,以便您可以验证系统的每个模块的可靠性。 TypeMock (learn.typemock.com) 类似的模拟框架周围了一段时间。 它们让您创建单元测试,而不实际修改基本代码。 这种方法来证明非常有益,特别是对于大型的旧版基本代码。
Pex 附带了称为摩尔一个功能,通过它可以实现相同的目标。 它允许您生成旧式代码的 Pex 单元测试,而实际上重构源代码。 摩尔真正打算测试系统如静态方法和密封的类的否则为 untestable 部分。
摩尔存根 (stub) 以类似方式工作:Pex 的代码生成 mole 类型公开的每个方法的委托属性。 您可以将附加一个委托,然后将附加一个 mole。 在该点所有自定义的委托获取有线向上神奇通过 Pex 探查器。
Pex 会自动创建为指定的所有静态的、 密封的和公共接口的摩尔.stubx 文件中。 Pex Mole 看起来非常类似于一个存根 (stub) 键入 (请参阅下载的示例代码)。
使用摩尔是相当简单。 可以为 Mole 方法提供实现,并在您 PUT 中使用它们。 请注意因为 Mole 插入存根的实现在运行时,您无需更改您能够生成单元测试使用摩尔根本基本代码。
let’s 旧版的 Fill 方法上使用摩尔:
public class Order {
public bool Fill(Product product, int quantity) {
// Check if warehouse has any inventory
Warehouse wareHouse = new Warehouse();
if (wareHouse.HasInventory(product, quantity)) {
// Subtract the quantity from the product
// in the warehouse
wareHouse.Remove(product, quantity);
return true;
}
return false;
}
}
我创建为利用了 Mole 类型 (请参阅 的 图 9) 填充方法 PUT。
图 9 使用 Mole 类型上填充
[PexMethod]
public bool Fill([PexAssumeUnderTest]Order target,
Product product, int quantity) {
var products = new PexChosenIndexedValue<Product, int>(
this, "products");
// Attach a mole of WareHouse type
var wareHouse = new MWarehouse {
HasInventoryProductInt32 = (p, q) => {
Assert.IsNotNull(p);
return products.GetItem(p) >= q;
}
};
// Run the fill method for the lifetime of the mole
// so it uses MWareHouse
bool result = target.Fill(product, quantity);
return result;
}
MWareHouse 是由创建自动 Pex 生成存根 (stub) 和摩尔通过.stubx 文件时为 Mole 类型。 我为 MWareHouse 类型的 HasInventory 委托提供自定义实现,然后调用 Fill 方法。 请注意 nowhere 执行我提供给订单的类型构造函数仓库对象的实现。 Pex 将附加到订单类型 MWareHouse 实例在运行时。 在 PUT 的生存期内编写在块内的任何代码将充分利用 MWareHouse 类型实现旧式代码中所需仓库实现的任何位置。
当 Pex 生成使用摩尔的传统的单元测试时,它,以便它们将与将允许变为活动摩尔该 Pex 探查器会执行它们,附加属性 [HostType(“Pex”)]。
组合
我谈 Pex 和如何使用它们的各种功能。 现在它 ’s 实际运行在 PUT 并观察结果的时间。 若要进行顺序的一个研究的 Fill 方法只需在 PUT 上右击并选择运行 Pex 浏览。 可以在一个类或整个项目 (可选) 运行该探索。
运行 Pex 探索时, 连同 PUT 类文件创建分部类。 此分部类包含 Pex 将为该 PUT 生成的所有标准的单元测试。 用于填充方法 Pex 生成使用各种测试输入的标准的单元测试。 测试 的 图 10 所示。
图 10 的 Pex 生成测试
[TestMethod]
[PexGeneratedBy(typeof(OrderTest))]
public void Fill15()
{
Warehouse warehouse;
Order order;
Product product;
bool b;
warehouse = new Warehouse();
order = OrderFactory.Create((IWareHouse)warehouse);
product = new Product("Base ball", (string)null);
b = this.Fill(order, product, 0);
Assert.AreEqual<bool>(true, b);
}
[TestMethod]
[PexGeneratedBy(typeof(OrderTest))]
public void Fill16()
{
Warehouse warehouse;
Order order;
Product product;
bool b;
warehouse = new Warehouse();
order = OrderFactory.Create((IWareHouse)warehouse);
product = new Product("Basket Ball", (string)null);
b = this.Fill(order, product, 0);
Assert.AreEqual<bool>(true, b);
}
[TestMethod]
[PexGeneratedBy(typeof(OrderTest))]
public void Fill17()
{
Warehouse warehouse;
Order order;
Product product;
bool b;
warehouse = new Warehouse();
order = OrderFactory.Create((IWareHouse)warehouse);
product = new Product((string)null, (string)null);
b = this.Fill(order, product, 1);
Assert.AreEqual<bool>(false, b);
}
要观察此处的要点是产品类型的变体。 虽然我没有为其提供任何工厂,Pex 就能够创建的类型的不同变体。
此外请注意生成的测试包含断言。 当一个 PUT 返回值时,Pex 将嵌入返回了在测试生成时间到生成的测试代码作为断言的值。 作为结果生成的测试是经常能够在将来检测重大更改,即使它们 don’t 违反其他程序代码中的断言或导致在执行引擎级别的异常。
Pex 研究结果窗口 (请参阅 的 图 11) 提供了由 Pex 生成单元测试的详细信息。 它还提供了有关 Pex 创建的工厂和该研究的过程中发生的事件的信息。 注意 的 图 11 中两个测试失败。 Pex 显示了针对这两个它们 NullReferenceException。 这可以是一个常见的问题您错过出最终可能导致异常时在生产环境中运行的代码路径中放置验证检查。
图 11 的 Pex 浏览结果
Pex 不只生成测试,但还分析改进的代码。 它提供了一组可以使代码更稳定的建议。 这些建议不只是描述性的消息,但为问题区域中的实际代码。 用一个单击按钮的 Pex 代码注入实际的源文件。 选择 Pex 研究结果窗口中的失败的测试。 在右下角按钮将显示带标题添加前提条件。 单击此按钮将代码添加到源代码文件中。
这些生成的测试都正常 MSTest 单元测试,并从 Visual Studio 测试编辑器还可以运行。 如果您打开编辑器,Pex 生成的所有测试将都可作为标准的单元测试。 Pex 可以生成用于其他单元测试框架 (如 NUnit 和 xUnit 的类似测试。
Pex 还具有对生成覆盖率报表的内置支持。 这些报告提供全面的详细信息周围动态覆盖率下测试代码。 您可以启用从 Pex 选项在工具菜单中的 Visual Studio 的报表,然后通过单击视图中打开它们 | 报表 Pex 菜单栏后已完成的研究。
使您测试未来就绪
到目前为止您见 Pex 以前能够生成代码覆盖率的旧式代码与次要重构源代码的方式。 Pex 的美容是免除从编写单元测试开发人员,并生成它们自动,从而减少总体测试工作。
单元测试时主要痛苦点之一维护该测试套件本身。 随着进度在项目中,您通常情况下进行大量的对现有的代码所做的修改。 因为单元测试都依赖于源代码,代码中的任何更改会影响相应的单元测试。 它们可能会中断,或减少代码覆盖率。 一段时间内保持测试套件实时成为一项挑战。
Pex 派上用场在这种情况下。 Pex 基于探索性方法,因为它可以搜索代码基中的任何新更改和创建基于这些新测试用例。
回归测试的主要目的是检测是否修改现有代码中的添加内容有影响或基本代码产生负面,通过引入的功能的 bug 或创建新的错误条件。
Pex 可以自动生成回归测试套件。 在以后执行该回归测试套件时, 它将检测到重大的更改,导致在程序代码中失败的断言、,导致执行引擎 (NullReferenceException) 级别的异常或的导致断言嵌入在生成的测试失败。 每次运行全新,Pex 探索时为观察值下代码生成单元测试。 Pex 和相应的单位为其生成测试通过拾取行为中的任何更改。
更改是不可避免
上一段时间在 Fabrikam,开发人员团队意识到它所做的意义上以具有 ProductId 属性添加到产品类,这样,如果公司将新产品添加到它们目录它们可唯一标识。
此外订单类不保存订单到数据存储区以便新的私有方法 SaveOrders 已添加到订单类。 产品具有某些库存时,此方法将由 Fill 方法被调用。
修改后的 Fill 方法类如下所示:
public bool Fill(Product product, int quantity) {
if (product == null) {
throw new ArgumentException();
}
if (this.orderWareHouse.HasInventory(product, quantity)) {
this.SaveOrder(product.ProductId, quantity);
this.orderWareHouse.Remove(product, quantity);
return true;
}
return false;
}
不更改 Fill 方法的签名,是因为我不需要修改该 PUT。 我只需 Pex 浏览再次运行。 Pex 运行该研究,但这次它生成使用 ProductId 也利用新的产品定义的输入。 它将生成 a 全新测试套件考虑对 Fill 方法所做的更改。 代码覆盖率谈到 100 %) 以确保所有新的和现有的代码路径都已经被评估。
其他的单元测试生成的测试已添加的 ProductId 字段和对 Fill 方法所做的更改的变体 Pex (请参阅的 图 12)。 在此处 PexChooseStubBehavior 设置对于该存根 (stub) 回退行为 ; 而不是只引发一 StubNotImplementedException,存根的方法将调用 PexChoose 提供可能的返回值。 在 Visual Studio 中运行测试,代码覆盖率附带再次为 100%。
图 12 的附加 Pex 生成单元测试
[TestMethod]
[PexGeneratedBy(typeof(OrderTest))]
public void Fill12()
{
using (PexChooseStubBehavior.NewTest())
{
SIWareHouse sIWareHouse;
Order order;
Product product;
bool b;
sIWareHouse = new SIWareHouse();
order = OrderFactory.Create((IWareHouse)sIWareHouse);
product = new Product((string)null, (string)null);
b = this.Fill(order, product, 0);
Assert.AreEqual<bool>(false, b);
}
}
[TestMethod]
[PexGeneratedBy(typeof(OrderTest))]
public void Fill13()
{
using (PexChooseStubBehavior.NewTest())
{
SIWareHouse sIWareHouse;
Order order;
Product product;
bool b;
sIWareHouse = new SIWareHouse();
order = OrderFactory.Create((IWareHouse)sIWareHouse);
product = new Product((string)null, (string)null);
IPexChoiceRecorder choices = PexChoose.NewTest();
choices.NextSegment(3)
.OnCall(0,
"SIWareHouse.global::FabrikamSports.IWareHouse.HasInventory(Product, Int32)")
.Returns((object)true);
b = this.Fill(order, product, 0);
Assert.AreEqual<bool>(true, b);
}
}
致谢
我希望感谢 Peli de Halleux 和 Nikolai Tillman 鼓励我编写此文章、 特殊感谢 Peli 到他 tireless 的支持、 有价值的注释和详尽的审阅。
Nikhil Sachdeva* 是一个软件开发工程师 OCTO SE 团队在 Microsoft。您可以在 blogs.msdn.com/erudition 与他联系。您还可以过帐您周围 Pex 位于的 social.msdn.microsoft.com/Forums/en/pex/threads的查询。*