使用存根针对单元测试隔离应用程序的各个部分

存根类型 是 Microsoft 伪造品 framework 提供轻松地向独立组件与其他组件进行测试调用这两种技术之一。 存根为测试期间,取代另一个元素的一小段代码。 使用存根的优点是它返回结果一致,使测试更易于编写。 您可以运行测试,即使其他元素不起作用。

有关伪造品的概述和快速启动教程,请参见 用 Microsoft Fakes 隔离测试代码

若要使用存根,您必须编写自己的元素,以便只使用了接口,而是选件类,引用应用程序的其他部分。 因为它在一节中进行更改不太可能需要在另一个中,的更改这是一个好的设计约定。 对于测试,它允许您用实际元素替换存根。

在关系图上,组件 StockAnalyzer 是我们要测试的脚本。 它通常使用另一个组件,RealStockFeed。 但是,RealStockFeed 返回不同的结果,然后调用其方法时,会难以测试 StockAnalyzer。 在测试过程中,我们在不同的选件类替换它,StubStockFeed。

Real 和 Stub 类符合一个接口。

由于存根这取决于您可以对结构您的代码,通常使用存根隔离需要从另一个的应用程序的一部分。 若要确定它从不受您的控制,如 System.dll,您的其他程序集通常使用填充。 请参见 使用填充码针对单元测试将应用程序与程序集隔离

要求

  • Visual Studio 旗舰版

主题内容

如何使用存根

Hh549174.collapse_all(zh-cn,VS.110).gif依赖项注入的设计

若要使用存根,应用程序必须经过专门设计,以便不同的元素也相互依赖,但是,仅依赖于接口定义。 而不是耦合在编译时,元素已连接运行时。 此模式有助于使可靠且易于更新的软件,更改,所以往往不在组件边界传播。 建议在它后面,即使不使用存根。 如果要编写新代码,依赖项注入 遵循非常容易。 如果您编写测试对于现有软件,您可能必须重构它。 如果这是可行,则可以考虑使用填充。

我们以涵盖示例,一个的此讨论在关系图上。 选件类 StockAnalyzer 读取股票行市并生成一些有趣的结果。 它有一些公共方法,我们需要测试。 为简单起见,查看这些方法之一,报告特定共享的当前价格的一个非常简单的一个。 我们希望对单元测试该方法编写。 这是测试的草案初稿:

        [TestMethod]
        public void TestMethod1()
        {
            // Arrange:
            var analyzer = new StockAnalyzer();
            // Act:
            var result = analyzer.GetContosoPrice();
            // Assert:
            Assert.AreEqual(123, result); // Why 123?
        }
    <TestMethod()> Public Sub TestMethod1()
        ' Arrange:
        Dim analyzer = New StockAnalyzer()
        ' Act:
        Dim result = analyzer.GetContosoPrice()
        ' Assert:
        Assert.AreEqual(123, result) ' Why 123?
    End Sub

此的问题测试十分明显的:股票行市更改,并且,因此断言通常将失败。

另一个问题可能是 StockFeed 元素,StockAnalyzer 使用,仍在开发中。 这是方法的代码的草案初稿测试:

        public int GetContosoPrice()
        {
            var stockFeed = new StockFeed(); // NOT RECOMMENDED
            return stockFeed.GetSharePrice("COOO");
        }
    Public Function GetContosoPrice()
        Dim stockFeed = New StockFeed() ' NOT RECOMMENDED
        Return stockFeed.GetSharePrice("COOO")
    End Function

遵循现在情况,因为,在 StockFeed 选件类的工作即可完成,此方法可能无法生成或可能引发异常。

接口注入解决这两个问题。

接口注入应用以下规则:

  • 您的应用程序中的所有元素代码应从不显式引用另一个元素的选件类,在声明或在 new 语句。 相反,应声明变量和参数与接口。 应由元素的容器只创建组件实例。

    由“元素”在我们意味着选件类或同时开发和更新选件类的组。 通常,元素是一个 Visual Studio 项目中的代码。 因为同时,更新其分离选件类中的元素内不太重要。

    分离您的从一个相对稳定的平台的选件类中的元素 (如 System.dll) 也是因此不重要。 所有这些选件类编写接口将混乱您的代码。

