使用存根隔离应用程序的各个部分以进行单元测试

存根类型是 Microsoft Fakes 框架提供的重要技术,可以轻松地隔离受测组件与它依赖的其他组件。 存根充当一小段代码,用来在测试期间替换另一个组件。 使用存根的主要好处是能够获取一致的结果,以更轻松地编写测试。 即使其他组件尚未完全正常运行,仍可使用存根执行测试。

为了有效地应用存根,建议以主要依赖接口而不是来自应用程序其他部件的具体类的方式设计组件。 此设计方法可促进分离,并降低一部分发生更改时需要修改另一部分的可能性。 在测试方面,此设计模式允许将存根实现替换为实际组件,从而促进目标组件的有效隔离和准确测试。

例如,请考虑以下关系图,该图演示了所涉及的组件:

Diagram of Real and Stub classes of StockAnalyzer.

在此图中,受测组件为 StockAnalyzer,它通常依赖另一个名为 RealStockFeed 的组件。 但是,RealStockFeed 对测试提出了一个难题,因为它在每次调用其方法时都会返回不同的结果。 这种差异使得很难确保对 StockAnalyzer 进行一致且可靠的测试。

为了在测试期间克服此障碍,我们可以采用依赖关系注入的做法。 此方法涉及在编写代码时不应明确提及应用程序的其他组件中的类。 相反,可以定义另一个组件和存根可为测试目的实现的接口。

下面是如何在代码中使用依赖关系注入的示例:

public int GetContosoPrice(IStockFeed feed) => feed.GetSharePrice("COOO");

存根限制

查看存根的以下限制。

创建存根:分步指南

我们通过一个启发性示例(上图中的示例)开始此练习。

创建类库

按照以下步骤创建类库。

  1. 打开 Visual Studio 并创建类库项目。

    Screenshot of Class Library project in Visual Studio.

  2. 配置项目属性:

    • 将“项目名称”设置为 StockAnalysis
    • 将“解决方案名称”设置为 StubsTutorial
    • 将项目“目标框架”设置为 .NET 8.0
  3. 删除默认文件 Class1.cs

  4. 添加名为 IStockFeed.cs 的新文件,并在以下接口定义中复制:

    // IStockFeed.cs
    public interface IStockFeed
    {
        int GetSharePrice(string company);
    }
    
  5. 添加名为 StockAnalyzer.cs 的另一个新文件,并在以下类定义中复制:

    // StockAnalyzer.cs
    public class StockAnalyzer
    {
        private IStockFeed stockFeed;
        public StockAnalyzer(IStockFeed feed)
        {
            stockFeed = feed;
        }
        public int GetContosoPrice()
        {
            return stockFeed.GetSharePrice("COOO");
        }
    }
    

创建测试项目

为练习创建测试项目。

  1. 右键单击解决方案并添加名为 MSTest 测试项目的新项目。

  2. 将项目名称设置为 TestProject

  3. 将项目的目标框架设置为 .NET 8.0

    Screenshot of Test project in Visual Studio.

添加 Fakes 程序集

为项目添加 Fakes 程序集。

  1. 添加对 StockAnalyzer 的项目引用。

    Screenshot of the command Add Project Reference.

  2. 添加 Fakes 程序集。

    1. 在“解决方案资源管理器”中,找到程序集引用:

      • 对于旧版 .NET Framework 项目(非 SDK 样式),展开单元测试项目的“引用”节点。

      • 对于面向 .NET Framework、.NET Core 或 .NET 5.0 或更高版本的 SDK 样式项目,展开“依赖项”节点,以在“程序集”、“项目”或“包”下找到要虚设的程序集。

      • 如果使用的是 Visual Basic,请选择“解决方案资源管理器”工具栏中的“显示所有文件”,以查看“引用”节点。

    2. 选择包含要为其创建存根的类定义的程序集。

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

      Screenshot of the command Add Fakes Assembly.

创建单元测试

