使用填充码针对单元测试将应用程序与程序集隔离

Shim types are one of two technologies that the Microsoft Fakes Framework uses to let you easily isolate components under test from the environment.Shims divert calls to specific methods to code that you write as part of your test.Many methods return different results dependent on external conditions, but a shim is under the control of your test and can return consistent results at every call.This makes your tests much easier to write.

Use shims to isolate your code from assemblies that are not part of your solution.To isolate components of your solution from each other, we recommend that you use stubs.

For an overview and quick start guidance, see 用 Microsoft Fakes 隔离测试代码

要求

  • Visual Studio 旗舰版

See Video (1h16): Testing Un-testable Code with Fakes in Visual Studio 2012

主题内容

Here’s what you’ll learn in this topic:

Example: The Y2K bug

How to use shims

  • 添加 Fakes 程序集

  • 使用 ShimsContext

  • Write Tests with Shims

用于不同类型方法的存根

更改默认行为

检测环境访问权限

并发

调用从 shim 方法的初始方法

限制

示例: 计算机 2000 年问题 Bug

考虑引发 1 月 1 日的异常 2000 年的方法:

// code under test
public static class Y2KChecker {
    public static void Check() {
        if (DateTime.Now == new DateTime(2000, 1, 1))
            throw new ApplicationException("y2kbug!");
    }
}

测试此方法尤其有问题的,因为程序依赖于 DateTime.Now,取决于计算机的时钟,环境的方法依赖项,非确定性的方法。此外,DateTime.Now 为静态属性,因此无法在此处使用类型。此问题是基于的症状在单元测试中隔离问题:直接调用数据库 API 的程序,与 Web 服务通信,依此类推很难更改单元测试,因为它们的逻辑依赖环境。

这是应该使用的地方以代码类型。Shim types provide a mechanism to detour any .NET method to a user defined delegate.Shim types are code-generated by the Fakes generator, and they use delegates, which we call shim types, to specify the new method implementations.

The following test shows how to use the shim type, ShimDateTime, to provide a custom implementation of DateTime.Now:

//unit test code
// create a ShimsContext cleans up shims 
using (ShimsContext.Create()
    // hook delegate to the shim method to redirect DateTime.Now
    // to return January 1st of 2000
    ShimDateTime.NowGet = () => new DateTime(2000, 1, 1);
    Y2KChecker.Check();
}

如何使用填充

添加 Fakes 程序集

  1. In Solution Explorer, expand your unit test project’s References.

    • If you are working in Visual Basic, you must select Show All Files in the Solution Explorer toolbar, in order to see the References list.
  2. Select the assembly that contains the classes definitions for which you want to create shims.For example, if you want to shim DateTime, select System.dll

  3. On the shortcut menu, choose Add Fakes Assembly.

使用 ShimsContext

When using shim types in a unit test framework, you must wrap the test code in a ShimsContext to control the lifetime of your shims.If we didn’t require this, your shims would last until the AppDomain shut down.The easiest way to create a ShimsContext is by using the static Create() method as shown in the following code:

//unit test code
[Test]
public void Y2kCheckerTest() {
  using(ShimsContext.Create()) {
    ...
  } // clear all shims
}

It is critical to properly dispose each shim context.As a rule of thumb, always call the ShimsContext.Create inside of a using statement to ensure proper clearing of the registered shims.For example, you might register a shim for a test method that replaces the DateTime.Now method with a delegate that always returns the first of January 2000.If you forget to clear the registered shim in the test method, the rest of the test run would always return the first of January 2000 as the DateTime.Now value.This might be suprising and confusing.

使用填充编写测试

In your test code, insert a detour for the method you want to fake.例如:

[TestClass]
public class TestClass1
{ 
        [TestMethod]
        public void TestCurrentYear()
        {
            int fixedYear = 2000;

            using (ShimsContext.Create())
            {
              // Arrange:
                // Detour DateTime.Now to return a fixed date:
                System.Fakes.ShimDateTime.NowGet = 
                () =>
                { return new DateTime(fixedYear, 1, 1); };

                // Instantiate the component under test:
                var componentUnderTest = new MyComponent();

              // Act:
                int year = componentUnderTest.GetTheCurrentYear();

              // Assert: 
                // This will always be true if the component is working:
                Assert.AreEqual(fixedYear, year);
            }
        }
}
<TestClass()> _
Public Class TestClass1
    <TestMethod()> _
    Public Sub TestCurrentYear()
        Using s = Microsoft.QualityTools.Testing.Fakes.ShimsContext.Create()
            Dim fixedYear As Integer = 2000
            ' Arrange:
            ' Detour DateTime.Now to return a fixed date:
            System.Fakes.ShimDateTime.NowGet = _
                Function() As DateTime
                    Return New DateTime(fixedYear, 1, 1)
                End Function

            ' Instantiate the component under test:
            Dim componentUnderTest = New MyComponent()
            ' Act:
            Dim year As Integer = componentUnderTest.GetTheCurrentYear
            ' Assert: 
            ' This will always be true if the component is working:
            Assert.AreEqual(fixedYear, year)
        End Using
    End Sub