通过使用,如下所示的接口因此 StockAnalyzer 代码可以通过分离它以从 StockFeed:

    public interface IStockFeed
    {
        int GetSharePrice(string company);
    }

    public class StockAnalyzer
    {
        private IStockFeed stockFeed;
        public Analyzer(IStockFeed feed)
        {
            stockFeed = feed;
        }
        public int GetContosoPrice()
        {
            return stockFeed.GetSharePrice("COOO");
        }
    }
Public Interface IStockFeed
    Function GetSharePrice(company As String) As Integer
End Interface

Public Class StockAnalyzer
    ' StockAnalyzer can be connected to any IStockFeed:
    Private stockFeed As IStockFeed
    Public Sub New(feed As IStockFeed)
        stockFeed = feed
    End Sub  
    Public Function GetContosoPrice()
        Return stockFeed.GetSharePrice("COOO")
    End Function
End Class

在此示例中,则在构造时,StockAnalyzer 通过 IStockFeed 的实现。 在完成的应用程序,初始化代码将执行连接:

analyzer = new StockAnalyzer(new StockFeed())

具有执行此连接更灵活的方式。 例如,StockAnalyzer 可以接受能实例化 IStockFeed 不同的实现不同的情况的工厂对象。

Hh549174.collapse_all(zh-cn,VS.110).gif生成存根

您分离了要从其他元素测试它使用的选件类。 并使应用程序更强大和灵活,分离使用户能够连接元素下仅为测试目的测试对接口的存根实现。

您可以编写存根为选件类以常规方式。 但 Microsoft 伪造品为您提供了一个更具动态方式创建每个最合适的存根测试。

若要使用存根,必须先从生成接口定义的存根 (stub) 类型。

添加伪造品程序集

  1. 在解决方案资源管理器中,展开您的单元测试项目的 引用

    • 如果使用的是 Visual Basic,必须选择在解决方案资源管理器工具栏中 显示所有文件,才能看到引用列表。
  2. 选择包含接口定义要创建存根的程序集。

  3. 在快捷菜单上,选择 添加 Fakes 程序集

Hh549174.collapse_all(zh-cn,VS.110).gif您的测试编写存根

[TestClass]
class TestStockAnalyzer
{
    [TestMethod]
    public void TestContosoStockPrice()
    {
      // Arrange:

        // Create the fake stockFeed:
        IStockFeed stockFeed = 
             new StockAnalysis.Fakes.StubIStockFeed() // Generated by Fakes.
                 {
                     // Define each method:
                     // Name is original name + parameter types:
                     GetSharePriceString = (company) => { return 1234; }
                 };

        // In the completed application, stockFeed would be a real one:
        var componentUnderTest = new StockAnalyzer(stockFeed);

      // Act:
        int actualValue = componentUnderTest.GetContosoPrice();

      // Assert:
        Assert.AreEqual(1234, actualValue);
    }
    ...
}
<TestClass()> _
Class TestStockAnalyzer

    <TestMethod()> _
    Public Sub TestContosoStockPrice()
        ' Arrange:
        ' Create the fake stockFeed:
        Dim stockFeed As New StockAnalysis.Fakes.StubIStockFeed
        With stockFeed
            .GetSharePriceString = Function(company)
                                       Return 1234
                                   End Function
        End With
        ' In the completed application, stockFeed would be a real one:
        Dim componentUnderTest As New StockAnalyzer(stockFeed)
        ' Act:
        Dim actualValue As Integer = componentUnderTest.GetContosoPrice
        ' Assert:
        Assert.AreEqual(1234, actualValue)
    End Sub
End Class

此处魔术特定部分是选件类 StubIStockFeed。 对于每个公共输入引用的程序集,结构生成存根选件类的 Microsoft 伪造品。 存根选件类的名称是从派生接口的名称,与“Fakes.Stub”作为标题和参数类型追加的名称。

