Microsoft Fakes

Istnieje wiele framework’ow ułatwiających izolację danych w testach jednostkowych. Nie opisuje ich na blogu, bo nie wiele od siebie różnią się . Microsoft Fakes jednak ma kilka ciekawych rzeczy i dlatego nim dzisiaj zajmiemy się.

Niestety jest dostępny on wyłącznie w wersji Visual Studio Ultimate. Pierwszą wyróżniającą go cechą jest możliwość izolacji metod statycznych, które oczywiście nie mogą być w łatwy sposób mock’owane.  Rozważmy klasyczny przykład:

public class Person
{
    public void Method()
    {
        if (DateTime.Now == new DateTime(2000, 1, 1))
        {
            // jakas logika
        } 
    }
}

Jak przetestować powyższy warunek? W jaki sposób, zasymulować, że DateTime.Now zwróci rok 2000? Przede wszystkim, ktoś może powiedzieć, że należy unikać statycznych wywołać i wszystko powinno być wstrzykiwane przez konstruktor. Taka osoba będzie miała oczywiście rację. Niestety w praktyce trzeba pracować również z legacy code i nie zawsze mamy wszystko zaprojektowane tak jak powinno to być.

Microsoft Fakes implementuje tzw. shims. W odróżnieniu od stub’ow oraz mock’ow, umożliwiają one zastąpienie dowolnej statycznej metody innym wywołaniem. Stub z kolei, jak dobrze wiemy, polega na implementacji po prostu odpowiedniego interfejsu lub klasy. Mock to rozszerzony stub o możliwość śledzenia wywołań.

Najlepiej to pokazać na przykładzie. Z kontekstowego menu wybieramy “Add Fake Assembly”:

image

Chcemy stworzyć shim dla DateTime, który znajduje się w System – dlatego, też najpierw zaznaczyliśmy tą bibliotekę. Po chwili zostaną wygenerowane specjalne biblioteki, zawierające szereg stub’ow, mock’ow i shim’ow. To jest kolejna różnica między Microsoft Fakes a innymi frameworkami – wszystkie typy są po prostu generowane jako zwykłe klasy.

Konfiguracja shim’a wygląda następująco:

using (ShimsContext.Create())
{
    ShimDateTime.NowGet = () => new DateTime(2000, 1, 1);

    var person = new Person();
    person.Method();
}

Nie ma w tym nic trudnego – zwykła lambda. Wygenerowane shim’y posiadają pewną konwencję w nazwach. Klasa jest poprzedzona słowem Shim (DateTime->ShimDateTime), z kolei właściwość kończy się Set albo Get (DateTime.Now->ShimDateTime.NowGet).

Ponadto, wywołanie musi być opatrzone w ShimContext bo skonfigurowana lambda może być tylko wykonywana w danym kontekście.

Po uruchomieniu kodu,  przekonamy się, że warunek zostanie spełniony ponieważ DateTime.Now zwróci teraz rok 2000. Analogicznie możemy tworzyć shim’y dla dowolnych typów – wystarczy kliknąć Add Fake Assembly dla danej biblioteki.

Cały wygenerowany kod można z łatwością zobaczyć w VS. Na przykład ShimDateTime wygląda następująco:

// Type: System.Fakes.ShimDateTime
// Assembly: mscorlib.4.0.0.0.Fakes, Version=4.0.0.0, Culture=neutral, PublicKeyToken=0ae41878053f6703
// MVID: 6BEF2261-203B-4A2D-AABA-BC00EF51BEBA
// Assembly location: C:\Users\Piotr\documents\visual studio 2013\Projects\ConsoleApplication11\ClassLibrary1\FakesAssemblies\mscorlib.4.0.0.0.Fakes.dll

using Microsoft.QualityTools.Testing.Fakes;
using Microsoft.QualityTools.Testing.Fakes.Shims;
using System;
using System.Diagnostics;
using System.Globalization;