End Class

填充码类名称是通过在原始的类型名称前加上 Fakes.Shim 前缀构成的。

Shims work by inserting detours into the code of the application under test.Wherever a call to the original method occurs, the Fakes system performs a detour, so that instead of calling the real method, your shim code is called.

Notice that detours are created and deleted at run time.You must always create a detour within the life of a ShimsContext.When it is disposed, any shims you created while it was active are removed.The best way to do this is inside a using statement.

You might see a build error stating that the Fakes namespace does not exist.This error sometimes appears when there are other compilation errors.Fix the other errors and it will vanish.

用于不同类型方法的存根

Shim types allow you to replace any .NET method, including static methods or non-virtual methods, with your own delegates.

静态方法

The properties to attach shims to static methods are placed in a shim type.Each property has only a setter that can be used to attach a delegate to the target method.For example, given a class MyClass with a static method MyMethod:

//code under test
public static class MyClass {
    public static int MyMethod() {
        ...
    }
}

We can attach a shim to MyMethod that always returns 5:

// unit test code
ShimMyClass.MyMethod = () =>5;

实例方法 (对于所有实例)

Similarly to static methods, instance methods can be shimmed for all instances.The properties to attach those shims are placed in a nested type named AllInstances to avoid confusion.For example, given a class MyClass with an instance method MyMethod:

// code under test
public class MyClass {
    public int MyMethod() {
        ...
    }
}

You can attach a shim to MyMethod that always returns 5, regardless of the instance:

// unit test code
ShimMyClass.AllInstances.MyMethod = () => 5;

The generated type structure of ShimMyClass looks like the following code:

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
    public static class AllInstances {
        public static Func<MyClass, int>MyMethod {
            set {
                ...
            }
        }
    }
}

Notice that Fakes passes the runtime instance as the first argument of the delegate in this case.

实例方法 (对于一个运行时实例)

Instance methods can also be shimmed by different delegates, based on the receiver of the call.This enables the same instance method to have different behaviors per instance of the type.The properties to set up those shims are instance methods of the shim type itself.Each instantiated shim type is also associated with a raw instance of a shimmed type.

For example, given a class MyClass with an instance method MyMethod:

// code under test
public class MyClass {
    public int MyMethod() {
        ...
    }
}

We can set up two shim types of MyMethod such that the first one always returns 5 and the second always returns 10:

// unit test code
var myClass1 = new ShimMyClass()
{
    MyMethod = () => 5
};
var myClass2 = new ShimMyClass { MyMethod = () => 10 };

The generated type structure of ShimMyClass looks like the following code:

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
    public Func<int> MyMethod {
        set {
            ...
        }
    }
    public MyClass Instance {
        get {
            ...
        }
    }
}

The actual shimmed type instance can be accessed through the Instance property:

// unit test code
var shim = new ShimMyClass();
var instance = shim.Instance;

The shim type also has an implicit conversion to the shimmed type, so you can usually simply use the shim type as is:

// unit test code
var shim = new ShimMyClass();
MyClass instance = shim; // implicit cast retrieves the runtime
                         // instance

构造函数

Constructors can also be shimmed in order to attach shim types to future objects.Each constructor is exposed as a static method Constructor in the shim type.For example, given a class MyClass with a constructor taking an integer:

// code under test
public class MyClass {
    public MyClass(int value) {
        this.Value = value;
    }
    ...
}

We set up the shim type of the constructor so that every future instance returns -5 when the Value getter is invoked, regardless of the value in the constructor:

// unit test code
ShimMyClass.ConstructorInt32 = (@this, value) => {
    var shim = new ShimMyClass(@this) {
        ValueGet = () => -5
    };
};

Note that each shim type exposes two constructors.The default constructor should be used when a fresh instance is needed, while the constructor taking a shimmed instance as argument should be used in constructor shims only:

// unit test code
public ShimMyClass() { }
public ShimMyClass(MyClass instance) : base(instance) { }

The generated type structure of ShimMyClass resembles the followoing code:

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass>
{
    public static Action<MyClass, int> ConstructorInt32 {
        set {
            ...
        }
    }

    public ShimMyClass() { }
    public ShimMyClass(MyClass instance) : base(instance) { }
    ...
}

基成员

The shim properties of base members can be accessed by creating a shim for the base type and passing the child instance as a parameter to the constructor of the base shim class.

For example, given a class MyBase with an instance method MyMethod and a subtype MyChild:

public abstract class MyBase {
    public int MyMethod() {
        ...
    }
}

public class MyChild : MyBase {
}