存根还会生成用于属性的 getter 和 setter,为事件和对泛型方法。

Hh549174.collapse_all(zh-cn,VS.110).gif验证参数值

您可以验证,而您的组件仅仅是到另一个元素时,将正确的值。 在存根可以将断言,也可以在测试的主体可以存储值并验证它。 例如:

[TestClass]
class TestMyComponent
{
       
    [TestMethod]
    public void TestVariableContosoPrice()
    {
     // Arrange:
        int priceToReturn;
        string companyCodeUsed;
        var componentUnderTest = new StockAnalyzer(new StubIStockFeed()
            {
               GetSharePriceString = (company) => 
                  { 
                     // Store the parameter value:
                     companyCodeUsed = company;
                     // Return the value prescribed by this test:
                     return priceToReturn;
                  };
            };
        // Set the value that will be returned by the stub:
        priceToReturn = 345;

     // Act:
        int actualResult = componentUnderTest.GetContosoPrice(priceToReturn);

     // Assert:
        // Verify the correct result in the usual way:
        Assert.AreEqual(priceToReturn, actualResult);

        // Verify that the component made the correct call:
        Assert.AreEqual("COOO", companyCodeUsed);
    }
...}
<TestClass()> _
Class TestMyComponent
    <TestMethod()> _
    Public Sub TestVariableContosoPrice()
        ' Arrange:
        Dim priceToReturn As Integer
        Dim companyCodeUsed As String = ""
        Dim stockFeed As New StockAnalysis.Fakes.StubIStockFeed()
        With stockFeed
            ' Implement the interface's method:
            .GetSharePriceString = _
                Function(company)
                    ' Store the parameter value:
                    companyCodeUsed = company
                    ' Return a fixed result:
                    Return priceToReturn
                End Function
        End With
        ' Create an object to test:
        Dim componentUnderTest As New StockAnalyzer(stockFeed)
        ' Set the value that will be returned by the stub:
        priceToReturn = 345

        ' Act:
        Dim actualResult As Integer = componentUnderTest.GetContosoPrice()