现在创建单元测试。

  1. 修改默认文件 UnitTest1.cs 以添加以下 Test Method 定义。

    [TestClass]
    class UnitTest1
    {
        [TestMethod]
        public void TestContosoPrice()
        {
            // Arrange:
            int priceToReturn = 345;
            string companyCodeUsed = "";
            var componentUnderTest = new StockAnalyzer(new StockAnalysis.Fakes.StubIStockFeed()
            {
                GetSharePriceString = (company) =>
                {
                    // Store the parameter value:
                    companyCodeUsed = company;
                    // Return the value prescribed by this test:
                    return priceToReturn;
                }
            });
    
            // Act:
            int actualResult = 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);
        }
    }
    

    此处最为神奇的就是 StubIStockFeed 类。 对于所引用程序集中的每个接口,Microsoft Fakes 机制将生成一个存根类。 存根类的名称派生自接口的名称,前缀为“Fakes.Stub”,并且在名称后面追加了参数类型名称。

    另外,还会为属性的 getter 和 setter、事件和泛型方法生成存根。 有关详细信息,请参阅使用存根隔离应用的各个部分以供单元测试使用

    Screenshot of Solution Explorer showing all files.

  2. 打开测试资源管理器并运行测试。

    Screenshot of Test Explorer.

不同类型类型成员的存根

不同种类的类型成员都有存根。

方法

在提供的示例中,可以通过将委托附加到存根类的实例对方法进行存根处理。 存根类型的名称是从方法名称和参数名称派生而来的。 例如,请考虑以下 IStockFeed 接口和 GetSharePrice 方法:

// IStockFeed.cs
interface IStockFeed
{
    int GetSharePrice(string company);
}

我们使用 GetSharePriceString 将存根附加到 GetSharePrice

// unit test code
var componentUnderTest = new StockAnalyzer(new StockAnalysis.Fakes.StubIStockFeed()
        {
            GetSharePriceString = (company) =>
            {
                // Store the parameter value:
                companyCodeUsed = company;
                // Return the value prescribed by this test:
                return priceToReturn;
            }
        });

如果没有为某个方法提供存根,Fakes 将生成一个返回该返回类型的 default value 的函数。 对于数字,默认值为 0。 对于类类型,默认值在 C# 中为 null,在 Visual Basic 中为 Nothing

属性

属性 getter 和 setter 作为单独的委托公开,可分别进行存根处理。 例如,考虑 ValueIStockFeedWithProperty 属性:

interface IStockFeedWithProperty
{
    int Value { get; set; }
}

若要对 Value 的 getter 和 setter 进行存根处理并模拟自动属性,可以使用以下代码:

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

如果没有为属性的 setter 或 getter 提供存根方法,Fakes 将生成一个存储值的存根,以便让存根属性像简单变量一样工作。

事件

事件将作为委托字段公开,只需调用事件支持字段即可引发任何存根事件。 让我们考虑以下要进行存根处理的接口:

interface IStockFeedWithEvents
{
    event EventHandler Changed;
}

要引发 Changed 事件,请调用支持委托:

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

泛型方法

通过为方法的每个必需的实例化提供委托可以对泛型方法进行存根处理。 例如,假定有以下包含泛型方法的接口:

interface IGenericMethod
{
    T GetValue<T>();
}

可以对 GetValue<int> 实例化进行存根处理,如下所示:

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

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

如果代码调用具有任何其他实例化的 GetValue<T>,存根将会执行该行为。

虚拟类的存根

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

// 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 ;

如果没有为虚方法提供委托,Fakes 可以提供默认行为,也可以调用基类中的方法。 若要调用基方法,请设置 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));

更改存根的默认行为

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

可以通过设置任何存根实例的 InstanceBehavior 属性来随时更改该行为。 例如,下面的代码片段将更改该行为,以便存根不执行任何操作或返回返回类型的默认值,即 default(T)

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

对于未使用 StubsBehaviors.Current 属性设置行为的所有存根对象,还可以在全局范围内更改该行为:

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