We can set up a shim of MyBase by creating a new ShimMyBase shim:

// unit test code
var child = new ShimMyChild();
new ShimMyBase(child) { MyMethod = () => 5 };

Note that the child shim type is implicitly converted to the child instance when passed as a parameter to the base shim constructor.

The generated type structure of ShimMyChild and ShimMyBase resembles the following code:

// Fakes generated code
public class ShimMyChild : ShimBase<MyChild> {
    public ShimMyChild() { }
    public ShimMyChild(Child child)
        : base(child) { }
}
public class ShimMyBase : ShimBase<MyBase> {
    public ShimMyBase(Base target) { }
    public Func<int> MyMethod
    { set { ... } }
}

静态构造函数

Shim types expose a static method StaticConstructor to shim the static constructor of a type.Since static constructors are executed once only, you need to ensure that the shim is configured before any member of the type is accessed.

终结器

Finalizers are not supported in Fakes.

私有方法

The Fakes code generator will create shim properties for private methods that only have visible types in the signature, i.e. parameter types and return type visible.

Binding interfaces

When a shimmed type implements an interface, the code generator emits a method that allows it to bind all the members from that interface at once.

For example, given a class MyClass that implements IEnumerable<int>:

public class MyClass : IEnumerable<int> {
    public IEnumerator<int> GetEnumerator() {
        ...
    }
    ...
}

We can shim the implementations of IEnumerable<int> in MyClass by calling the Bind method:

// unit test code
var shimMyClass = new ShimMyClass();
shimMyClass.Bind(new List<int> { 1, 2, 3 });

The generated type structure of ShimMyClass resembles the following code:

// Fakes generated code
public class ShimMyClass : ShimBase<MyClass> {
    public ShimMyClass Bind(IEnumerable<int> target) {
        ...
    }
}

更改默认行为

Each generated shim type holds an instance of the IShimBehavior interface, through the ShimBase<T>.InstanceBehavior property.The behavior is used whenever a client calls an instance member that was not explicitly shimmed.

If the behavior has not been explicitly set, it will use the instance returned by the static ShimsBehaviors.Current property.By default, this property returns a behavior that throws a NotImplementedException exception.

This behavior can be changed at any time by setting the InstanceBehavior property on any shim instance.For example, the following snippet changes the shim to a behavior that does nothing or returns the default value of the return type—that is, default(T):

// unit test code
var shim = new ShimMyClass();
//return default(T) or do nothing
shim.InstanceBehavior = ShimsBehaviors.DefaultValue;

The behavior can also be changed globally for all shimmed instances for which the InstanceBehavior property was not explicitly set by setting the static ShimsBehaviors.Current property:

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

Detecting environment accesses

It is possible to attach a behavior to all the members, including static methods, of a particular type by assigning the ShimsBehaviors.NotImplemented behavior to the static property Behavior of the corresponding shim type:

// unit test code
// assigning the not implemented behavior
ShimMyClass.Behavior = ShimsBehaviors.NotImplemented;
// shorthand
ShimMyClass.BehaveAsNotImplemented();

并发

Shim types apply to all threads in the AppDomain and don’t have thread affinity.This is an important fact if you plan to use a test runner that support concurrency: tests involving shim types cannot run concurrently.This property is not enfored by the Fakes runtime.

Calling the original method from the shim method

Imagine that we wanted to actually write the text to the file system after validating the file name passed to the method.In that case, we would want to call the original method in the middle of the shim method.

The first approach to solve this problem is to wrap a call to the original method using a delegate and ShimsContext.ExecuteWithoutShims() as in the following code:

// unit test code
ShimFile.WriteAllTextStringString = (fileName, content) => {
  ShimsContext.ExecuteWithoutShims(() => {

      Console.WriteLine("enter");
      File.WriteAllText(fileName, content);
      Console.WriteLine("leave");
  });
};

Another approach is to set the shim to null, call the original method and restore the shim.

// unit test code
ShimsDelegates.Action<string, string> shim = null;
shim = (fileName, content) => {
  try {
    Console.WriteLine("enter”);
    // remove shim in order to call original method
    ShimFile.WriteAllTextStringString = null;
    File.WriteAllText(fileName, content);
  }
  finally
  {
    // restore shim
    ShimFile.WriteAllTextStringString = shim;
    Console.WriteLine("leave");
  }
};
// initialize the shim
ShimFile.WriteAllTextStringString = shim;

限制

Shims cannot be used on all types from the .NET base class library mscorlib and System.

外部资源

指导

使用 Visual Studio 2012 对连续交付进行测试 - 第 2 章:单元测试:测试内部

请参见

概念

用 Microsoft Fakes 隔离测试代码

其他资源

Peter Provost’s blog: Visual Studio 2012 Shims

Video (1h16): Testing Un-testable Code with Fakes in Visual Studio 2012