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”:
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& 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& 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& 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& 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& 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); }
2 thoughts on “Microsoft Fakes”