        ' Assert:
        ' Verify the correct result in the usual way:
        Assert.AreEqual(priceToReturn, actualResult)
        ' Verify that the component made the correct call:
        Assert.AreEqual("COOO", companyCodeUsed)
    End Sub
...
End Class

不同类型的存根类型成员

Hh549174.collapse_all(zh-cn,VS.110).gif方法

如该示例所述,方法可通过附加存根选件类的实例的委托碰。 存根类型的名称从方法和参数的名称派生。 例如将以下 IMyInterface 接口和方法 MyMethod:

// application under test
interface IMyInterface 
{
    int MyMethod(string value);
}

我们将总是返回 1 的存根 (stub) 对 MyMethod :

// unit test code
  var stub = new StubIMyInterface ();
  stub.MyMethodString = (value) => 1;

如果为函数不提供存根,伪造品将生成返回类型的默认值的函数。 对于数字,默认值为 0,并且,对于选件类类型是 null (c#) 或 Nothing (Visual Basic)。

Hh549174.collapse_all(zh-cn,VS.110).gif属性

属性 getter 和 setter 显示为单独的委托,并可以单独无存根 (stubbed-out)。 例如,请考虑 IMyInterfaceValue 属性:

// code under test
interface IMyInterface 
{
    int Value { get; set; }
}

我们将委托附加到 Value 的 getter 和 setter 模拟自动属性:

// unit test code
int i = 5;
var stub = new StubIMyInterface();
stub.ValueGet = () => i;
stub.ValueSet = (value) => i = value;

如果为 setter 或属性的 getter 方法不提供存根方法,伪造品将生成中存储值的存根,因此,存根 (stub) 属性的工作方式类似于简单变量。

Hh549174.collapse_all(zh-cn,VS.110).gif事件

事件公开为委托字段。 因此,所有无存根 (stubbed-out) 的事件可通过调用操作支持字段引发。 让我们考虑以下接口存根:

// code under test
interface IWithEvents 
{
    event EventHandler Changed;
}

若要引发 Changed 事件,我们调用支持委托:

// unit test code
  var withEvents = new StubIWithEvents();
  // raising Changed
  withEvents.ChangedEvent(withEvents, EventArgs.Empty);

Hh549174.collapse_all(zh-cn,VS.110).gif泛型方法

可能对存根泛型方法通过提供委托为方法的每个所需实例化。 例如命名包含泛型方法的以下接口:

// code under test
interface IGenericMethod 
{
    T GetValue<T>();
}

您可以编写的测试存根 GetValue<int> 实例化:

// unit test code
[TestMethod]
public void TestGetValue() 
{
    var stub = new StubIGenericMethod();
    stub.GetValueOf1<int>(() => 5);

    IGenericMethod target = stub;
    Assert.AreEqual(5, target.GetValue<int>());
}

如果代码是调用与其他实例化的 GetValue<T>,存根将调用该行为。

Hh549174.collapse_all(zh-cn,VS.110).gif虚拟选件类存根

在前面的示例中,存根从接口生成。 还可以生成具有虚拟或抽象成员的选件类的存根。 例如:

// Base class in application under test
    public abstract class MyClass
    {
        public abstract void DoAbstract(string x);
        public virtual int DoVirtual(int n)
        { return n + 42; }
        public int DoConcrete()
        { return 1; }
    }

在从此选件类生成的存根,可以设置 DoAbstract() 和 DoVirtual() 的委托不是方法,但是,DoConcrete()。

// unit test
  var stub = new Fakes.MyClass();
  stub.DoAbstractString = (x) => { Assert.IsTrue(x>0); };
  stub.DoVirtualInt32 = (n) => 10 ;
  

如果为虚拟方法不提供一个委托,伪造品可以提供一个默认值行为,也可以调用基类的方法。 若要让该基方法调用,请设置 CallBase 属性:

// unit test code
var stub = new Fakes.MyClass();
stub.CallBase = false;
// No delegate set – default delegate:
Assert.AreEqual(0, stub.DoVirtual(1));

stub.CallBase = true;
//No delegate set - calls the base:
Assert.AreEqual(43,stub.DoVirtual(1));

调试存根

存根类型旨在提供平滑调试体验。 默认情况下,调试器将指示单步执行所有生成的代码,因此,应单步执行直接添加到附加到存根的自定义成员实现。

存根限制

  1. 使用指针的方法签名不受支持。

  2. 因为存根类型依赖于虚方法计划,密封选件类或静态方法不能碰。 对于这种情况下,使用上述类型添加 使用填充码针对单元测试将应用程序与程序集隔离所述

更改存根默认值行为

每个生成的存根类型来保存 IStubBehavior 接口的实例 (通过 IStub.InstanceBehavior 属性)。 该行为调用,只要客户端调用成员未附加自定义委托。 如果该行为未设置,它将使用 StubsBehaviors.Current 属性返回的实例。 默认情况下,此属性返回引发 NotImplementedException 异常行为。

该行为可将任何存根实例的 InstanceBehavior 属性在 + 任何 + 时间更改。 例如,下面的代码段更改不执行任何操作或返回返回类型的默认值的行为:default(T):

// unit test code
var stub = new StubIFileSystem();
// return default(T) or do nothing
stub.InstanceBehavior = StubsBehaviors.DefaultValue;

该行为可能为行为未通过设置 StubsBehaviors.Current 属性设置为的任何存根对象全局也会更改:

// unit test code
//change default behavior for all stub instances
//where the behavior has not been set
StubBehaviors.Current = 
    BehavedBehaviors.DefaultValue;

外部资源

Hh549174.collapse_all(zh-cn,VS.110).gif指南

测试使用 Visual Studio 进行附带的 2012 版–第 2 章:单元测试:测试。

请参见

概念

用 Microsoft Fakes 隔离测试代码