# 四、测试代码契约

你们中的许多人可能对 Pex 很熟悉，Pex 演变成 IntelliTest 可能并不令人惊讶。智能测试是 Visual Studio Enterprise 2015 中的一项集成功能。代码契约和智能测试集成得很好，开始使用智能测试真的很容易。

如前所述，智能测试已集成到 Visual Studio Enterprise 2015 中。如果您希望在 Visual Studio 2015 的其他版本中集成 IntelliTest，您可以在 Visual Studio 2015 [用户语音网站](https://visualstudio.uservoice.com/forums/121579-visual-studio-2015)上发出自己的声音。微软在 Visual Studio 团队中的成员确实会密切关注这个网站——你的投票永远不会白费。

智能测试的入门非常简单。让我们重温一下我们在上一章中使用的一种方法。

```cs
    /// <summary>
    /// Calculate the production volume of steel per bin
    /// </summary>
    /// <param name="binVolume"></param>
    /// <param name="factor"></param>
    /// <returns>Bin Volume less Remainder</returns>
    public int ProductionVolumePerBin(int binVolume, int factor)
    {
        Contract.Ensures(Contract.Result<int>() == binVolume,
            "The factor used will result in scrap. Please modify the cutting factor.");

        int remainder = CutSteel(binVolume, factor);
        return binVolume - remainder;
    }

    /// <summary>
    /// Calculate any remainder after the modulus operation between volume and factor
    /// </summary>
    /// <param name="volumeToCut"></param>
    /// <param name="factor"></param>
    /// <returns>Remainder after cutting</returns>
    private int CutSteel(int volumeToCut, int factor)
    {
        // Use modulus to determine if the factor produces any scrap
        return volumeToCut % factor;
    }

```

代码清单 61:为方法创建智能测试

和前面一样，前面的代码清单告诉调用方法，契约下的方法将导致切割体积始终等于容器体积。这意味着所有的钢都被完美地切割，并且没有由于使用特定因子而导致的切断。

要为`ProductionVolumePerBin()`方法生成新的智能测试，右键单击该方法并从上下文菜单中选择`Create IntelliTest`。

![](img/00047.jpeg)

图 45:创建智能测试

Visual Studio 现在将显示“创建智能测试”窗口，您可以在其中为生成的智能测试配置其他设置。

![](img/00048.jpeg)

图 46:创建智能测试设置

如果这是你第一次创建智能测试，你会看到`MSTest`是`Test Framework`下面列出的唯一选项。但是，如果需要，您可以安装第三方单元测试框架(稍后将对此进行详细介绍)。完成后，点击`OK`。

![](img/00049.jpeg)

图 47:添加到解决方案中的测试项目

Visual Studio 现在将为您创建测试项目。完成流程后，新项目将在`Solution Explorer`窗口中可见。

![](img/00050.jpeg)

图 48:智能测试已创建

展开`ERPWarehouseIntegrationTest.cs`文件，你会看到已经创建了一个名为`ProductionVolumePerBinTest`的测试。

```cs
    // <copyright file="ERPWarehouseIntegrationTest.cs">Copyright © 2015</copyright>
    using System;
    using Microsoft.Pex.Framework;
    using Microsoft.Pex.Framework.Validation;
    using Microsoft.VisualStudio.TestTools.UnitTesting;

    namespace CodeContractsDemoProject.Tests
    {
        /// <summary>This class contains parameterized unit tests for ERPWarehouseIntegration</summary>
        [PexClass(typeof(ERPWarehouseIntegration))]
        [PexAllowedExceptionFromTypeUnderTest(typeof(InvalidOperationException))]
        [PexAllowedExceptionFromTypeUnderTest(typeof(ArgumentException), AcceptExceptionSubtypes = true)]
        [TestClass]
        public partial class ERPWarehouseIntegrationTest
        {
            /// <summary>Test stub for ProductionVolumePerBin(Int32, Int32)</summary>
    [PexMethod]
            public int ProductionVolumePerBinTest(
                [PexAssumeUnderTest]ERPWarehouseIntegration target,
                int binVolume,
                int factor
            )
            {
                int result = target.ProductionVolumePerBin(binVolume, factor);
                return result;
                // TODO: add assertions to method ERPWarehouseIntegrationTest.ProductionVolumePerBinTest(ERPWarehouseIntegration, Int32, Int32)
            }
        }
    }

```

代码清单 62:已创建生产卷测试

生成这个测试只需要点击几下鼠标。IntelliTest 的集成允许开发人员轻松地为他们代码中的关键逻辑创建测试。

在我们创建了第一个智能测试之后，我们需要对它做些什么。逻辑选项是运行测试，并查看该方法的测试结果。为此，右键单击该方法，然后在上下文菜单中单击`Run IntelliTest`。

![](img/00051.jpeg)

图 49:运行智能测试

Visual Studio 将继续构建您的项目，然后启动`IntelliTest Exploration Results`窗口。此窗口显示所有可能的方法参数，这些参数将导致被测试方法的最大代码覆盖率。现在，您可以大致了解该方法在测试中的表现。

![](img/00052.jpeg)

图 50:智能测试探索结果

马上我们可以看到三次测试失败，一次通过。在`IntelliTest Exploration Results`窗口的右侧，`Details`和`Stack trace`显示在可折叠节点中。让我们仔细看看测试结果。

### 测试 2:通过

`ProductionVolumePerBin()`方法必须始终产生等于料仓体积的切割体积。从测试 2 中使用的变量，我们可以看到它通过了，因为箱体积是`0`，方法的结果也是`0`。

```cs
    [TestMethod]
    [PexGeneratedBy(typeof(ERPWarehouseIntegrationTest))]
    public void ProductionVolumePerBinTest33()
    {
        ERPWarehouseIntegration eRPWarehouseIntegration;
        int i;
        eRPWarehouseIntegration = new ERPWarehouseIntegration();
        i = this.ProductionVolumePerBinTest(eRPWarehouseIntegration, 0, 1);
        Assert.AreEqual<int>(0, i);
        Assert.IsNotNull((object)eRPWarehouseIntegration);
        Assert.AreEqual<int>(0, eRPWarehouseIntegration.MaxBinQuantity);
        Assert.AreEqual<int>(0, eRPWarehouseIntegration.CurrentBinQuantity);
    }

```

代码清单 63:详细生成的代码

查看`Details`节点，我们可以看到为单元测试生成的代码。

### 测试 1:失败，个体异常

看看测试 1，我们可以看到它失败了，因为它试图除以零。

![](img/00053.jpeg)

图 51:除以零异常

这意味着我们忘记了处理因子值为零。如果你看一下`CutSteel()`方法，你会看到我们处理的是一个模数，而不是除数。那么我们怎么会有被零除的例外呢？经验法则是，如果一个`/`或一个`%`的第二个操作数为零，我们会有一个除以零的异常。

### 测试 3:失败，合同异常

测试 3 失败了，因为代码契约确保了方法不遵守的结果。请记住，`ProductionVolumePerBin()`方法确保料箱体积返回到调用代码，这意味着我们已经产生了零废料的完美切割。测试失败，在生产环境中会出现这种情况。我们的代码契约确保方法返回的结果总是等于容器的体积。这意味着我们需要应对不完美削减的可能性。

### 测试 4:失败，飞越异常

最后一次失败的测试相当有趣。我们可以看到`binVolume`的测试通过`int.MinValue`。根据 MSDN 文件，`int.MinValue`代表`Int32`的最小可能值。它也是一个常量，值为`-2,147,483,648`。该系数的值为`-1`，而模量为`OverflowException`。这意味着我们在`CutSteel()`方法中的代码实际上将是`int.MinValue % -1;`，这没有通过我们的测试。

发生这种情况的原因是因为 C# 语言规范是这样实现的。在`C# Language Specification`的`7.8.3 Remainder operator`中表示:“如果左操作数是最小的`int`或`long`值，右操作数是`-1`，则抛出一个`System.OverflowException`

| ![](img/00009.gif) | 注意:如果你想深入一点，你可以在 StackOverflow 上查看[这个线程](https://stackoverflow.com/questions/31775042/why-does-the-c-sharp-specification-leave-int-minvalue-1-implementation-defi)，获得一个很好的解释，为什么在 C# 中会这样实现。 |

从前面的智能测试中我们可以看出我们的`ProductionVolumePerBin()`方法存在一些漏洞。这些都需要修复，最明显要修复的就是除零异常。让我们在我们的方法中添加一个`Contract.Requires()`，只允许因子值大于`1`。

```cs
    /// <summary>
    /// Calculate the production volume of steel per bin
    /// </summary>
    /// <param name="binVolume"></param>
    /// <param name="factor"></param>
    /// <returns>Bin Volume less Remainder</returns>
    public int ProductionVolumePerBin(int binVolume, int factor)
    {
        Contract.Requires(factor > 1,
            "The supplied cutting factor must be more than the value 1.");
        Contract.Ensures(Contract.Result<int>() == binVolume,
        "The factor used will result in scrap. Please modify the cutting factor.");

        int remainder = CutSteel(binVolume, factor);
        return binVolume - remainder;
    }

```

代码清单 64:确保有效整数的修改代码

添加`Contract.Requires()`前提条件后，该方法只允许有效的切割因子值。右键单击`ProductionVolumePerBin()`方法并从上下文菜单中选择`Run IntelliTest`，再次运行智能测试。

![](img/00054.jpeg)

图 52:有效整数更改后的智能测试结果

测试的结果大不相同。我们的合同前提条件工作正常，并且限制了错误的值传递给我们的方法。然而，我们可以看到智能测试通过了一个明显小于 T1 值的 T0 值。让我们先解决这个问题，要求`binVolume`值永远不要小于`factor`值。

```cs
    /// <summary>
    /// Calculate the production volume of steel per bin
    /// </summary>
    /// <param name="binVolume"></param>
    /// <param name="factor"></param>
    /// <returns>Bin Volume less Remainder</returns>
    public int ProductionVolumePerBin(int binVolume, int factor)
    {
        Contract.Requires(factor > 1,
            "The supplied cutting factor must be more than the value 1.");
        Contract.Requires(binVolume > factor,
            "The cutting factor cannot be greater than the bin volume");
        Contract.Ensures(Contract.Result<int>() == binVolume,
            "The factor used will result in scrap. Please modify the cutting factor.");

        int remainder = CutSteel(binVolume, factor);
        return binVolume - remainder;
    }

```

代码清单 65:修改代码以确保有效的切割因子

为此，我们需要添加另一个`Contract.Requires()`前置条件，要求`binVolume`值始终大于`factor`值。从上下文菜单中再次运行智能测试。

![](img/00055.jpeg)

图 53:有效切割系数改变后的智能测试结果

从该测试返回的结果告诉我们，我们在使用`ProductionVolumePerBin()`方法时遇到的唯一问题是，它在返回的值上仍然失败。我们的方法保证调用代码每次都会返回一个完美的切割，但是它没有通过这个契约。如果提供的切割因子无效，我们可能会考虑让我们的代码向用户建议一个有效的切割因子，从而为这个方法增加一点智能。

```cs
    /// <summary>
    /// The new valid cutting factor calculated by ProductionVolumePerBin
    /// </summary>
    public int CalculatedCuttingFactor { get; private set; } = 0;

    /// <summary>
    /// Calculate the production volume of steel per bin
    /// </summary>
    /// <param name="binVolume"></param>
    /// <param name="factor"></param>
    /// <returns>Bin Volume less Remainder</returns>
    public int ProductionVolumePerBin(int binVolume, int factor)
    {
        Contract.Requires(IsEven(binVolume),
            "Invalid bin volume entered");
        Contract.Requires(factor > 1,
            "The supplied cutting factor must be more than the value 1.");
        Contract.Requires(binVolume > factor,
            "The cutting factor cannot be greater than the bin volume");
        Contract.Ensures(Contract.Result<int>() == binVolume,
            "The factor used will result in scrap. Please modify the cutting factor.");

        int remainder = CutSteel(binVolume, factor);
        while ((binVolume - remainder) != binVolume)
        {
            CalculatedCuttingFactor = CalculateNewCutFactor(binVolume);
            remainder = CutSteel(binVolume, CalculatedCuttingFactor);
        }

        return binVolume - remainder;
    }

    /// <summary>
    /// Calculate any remainder after the modulus operation between volume and factor
    /// </summary>
    /// <param name="volumeToCut"></param>
    /// <param name="factor"></param>
    /// <returns>Remainder after cutting</returns>
    private int CutSteel(int volumeToCut, int factor)
    {
        // Use modulus to determine if the factor produces any scrap
        return volumeToCut % factor;
    }

    /// <summary>
    /// Calculate a new cutting factor
    /// r.Next(1, 7); returns a random number between 1 and 6
    /// </summary>
    /// <param name="binVol">Upper range value of random (bin volume + 1)</param>
    /// <returns>
    /// A new cutting factor greater than 1 and equal to the bin volume
    /// </returns>
    private int CalculateNewCutFactor(int binVol)
    {
        Random r = new Random();
        return r.Next(2, binVol + 1);
    }

    /// <summary>
    /// Ensure that the passed volume is even
    /// </summary>
    /// <param name="volume">The volume to verify</param>
    /// <returns>boolean</returns>
    public bool IsEven(int volume)
    {
        return volume % 2 == 0;
    }

```

代码清单 66:智能生产卷绑定方法

从前面修改的代码清单中可以看出，我做了一些事情。业务规则规定箱容量将始终为偶数。因此，我添加了一个`Contract.Requires()`前置条件，以确保只有偶数被传递给`ProductionVolumePerBin()`方法。

我们代码的另一个新增内容是`CalculatedCuttingFactor`属性，如果提供的因子无效，该属性将保存新计算的切割因子。为此，我加入了一种叫做`CalculateNewCutFactor`的新方法，它将尝试切割因子的替代值，以产生完美的切割。

`ProductionVolumePerBin()`方法将确定切割因子是否有效。如果没有，它将运行`while`循环，直到返回有效的切割因子并实现完美切割。但是，在生产环境中，您可能希望考虑使用健全循环计数器变量来创建退出条件，或者在达到某个最大迭代次数时引发异常。对`ProductionVolumePerBin()`方法还有很多微调可以做，这里我就不赘述了，因为我只是想说明一个概念。

修改代码后，再次运行智能测试。

![](img/00056.jpeg)

图 54:所有智能测试通过

从测试结果中我们可以看到`ProductionVolumePerBin()`方法满足了我们的代码契约的要求。它还生成了六个警告，我稍后将讨论这些警告。

调用代码现在可以实现`ProductionVolumePerBin()`方法，而不需要处理返回的无效值。它知道该方法每次都会返回一个完美的切割。唯一需要做的检查是查看是否已经建议了新的因子，或者所提供的因子是否有效。

```cs
    int binVol = 20;
    int factor = 3;
    CodeContractsDemoProject.ERPWarehouseIntegration oWhi =
        new CodeContractsDemoProject.ERPWarehouseIntegration();
    int result = oWhi.ProductionVolumePerBin(binVol, factor);
    if (oWhi.CalculatedCuttingFactor != factor && oWhi.CalculatedCuttingFactor != 0)
    {
        Console.Write($"The supplied cutting factor of {factor} resulted in "
            + "an imperfect cut. The system suggests using the following "
            + $"cutting factor: {oWhi.CalculatedCuttingFactor}");
    }
    else
        Console.Write($"The cutting factor of {factor} resulted in 0 scrap");
    Console.ReadLine();

```

代码清单 67:调用生产卷绑定的代码

你会注意到我每次都在`Console.Write`中使用`string interpolation`。这是 C# 6 中的新特性之一。前面的代码只需要检查`CalculatedCuttingFactor`属性，看看切割因子是否已经改变。它知道在契约下，`ProductionVolumePerBin()`方法总是会导致完美的切割。

我们可以进一步改进前面的代码，但是我想说明的概念很清楚。代码契约非常适合在 Visual Studio Enterprise 2015 中使用智能测试创建的测试。您可以结合这两种技术的力量来创建高度健壮的代码和高度强制的业务规则，以使您的应用程序在生产环境中表现良好。

如果不讨论警告输出屏幕，智能测试的讨论将是不完整的。

![](img/00057.jpeg)

图 55:智能测试警告

您会注意到`IntelliTest Exploration Results`屏幕生成了六个警告。这些通常是需要你注意的警告。为了安全起见，请检查这些文件中是否有任何明显的问题。虽然这有点超出了本书的范围，但我将简要提及在`Testability`和`Boundary`中看到的两种警告类型。

### 易测性

我们看到的警告是，在我们的代码中检测到了一个未编译的方法。这是特定于`CalculateNewCutFactor()`方法中使用的`Random()`方法的。这仅仅意味着 IntelliTest 不能深入到我的代码中的所有路径，以便生成它需要的输出来进行测试。

### 分界线

智能测试对它执行的路径施加了某些限制，以防止它在应用程序进入无限循环的情况下卡住。通过点击菜单栏上的`Fix`图标可以修改这些限制。

### 代码覆盖率

理想情况下，您希望看到 100%的代码覆盖率(33/33 块)。我们的测试只覆盖了 28/33 个街区。进一步阅读智能测试将使您了解如何确保良好的代码覆盖率，以及哪些警告可以被安全地抑制。

如前所述，在创建智能测试时，您可以获得测试框架的附加扩展。为此，您可以使用 Visual Studio 扩展管理器或前往 MSDN 网站上的 [Visual Studio 图库](https://visualstudiogallery.msdn.microsoft.com/)。

以下是如何从 Visual Studio 扩展安装第三方框架:

1.  导航至`Tools`并选择`Extensions and Updates`。
2.  展开`Online` > `Visual Studio Gallery` > `Tools`，选择`Testing`。
3.  浏览结果并选择您需要的框架。
4.  点击`Download`。

![](img/00058.jpeg)

图 56:从 Visual Studio 安装第三方框架

从 Visual Studio 库安装第三方框架:

1.  前往 [Visual Studio 图库](https://visualstudiogallery.msdn.microsoft.com/)。
2.  在`Find`文本框中输入框架名称。
3.  从搜索结果中选择要下载的框架。

如果您不确定要使用哪个框架，或者不知道要查找的框架的名称，可以在 Visual Studio Gallery 上浏览框架列表:

1.  前往 [Visual Studio 图库](https://visualstudiogallery.msdn.microsoft.com/)。
2.  点击`Browse`链接。
3.  在`Categories`下，展开`Tools`，选择`Testing`。
4.  选择您想要的框架并下载工具。