namespace System.Fakes
{
  /// <summary>
  /// Shim type of System.DateTime
  /// </summary>
  [ShimClass(typeof (DateTime))]
  [DebuggerDisplay("Shim of DateTime")]
  [DebuggerNonUserCode]
  public sealed class ShimDateTime : ShimBase
  {
    /// <summary>
    /// Assigns the 'Current' behavior for all methods of the shimmed type
    /// </summary>
    public static void BehaveAsCurrent();
    /// <summary>
    /// Assigns the 'NotImplemented' behavior for all methods of the shimmed type
    /// </summary>
    public static void BehaveAsNotImplemented();
    /// <summary>
    /// Sets the shim of DateTime.op_Addition(DateTime d, TimeSpan t)
    /// </summary>
    public static FakesDelegates.Func<DateTime, TimeSpan, DateTime> AdditionOpDateTimeTimeSpan { [ShimMethod("op_Addition", 24)] set; }
    /// <summary>
    /// Assigns the behavior for all methods of the shimmed type
    /// </summary>
    public static IShimBehavior Behavior { set; }
    /// <summary>
    /// Sets the shim of DateTime.Compare(DateTime t1, DateTime t2)
    /// </summary>
    public static FakesDelegates.Func<DateTime, DateTime, int> CompareDateTimeDateTime { [ShimMethod("Compare", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.DateToTicks(Int32 year, Int32 month, Int32 day)
    /// </summary>
    public static FakesDelegates.Func<int, int, int, long> DateToTicksInt32Int32Int32 { [ShimMethod("DateToTicks", 40)] set; }
    /// <summary>
    /// Sets the shim of DateTime.DaysInMonth(Int32 year, Int32 month)
    /// </summary>
    public static FakesDelegates.Func<int, int, int> DaysInMonthInt32Int32 { [ShimMethod("DaysInMonth", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.DoubleDateToTicks(Double value)
    /// </summary>
    public static FakesDelegates.Func<double, long> DoubleDateToTicksDouble { [ShimMethod("DoubleDateToTicks", 40)] set; }
    /// <summary>
    /// Sets the shim of DateTime.op_Equality(DateTime d1, DateTime d2)
    /// </summary>
    public static FakesDelegates.Func<DateTime, DateTime, bool> EqualityOpDateTimeDateTime { [ShimMethod("op_Equality", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.Equals(DateTime t1, DateTime t2)
    /// </summary>
    public static FakesDelegates.Func<DateTime, DateTime, bool> EqualsDateTimeDateTime { [ShimMethod("Equals", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.FromBinary(Int64 dateData)
    /// </summary>
    public static FakesDelegates.Func<long, DateTime> FromBinaryInt64 { [ShimMethod("FromBinary", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.FromBinaryRaw(Int64 dateData)
    /// </summary>
    public static FakesDelegates.Func<long, DateTime> FromBinaryRawInt64 { [ShimMethod("FromBinaryRaw", 40)] set; }
    /// <summary>
    /// Sets the shim of DateTime.FromFileTime(Int64 fileTime)
    /// </summary>
    public static FakesDelegates.Func<long, DateTime> FromFileTimeInt64 { [ShimMethod("FromFileTime", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.FromFileTimeUtc(Int64 fileTime)
    /// </summary>
    public static FakesDelegates.Func<long, DateTime> FromFileTimeUtcInt64 { [ShimMethod("FromFileTimeUtc", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.FromOADate(Double d)
    /// </summary>
    public static FakesDelegates.Func<double, DateTime> FromOADateDouble { [ShimMethod("FromOADate", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.op_GreaterThan(DateTime t1, DateTime t2)
    /// </summary>
    public static FakesDelegates.Func<DateTime, DateTime, bool> GreaterThanOpDateTimeDateTime { [ShimMethod("op_GreaterThan", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.op_GreaterThanOrEqual(DateTime t1, DateTime t2)
    /// </summary>
    public static FakesDelegates.Func<DateTime, DateTime, bool> GreaterThanOrEqualOpDateTimeDateTime { [ShimMethod("op_GreaterThanOrEqual", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.op_Inequality(DateTime d1, DateTime d2)
    /// </summary>
    public static FakesDelegates.Func<DateTime, DateTime, bool> InequalityOpDateTimeDateTime { [ShimMethod("op_Inequality", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.IsLeapYear(Int32 year)
    /// </summary>
    public static FakesDelegates.Func<int, bool> IsLeapYearInt32 { [ShimMethod("IsLeapYear", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.op_LessThan(DateTime t1, DateTime t2)
    /// </summary>
    public static FakesDelegates.Func<DateTime, DateTime, bool> LessThanOpDateTimeDateTime { [ShimMethod("op_LessThan", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.op_LessThanOrEqual(DateTime t1, DateTime t2)
    /// </summary>
    public static FakesDelegates.Func<DateTime, DateTime, bool> LessThanOrEqualOpDateTimeDateTime { [ShimMethod("op_LessThanOrEqual", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.get_Now()
    /// </summary>
    public static FakesDelegates.Func<DateTime> NowGet { [ShimMethod("get_Now", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.ParseExact(String s, String[] formats, IFormatProvider provider, DateTimeStyles style)
    /// </summary>
    public static FakesDelegates.Func<string, string[], IFormatProvider, DateTimeStyles, DateTime> ParseExactStringStringArrayIFormatProviderDateTimeStyles { [ShimMethod("ParseExact", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.ParseExact(String s, String format, IFormatProvider provider)
    /// </summary>
    public static FakesDelegates.Func<string, string, IFormatProvider, DateTime> ParseExactStringStringIFormatProvider { [ShimMethod("ParseExact", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.ParseExact(String s, String format, IFormatProvider provider, DateTimeStyles style)
    /// </summary>
    public static FakesDelegates.Func<string, string, IFormatProvider, DateTimeStyles, DateTime> ParseExactStringStringIFormatProviderDateTimeStyles { [ShimMethod("ParseExact", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.Parse(String s)
    /// </summary>
    public static FakesDelegates.Func<string, DateTime> ParseString { [ShimMethod("Parse", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.Parse(String s, IFormatProvider provider)
    /// </summary>
    public static FakesDelegates.Func<string, IFormatProvider, DateTime> ParseStringIFormatProvider { [ShimMethod("Parse", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.Parse(String s, IFormatProvider provider, DateTimeStyles styles)
    /// </summary>
    public static FakesDelegates.Func<string, IFormatProvider, DateTimeStyles, DateTime> ParseStringIFormatProviderDateTimeStyles { [ShimMethod("Parse", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.SpecifyKind(DateTime value, DateTimeKind kind)
    /// </summary>
    public static FakesDelegates.Func<DateTime, DateTimeKind, DateTime> SpecifyKindDateTimeDateTimeKind { [ShimMethod("SpecifyKind", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.DateTime()
    /// </summary>
    public static FakesDelegates.Action StaticConstructor { [ShimMethod(".cctor", 40)] set; }
    /// <summary>
    /// Sets the shim of DateTime.op_Subtraction(DateTime d1, DateTime d2)
    /// </summary>
    public static FakesDelegates.Func<DateTime, DateTime, TimeSpan> SubtractionOpDateTimeDateTime { [ShimMethod("op_Subtraction", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.op_Subtraction(DateTime d, TimeSpan t)
    /// </summary>
    public static FakesDelegates.Func<DateTime, TimeSpan, DateTime> SubtractionOpDateTimeTimeSpan { [ShimMethod("op_Subtraction", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.TicksToOADate(Int64 value)
    /// </summary>
    public static FakesDelegates.Func<long, double> TicksToOADateInt64 { [ShimMethod("TicksToOADate", 40)] set; }
    /// <summary>
    /// Sets the shim of DateTime.TimeToTicks(Int32 hour, Int32 minute, Int32 second)
    /// </summary>
    public static FakesDelegates.Func<int, int, int, long> TimeToTicksInt32Int32Int32 { [ShimMethod("TimeToTicks", 40)] set; }
    /// <summary>
    /// Sets the shim of DateTime.get_Today()
    /// </summary>
    public static FakesDelegates.Func<DateTime> TodayGet { [ShimMethod("get_Today", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.TryCreate(Int32 year, Int32 month, Int32 day, Int32 hour, Int32 minute, Int32 second, Int32 millisecond, DateTime&amp; result)
    /// </summary>
    public static FakesDelegates.OutFunc<int, int, int, int, int, int, int, DateTime, bool> TryCreateInt32Int32Int32Int32Int32Int32Int32DateTimeOut { [ShimMethod("TryCreate", 40)] set; }
    /// <summary>
    /// Sets the shim of DateTime.TryParseExact(String s, String[] formats, IFormatProvider provider, DateTimeStyles style, DateTime&amp; result)
    /// </summary>
    public static FakesDelegates.OutFunc<string, string[], IFormatProvider, DateTimeStyles, DateTime, bool> TryParseExactStringStringArrayIFormatProviderDateTimeStylesDateTimeOut { [ShimMethod("TryParseExact", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.TryParseExact(String s, String format, IFormatProvider provider, DateTimeStyles style, DateTime&amp; result)
    /// </summary>
    public static FakesDelegates.OutFunc<string, string, IFormatProvider, DateTimeStyles, DateTime, bool> TryParseExactStringStringIFormatProviderDateTimeStylesDateTimeOut { [ShimMethod("TryParseExact", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.TryParse(String s, DateTime&amp; result)
    /// </summary>
    public static FakesDelegates.OutFunc<string, DateTime, bool> TryParseStringDateTimeOut { [ShimMethod("TryParse", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.TryParse(String s, IFormatProvider provider, DateTimeStyles styles, DateTime&amp; result)
    /// </summary>
    public static FakesDelegates.OutFunc<string, IFormatProvider, DateTimeStyles, DateTime, bool> TryParseStringIFormatProviderDateTimeStylesDateTimeOut { [ShimMethod("TryParse", 24)] set; }
    /// <summary>
    /// Sets the shim of DateTime.get_UtcNow()
    /// </summary>
    public static FakesDelegates.Func<DateTime> UtcNowGet { [ShimMethod("get_UtcNow", 24)] set; }
    /// <summary>
    /// Define shims for all instances members
    /// </summary>
    public static class AllInstances
    {
    }
  }
}

Microsoft Fakes to również stub’y.  Załóżmy, że mamy następujący interfejs:

public interface IPersonRepository
{
   void AddPerson(Person person);
   Person[] GetAll();
}

Następnie chcemy stworzyć stub, który dostarcza jakąś prostą implementacje powyższych metod. Najpierw klikamy “Add Fake Assembly” na referencji biblioteki, która zawiera powyższy interfejs. Zgodnie z konwencją, zostanie wygenerowana klasa StubIPersonRepository:

  [StubClass(X)]
  [DebuggerDisplay("Stub of IPersonRepository")]
  [DebuggerNonUserCode]
  public class StubIPersonRepository : StubBase<ConsoleApplication11.IPersonRepository>, ConsoleApplication11.IPersonRepository
  {
    /// <summary>
    /// Sets the stub of IPersonRepository.AddPerson(Person person)
    /// </summary>
    public FakesDelegates.Action<ConsoleApplication11.Person> AddPersonPerson;
    /// <summary>
    /// Sets the stub of IPersonRepository.GetAll()
    /// </summary>
    public FakesDelegates.Func<ConsoleApplication11.Person[]> GetAll;
  }

Jak widzimy, Microsoft Fakes oparty jest na delegatach. Jest to trochę inny model niż Moq, który bazował na proxy. W Microsoft Fakes, generowane są realne klasy, a potem za pomocą np. lambdy, możemy określić zachowanie poszczególnych metod:

var stub=new StubIPersonRepository();
var inMemoryRepository=new List<ConsoleApplication11.Person>();

stub.GetAll = () => inMemoryRepository.ToArray();
stub.AddPersonPerson = (person) => inMemoryRepository.Add(person);

Następnie możemy korzystać z powyższego stub’a, jak z normalnej klasy:

IPersonRepository personRepository = stub;
personRepository.AddPerson(new Person());

Framework implementuje również wzorzec obserwator, co w praktyce oznacza, że możemy korzystać również z mock’ow a nie wyłacznie z stub i dummy. Przykład:

var stub=new StubIPersonRepository();
var stubObserver = new StubObserver();

stub.InstanceObserver = stubObserver;

IPersonRepository personRepository = stub;
personRepository.AddPerson(new Person());
Person[] all = personRepository.GetAll();

StubObservedCall[] allCalls=stubObserver.GetCalls();

foreach (StubObservedCall call in allCalls)
{
 Console.WriteLine(call.StubbedMethod);
 Console.WriteLine(call.StubbedType);

 object[] arguments = call.GetArguments();

 foreach (object argument in arguments)
 {
     Console.WriteLine(argument);
 }
}

Niestety nie jest to zbyt proste w użyciu, dlatego na Microsoft Fakes należy patrzeć jako framework dostarczający głównie shim’y i stub’y a do mock’oq lepiej wykorzystać dobrze znany Moq. Na zakończenie warto zobaczyć wspomniany interfejs obserwatora:

[DebuggerNonUserCode]
public sealed class StubObserver : IStubObserver
{
    public void Clear();
    public StubObservedCall[] GetCalls();
    void IStubObserver.Enter(Type stubbedType, Delegate stubCall);
    void IStubObserver.Enter(Type stubbedType, Delegate stubCall, object arg1);
    void IStubObserver.Enter(Type stubbedType, Delegate stubCall, object arg1, object arg2);
    void IStubObserver.Enter(Type stubbedType, Delegate stubCall, object arg1, object arg2, object arg3);
    void IStubObserver.Enter(Type stubbedType, Delegate stubCall, params object[] args);
}

One thought on “Microsoft Fakes”

Leave a Reply

Your email address will not be published.