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);
}

Własna implementacja await’era

Wiele razy pisałem już o słowach async\await i z pewnością ułatwiają one życie. Dla przypomnienia rozważmy kod:

private async void DownloadAndSortAsync()
{
      int[] allNumbers = await DownloadNumbersAsync();
      int[] sortedNumbers = await SortNumbersAsync(allNumbers);            
      MessageBox.Show(string.Join(“,”,sortedNumbers));
}
private Task<int[]> DownloadNumbersAsync()
{
      return Task<int[]>.Factory.StartNew(() => DownloadNumbers());
}
 private Task<int[]> SortNumbersAsync(int[] numbers)
{
      return Task<int[]>.Factory.StartNew(() => SortNumbers(numbers));
}

Ciekawą możliwością jest implementacja własnego await’er,a który nie musi być związany z zadaniami (Task). Jako przykład, pokażę zaproponowany przez J.Richter awaiter dla zdarzeń. Załóżmy, że chcemy czekać na zdarzenie aż zostanie wysłane, ale nie chcemy jednocześnie blokować wątku.

Stwórzmy najpierw klasy, na których będziemy testować rozwiązanie:

public class MessageEventArgs : EventArgs
{
   public MessageEventArgs(string message)
   {
       Message = message;
   }
   public string Message { get; private set; }
}

public class MessageController
{
   public event EventHandler<MessageEventArgs> MessageReceived;

   public void SendMessage(string message)
   {
       if(MessageReceived!=null)
           MessageReceived(this,new MessageEventArgs(message));
   }
}

Prosty kontroler eksponujący zdarzenie. Kolejnym celem jest czekanie aż zdarzenie zostanie odebrane.  Innymi słowy, chcemy odbierać wiadomości w pętli i wykonać jakieś zadanie tzn.:

while(true)
{
    MessageEventArgs args = await _controller.MessageReceived;    
    Console.WriteLIne(args.Message);
}

Oczywiście powyższy kod nie zadziała ponieważ zdarzenie c# nie wspiera słowa kluczowego await.  Musimy najpierw zaimplementować interfejs INotifyCompletion:

/// <summary>
/// Represents an operation that schedules continuations when it completes.
/// </summary>
public interface INotifyCompletion
{
    /// <summary>
    /// Schedules the continuation action that's invoked when the instance completes.
    /// </summary>
    /// <param name="continuation">The action to invoke when the operation completes.</param><exception cref="T:System.ArgumentNullException">The <paramref name="continuation"/> argument is null (Nothing in Visual Basic).</exception>
    void OnCompleted(Action continuation);
}

Do zaimplementowania mamy tylko jedną metodę z interfejsu. OnCompleted przekaże nam tzw. continuationPoint. Po odebraniu zdarzenia właśnie tą delegatę będziemy wywoływać, aby powiadomić maszynę stanów await o zakończeniu operacji.

Najprostsza implementacja await’era wygląda następująco:

public class MessageEventAwaiter : INotifyCompletion
{
   private Action _continuationPoint;
   private MessageEventArgs _result;

   public void OnCompleted(Action continuation)
   {
       _continuationPoint = continuation;
   }

   public void EventHandler(object sender, MessageEventArgs messageEventArgs)
   {
       _result = messageEventArgs;
       if (_continuationPoint != null)
           _continuationPoint();
   }

   public MessageEventAwaiter GetAwaiter()
   {
       return this;
   }

   public MessageEventArgs GetResult()
   {
       return _result;
   }

   public bool IsCompleted
   {
       get { return _result != null; }
   }
}

W metodzie OnCompleted po prostu buforujemy continuationPoint, który zostanie przekazany nam przez state machine:

public void OnCompleted(Action continuation)
{
  _continuationPoint = continuation;
}

EventHandler to metoda, którą przekażemy zdarzeniu. To właśnie ona zostanie najpierw wywołana przez zdarzenie:

public void EventHandler(object sender, MessageEventArgs messageEventArgs)
{
  _result = messageEventArgs;
  if (_continuationPoint != null)
      _continuationPoint();
}

Innymi słowy, zdarzenie wywołuje metodę EventHandler, a ona z kolei powiadomi state machine za pomocą _continuationPoint.

Następnie mamy trzy metody, które są wymagane dla wsparcia słowa kluczowego await. Bez nich, kod po prostu by się nie skompilował:

public MessageEventAwaiter GetAwaiter()
{
  return this;
}

public MessageEventArgs GetResult()
{
  return _result;
}

public bool IsCompleted
{
  get { return _result != null; }
}

Nie są one zbyt skomplikowane, po prostu sprawdzają czy zadanie zostało już wykonane itp.

Tak naprawdę powyższy kod nie jeszcze idealny, ale spróbujmy go przetestować za pomocą:

var messageController = new MessageController();
var messageAwaiter=new MessageEventAwaiter();

Task.Factory.StartNew(() =>
{
 int i = 0;
 while (true)
 {
     Thread.Sleep(2000);
     messageController.SendMessage(i.ToString(CultureInfo.InvariantCulture));
     i++;
 }
});

messageController.MessageReceived+=messageAwaiter.EventHandler;

while (true)
{
 MessageEventArgs result = await messageAwaiter;
 Console.WriteLine(result.Message);
}

Najpierw tworzymy wątek, który co dwie sekundy wysyła zdarzenie, a potem w pętli odbieramy je za pomocą słowa kluczowego await. Gdybyśmy teraz uruchomili aplikację na ekranie zobaczylibyśmy wyłącznie pierwsze zdarzenie:

image

Dlaczego? Widzimy, że zadanie jest uznane za ukończone jak tylko _result jest ustawiony na jakaś wartość. Ma to miejsce już po pierwszym zdarzeniu.

Z tego wniosek, że musimy kolejkować zdarzenia:

public class MessageEventAwaiter : INotifyCompletion
{
   private Action _continuationPoint;
   private Queue<MessageEventArgs> _results = new Queue<MessageEventArgs>();

   public void OnCompleted(Action continuation)
   {
       _continuationPoint = continuation;
   }

   public void EventHandler(object sender, MessageEventArgs messageEventArgs)
   {
       _results.Enqueue(messageEventArgs);
       if (_continuationPoint != null)
           _continuationPoint();
   }

   public MessageEventAwaiter GetAwaiter()
   {
       return this;
   }

   public MessageEventArgs GetResult()
   {
       return _results.Dequeue();
   }

   public bool IsCompleted
   {
       get { return _results.Count > 0; }
   }
}

Teraz, zamiast przechowywać wyłącznie jedną wartość, mamy kolekcję danych. IsCompleted zwróci true, gdy cała kolejka będzie pusta. Na ekranie, po uruchomieniu, zobaczymy prawidłowe dane:

image

Kod wciąż nie jest idealny. W praktyce lepiej napisać generyczny awaiter, a nie taki, który obsługuje wyłącznie jeden typ zdarzenia. Kolejne możliwe ulepszenie to napisanie kodu thread-safe – powyższy przykład nie będzie działał w środowisku wielowątkowym, co zwykle ma miejsce w przypadku await. Tak czy inaczej, nie są to trudne zmiany do naniesienia, a myślę, że łatwiej było wytłumaczyć idee na przykładzie prostego przykładu a nie produkcyjnego kodu.

Debugowanie obiektów bez jawnej referencji

W Visual Studio istnieje pewna opcja, która umożliwia sprawdzenie wartości obiektu, do którego nie ma się jawnej referencji. Spójrzmy na następujący przykład:

class Program
{
   static void Main(string[] args)
   {        
       DoSomething();
   }

   private static void DoSomething()
   {
       var person=new Person();
       person.FirstName = "Piotr";
       person.LastName = "Zielinski";
   }
}

Jeśli ustawimy breakpoint w metodzie DoSomething naturalnie zobaczymy wartość obiektu person w oknie watch:

image

Jeśli jednak przejdziemy dalej, do funkcji main wtedy zmienna person będzie poza scope i watch nic nie pokaże:

image

Jest to naturalne zachowanie, ponieważ nie ma jawnej referencji do Person. Czasami jednak, mimo wszystko chcemy śledzić taki obiekt. Oczywiście w celu wykrycia memory leak służą profilery, ale czasami dostęp do referencji może być utrudniony a mimo to, chcemy mieć łatwy do niego dostęp. Łatwo wyobrazić sobie przykład z jakimś własnym kontenerem, gdzie nie ma publicznego dostępu do instancji. W VS można z menu podręcznego wybrać “Make Object ID”. Należy zatem kliknąć na zmiennej Person w watch i wybrać wspomnianą opcję:

image

Koło zmiennej wtedy pojawi się identyfikator w formie 1# :

image

Jest to tzw. słaba referencja. Możemy mieć do niej dostęp nawet w funkcji Main:

image

Wystarczy wpisać ją, jak każdą inną zmienną w oknie watch. Oczywiście, gdy GC zwolni zasoby, wtedy pojawi się NULL.  Obiekt wciąż jest widziany jako nieosiągalny i nie ma to wpływu na działanie GC.

SpecsFor MVC

W ostatnim poście było o SpecsFor, jako alternatywy dla SpecsFlow. Pokazane przykłady były ogólne i nie dotyczyły konkretnej technologii. Z BDD bardzo często korzysta się w celu przetestowania UI. W końcu wpisywane scenariusze, odzwierciedlają typową interakcję użytkownika z aplikacją. Osoby nietechniczne zwykle definiują wymagania z punktu widzenia użytkownika a nie wewnętrznej infrastruktury, która oczywiście nie jest im znana w szczegółach.

Dla aplikacji ASP.NET MVC powstał SpecsFor MVC. Dobra wiadomość jest taka, że sposób korzystania z niego jest analogiczny do klasycznego SpecsFor. Stanowi on jednak testy UI, wiec zamieni standardowe unit testy w wywołania wykonywane przez przeglądarkę (tak samo jak WatIn). W przeciwieństwie do klasycznych testów UI, nie ma tam fazy nagrywania. Zamiast tego, programiści piszą specyfikacje i kod (tak jak w normalnym SpecsFor).

Zaczynajmy od instalacji:

image

Następnie możemy przejść do definiowania testów. Przyznam, że na początku miałem kilka problemów związanych z tym. Przede wszystkim koniecznie należy stworzyć osobną bibliotekę dla testów, (co i tak w środowisku produkcyjnym jest czymś normalnym).

Kolejny etap to konfiguracja środowiska uruchomieniowego. SpecsFlow jak wspomniałem wcześniej, przetłumaczy testy jednostkowe do zwykłych wywołań w przeglądarce. Z tego względu, niezbędny będzie hosting aplikacji w IIS. Testy są wykonywane nie na poziomie kontrolerów, ale całych zapytań HTTP. Z tego względu należy stworzyć następująca klasę:

[SetUpFixture]
public class AssemblyStartup
{
   private SpecsForIntegrationHost _host;

   [SetUp]
   public void SetupTestRun()
   {
       var config = new SpecsForMvcConfig();
       config.UseIISExpress()
             .With(Project.Named("MvcApplication7"))
             .CleanupPublishedFiles()
             .ApplyWebConfigTransformForConfig("Debug");

       config.BuildRoutesUsing(RouteConfig.RegisterRoutes);

       config.UseBrowser(BrowserDriver.InternetExplorer);
       config.InterceptEmailMessagesOnPort(13565);

       _host = new SpecsForIntegrationHost(config);
       _host.Start();
   }

   [TearDown]
   public void TearDownTestRun()
   {
       _host.Shutdown();
   }
}

Kod po prostu uruchamia IIS express, hostując daną aplikację. W tym przypadku jest to MvcApplication7. SpecsFor opublikuje stronę, zgodnie z konfiguracją Debug. Ponadto, w testach będzie wykorzystywana przeglądarka Internet Explorer. Nic nie stoi na przeszkodzie, aby to zmienić ją i skorzystać z jakieś innej – wystarczy spojrzeć na BrowserDriver. Atrybut SetUpFixture oznacza, że klasa zostanie wywołana przed jakimikolwiek testami jednostkowymi, czyli dokładnie to co, czego oczekujemy.

Załóżmy, ze chcemy napisać test, który sprawdzi czy użytkownik po zarejestrowaniu konta, zostanie przekierowany do strony głównej. Innymi słowy:

Given: Użytkownik wszedł na stronę do rejestracji

When: Użytkownik wypełnił dane i nacisnął przycisk rejestruj

Then: Użytkownik został przekierowany do strony głównej.

public class UserRegistrationSpecs
{
   public class WhenCredentialsAreValid : SpecsFor<MvcWebApp>
   {
       protected override void Given()
       {
           SUT.NavigateTo<AccountController>(c => c.Register());
       }

       protected override void When()
       {
           SUT.FindFormFor<RegisterModel>()
               .Field(m => m.UserName).SetValueTo("Piotr")
               .Field(m => m.Password).SetValueTo("Test@1")
               .Field(m => m.ConfirmPassword).SetValueTo("Test@1")
               .Submit();
       }

       [Test]
       public void ThenUserShouldBeRedirectedToHomePage()
       {
           SUT.Route.ShouldMapTo<HomeController>(c => c.Index());
       }            
   }
}

NavigateTo śluzy oczywiście do przekierowań na podstawie routing. Proszę zauważyć, że nie korzystamy tutaj z adresów HTTP a zwykłych kontrolerów, z którymi programista ASP.NET MVC jest bardzo dobrze obeznany.

Ciekawszą metodą jest When. Przykład pokazuje jak łatwo można edytować pola w formularzu. Kod wpisze nazwę użytkownika, hasło, a następnie zasymuluje naciśniecie przycisku Submit. Wszystko to za pomocą silnie typowanego kodu, a nie identyfikacji za pomocą tagów HTML.

W klauzuli Then, sprawdzamy po prostu czy aktualna strona to ta obsługiwana przez kontroler HomeController oraz akcję Index. Po uruchomieniu testów, najpierw pojawi się aplikacja konsolowa, która odpala serwer i dokonuje publikacji strony:

image

Po chwili z kolei sterownik uruchomi daną przeglądarkę i wykona operacje zdefiniowane w specyfikacji, czyli wpisze dane uwierzytelniające i zarejestruje użytkownika:

image

W klasie konfigurującej (AssemblyStartup) z pewnością dostrzegliście następująca linię kodu:

config.InterceptEmailMessagesOnPort(13565);

SpecsFor ma wsparcie dla obsługi mailow i w testach można sprawdzać czy dany email został wysłany. Na przykład, jeśli rejestracja wysyła email to można sprawdzić czy faktycznie tak stało się:

SUT.Mailbox().MailMessages.Count().ShouldEqual(1);

Mamy dostęp nawet do informacji o konkretnym emailu, tzn.:

SUT.Mailbox().MailMessages[0].To[0].Address.ShouldEqual("to@pzielinski.com");
SUT.Mailbox().MailMessages[0].From.Address.ShouldEqual("from@pzielinski.com");

ASP.NET MVC ma wsparcie dla walidacji formularzy i tak samo w SpecsFor można sprawdzić czy wskazane pole posiada prawidłowe dane. Naturalne jest, że trzeba napisać kilka testów negatywnych, czyli sprawdzić, co się stanie, jak nazwa użytkownika nie została podana w prawidłowym formacie.:

SUT.FindFormFor<RegisterModel>().Field(m => m.UserName).ShouldBeInvalid();
SUT.FindFormFor<RegisterModel>().Field(m => m.Email).ShouldBeInvalid();

Większość formularzy ma również ValidationSummary ze szczegółami błędu. Bardziej zaawansowane testy mogą również weryfikować czy podany komunikat błędu jest prawidłowy:

SUT.ValidationSummary.Text.ShouldContain("The user name or password provided is incorrect.");

Podobnie jak wartości poł edycyjnych, można sprawdzić czy etykieta ma prawidłową wartość:

SUT.FindDisplayFor<AboutViewModel>().DisplayFor(m => m.User.UserName).Text.ShouldEqual("text");

Powyższa zmienna została wyświetlona w widoku za pomocą:

@Html.DisplayFor(m => m.BusinessDays[i])

Istnieje oczywiście dużo więcej helper’ow umożliwiających weryfikację treści strony. API jest łatwe i nie ma sensu tutaj opisywać wszystkich możliwości. Bardzo podoba mi się SpecsFor i preferuje operować na kontrolerach niż na czystym URL jak to miało miejsce w WatIn. Dużo łatwiej wykorzystywać istniejące już testy jednostkowe, ponieważ infrastruktura nie zmienia się. Jedyna niedogodność to problemy jakie miałem podczas konfiguracji i publikacji aplikacji. Wynikały one głownie z moich lokalnych ustawień IIS, ale mimo wszystko, wyrzucane wyjątki nic nie mówiły o realnym problemie a kończyło to się po prostu komunikatem „Build Failed”.

SpecsFor–kolejny framework do BDD

Przez kilka ostatnich wpisów poruszałem temat BDD, a konkretniej jednego z framework’ow – SpecFlow. Dzisiaj o kolejnym rozwiązaniu, które jest przydatne, gdy programiści definiują specyfikacje. SpecsFor można zainstalować standardowo z NuGet:

image

Załóżmy, że będziemy rozpatrywać następujący kod:

public interface IAuthorization
{
   bool Authorize(string login, string password);
}
public class OrderController
{
   private readonly IAuthorization _authorization;
   private List<string>  _orders=new List<string>();

   public List<string> Orders
   {
       get { return _orders; }
   }

   public OrderController(IAuthorization authorization)
   {
       _authorization = authorization;
   }    

   public void LogIn(string login,string password)
   {
       if(!_authorization.Authorize(login, password))
           throw new AuthorizationException();
   }
   public void SubmitOrder(List<string> items)
   {
       _orders=new List<string>(items);
   }     
}

Wiem, że przykład nie ma sensu, ale chodzi mi po prostu o kilka metod + IoC. SpecsFor to również BDD, wiec bardzo przypomina w użyciu SpecFlow. Różnica polega na tym, kto definiuje specyfikacje. W SpecFlow, specyfikacja ma formę zdań w naturalnym języku. Z kolei w SpecsFor, definiujemy je przez nazwy metod. Jeśli zależy nam na dobrej komunikacji miedzy developerami a osobami nietechnicznymi, wtedy SpecFlow moim zdaniem jest dużo ulepszy. W przypadku jakiś zadań niskopoziomowych, narzędzi dla programistów itp. SpecsFor może okazać się łatwiejszy w użyciu.

Jak wspomniałem, w poprzednim poście, SpecsFor składa się m.in z nUnit, Moq, expectedObjects, Should Library więc nie musimy nic dodatkowo instalować. Wszystko jest wbudowane już w SpecsFor.

Skoro jest to BDD, zwykle chcemy zdefiniować metody GWT (Given, When, Then). Jeśli nie jest to znane Wam, zachęcam do przeczytania poprzednich postów.

Przykład szablonu mógłby wyglądać następująca:

class OrderSpecs
{
   class WhenUserIsAuthorized : SpecsFor.SpecsFor<OrderController>
   {
       // warunki wstepne
       protected override void Given()
       {
           base.Given();
       }

       // na skutek jakiegos zdarzenia...
       protected override void When()
       {
           base.When();
       }

       // Nastepnie implementujemy jedna lub wiecej metod sprawdzajace post-conditions.
       [Test]
       public void ThenListOfOrdersCanBeReturned()
       {

       }
   }
   class WhenUserIsNotAuthorized : SpecsFor.SpecsFor<OrderController>
   {
       // warunki wstepne
       protected override void Given()
       {
           base.Given();
       }

       // na skutek jakiegos zdarzenia...
       protected override void When()
       {
           base.When();
       }

       // Nastepnie implementujemy jedna lub wiecej metod sprawdzajacych post-conditions.
       [Test]
       public void ThenErrorMessageShouldBeDisplayed()
       {

       }
   }
}

Zwykle definiuje się jedną klasę ze specyfikacjami. Następnie każda z zagnieżdżonych klas zawiera metody GWT. Given i When możemy przeładować, z kolei Then definiujemy jako własne metody w formie testów jednostkowych. Proszę zauważyć, że nazwy klas, opisują scenariusze użycia, dlatego warto je pogrupować w jedną klasę ze specyfikacjami. Oczywiście nic nie stoi na przeszkodzie, aby zdefiniować, każdy spec w osobnym pliku, ale myślę, że wzorzec zaproponowany przez twórców frameowrk’a ma sens.

Kolejny krok to implementacja metod:

public class WhenUserIsAuthorized : SpecsFor.SpecsFor<OrderController>
{
  private readonly List<string> _orders = new List<string>() {"Book", "Game", "Music"};
  private string _login = "piotr";
  private string _password = "haslo";
  
  // warunki wstepne
  protected override void Given()
  {
      GetMockFor<IAuthorization>().Setup(x => x.Authorize(It.IsAny<string>(), _password)).Returns(true);
      SUT.LogIn(_login, _password);
      SUT.SubmitOrder(_orders);

      base.Given();
  }

  // na skutek jakiegos zdarzenia...
  protected override void When()
  {
      SUT.SubmitOrder(_orders);

      base.When();
  }

  // Nastepnie implementujemy jedna lub wiecej metod sprawdzajacych post-conditions.
  [Test]
  public void ThenListOfOrdersShouldBeExposed()
  {
      SUT.Orders.ShouldLookLike(_orders);
      GetMockFor<IAuthorization>().Verify(x => x.Authorize(_login, _password), Times.Once);
  }
}

SUT to instancja testowanego obiektu. SpecsFor jest odpowiedzialny za stworzenie tego obiektu i w przeciwieństwie do klasycznych testów jednostkowych, nie musimy wstrzykiwać sami zależności. SpecsFor stworzy obiekt i wstrzyknie zależności.

GetMockFor służy właśnie do definiowania mock’ow, które automatycznie zostaną wstrzyknięte. W naszym przypadku jest to IAuthorization, który zostanie przekazany do konstruktora OrderController.

ShouldLookLike z kolei oparty jest na expectedObjects, który opisałem w poprzednim wpisie. Dzięki temu możliwe jest porównywanie całych obiektów (kolekcji, zagnieżdżonych klas itp.).

Verify z kolei to znów element Moq, umożliwiający sprawdzenie czy dana metoda w mock’u została wywołana.

Powyższy przykład pokazał więc, jak SpecsFor integruje się z nUnit, Moq, ShouldLib i expectedObject. Nic nie stoi na przeszkodzie, by używać frameowrk’a w stary, klasyczny sposób na zasadzie czystych testów jednostkowych. W przeciwieństwie do SpecsFlow jest to bardziej elastyczne narzędzie, co zwykle powoduje, ze programiści źle z niego korzystają. Osobiście wolę SpecsFlow bo jest dla mnie bardziej przejrzysty i łatwo zaprezentować potem scenariusze, które zostały przetestowane. Wystarczy skopiować tekst z feature files i mamy jeden dokument opisujący co zostało przewidziane w czasie definiowania testów.

Testy jednostkowe: expectedObjects oraz Should Assertion Library

W następnym poście mam zamiar napisać o SpecsFor, kolejnym framework’u ułatwiającym pisanie testów BDD. Najpierw jednak chciałbym przedstawić expectedObjects oraz Should Assertion Library, które są składowymi SpecsFlow. Wszystkie z wymienionych bibliotek można zainstalować przez NuGet.

Should Assertion Library to mała biblioteka ułatwiająca asercje danych. W standardowych unit testach zwykle piszemy coś w stylu:

Assert.IsTrue(value);
Assert.IsNull(value);
Assert.AreEqual(actualValue,expectedValue)

Jest to jak najbardziej w porządku, ale dzięki Should, możemy uzyskać dużo czytelniejszy kod, który zwłaszcza jest przydatny w BDD.

Zamiast Assert.IsTrue możemy teraz:

bool value = true;
value.ShouldBeTrue();

Jeśli warunek byłby nieprawdziwy, wtedy zostanie wyrzucony Should.Core.Exceptions.FalseException. Przykład:

bool value = true;
value.ShouldBeFalse();

Tak jak to w przypadku asercji, można również przekazać treść błędu:

bool value = true;
value.ShouldBeFalse("Wartosc nie jest prawdziwa");

Analogicznie, Should dostarcza wiele innych typów asercji:

obj.ShouldBeNull();

obj = new object();
obj.ShouldBeType(typeof(object));
obj.ShouldEqual(obj);
obj.ShouldNotBeNull();
obj.ShouldNotBeSameAs(new object());
obj.ShouldNotBeType(typeof(string));
obj.ShouldNotEqual("foo");

Ciekawym rozszerzeniem jest weryfikacja zasięgu wartości:

obj = "x";
obj.ShouldNotBeInRange("y", "z");
obj.ShouldBeInRange("a", "z");
obj.ShouldBeSameAs("x");

Should posiada również wsparcie dla kolekcji danych:

var list = new List<object>();
list.ShouldBeEmpty();
list.ShouldNotContain(new object());

Powyższy przykład to tzw. standardowy sposób weryfikacji. Druga alternatywa to Fluent Interface. Takim sposobem można przepisać powyższe zapytania w następujący sposób (źródło: dokumentacja):

obj = new object();
obj.Should().Be.OfType(typeof(object));
obj.Should().Equal(obj);
obj.Should().Not.Be.Null();
obj.Should().Not.Be.SameAs(new object());
obj.Should().Not.Be.OfType<string>();
obj.Should().Not.Equal("foo");

Przejdźmy teraz do expectedObjects. W unit testach porównujemy otrzymane wyjście z tym czego się spodziewamy. W przypadku prostych typów (liczby) nie jest to nic trudnego. Co jeśli mamy klasę typu Person?

class Person
{
   public string FirstName { get; set; }
   public string LastName { get; set; }

   public string ContactNumber { get; set; }
   public string Email { get; set; }
   public string Skype { get; set; }
}

Możliwe jest oczywiście weryfikowanie każdego pola osobno, ale jest to jednak zbyt czasochłonne i monotonne. Dzięki expectedObjects, możemy porównywać również typy złożone:

Person personA = new Person() {FirstName = "Piotr"};
Person personB = new Person() { FirstName = "Pawel" };
personA.ToExpectedObject().ShouldEqual(personB);

W tym przypadku, obiekty nie zgadzają się (imię) i zostanie wyrzucony wyjątek:

An unhandled exception of type 'System.Exception' occurred in ExpectedObjects.dll

Additional information: For Person.FirstName, expected "Piotr" but found "Pawel".

Jak widać, dokładnie w wyjątku będzie napisane, która część różni się.

expectedObjects potrafi również analizować zagniezdzone obiekty:

class Person
{
   public string FirstName { get; set; }
   public string LastName { get; set; }

   public Contact Contact { get; set; } 
}
class Contact
{
   public string ContactNumber { get; set; }
   public string Email { get; set; }
   public string Skype { get; set; }
}

Wywołanie:

Person personA = new Person() {Contact = new Contact() {Email = "email1"}};
Person personB = new Person() { Contact = new Contact() { Email = "email2" } };
personA.ToExpectedObject().ShouldEqual(personB);

Wyjątek:

An unhandled exception of type 'System.Exception' occurred in ExpectedObjects.dll

Additional information: For Person.Contact.Email, expected "email1" but found "email2".

Analogicznie można sprawdzać całe kolekcje danych:

var listA = new List<Person>();
var listB = new List<Person>();

listA.Add(new Person() {FirstName = "Piotr"});
listA.Add(new Person() { FirstName = "Pawel" });

listB.Add(new Person() { FirstName = "Piotr" });
listB.Add(new Person() { FirstName = "Pawel" });

listA.ToExpectedObject().ShouldEqual(listB);

Wyjątek:

An unhandled exception of type 'System.Exception' occurred in ExpectedObjects.dll

Additional information: For List`1[1].FirstName, expected "Pawel" but found "Pawel".

expectedObjects rozpoznają także słowniki:

var dictionaryA = new Dictionary<string,Person>();
var dictionaryB = new Dictionary<string, Person>();

dictionaryA.Add("1",new Person() { FirstName = "Piotr" });
dictionaryA.Add("2", new Person() { FirstName = "Pawel" });

dictionaryB.Add("1", new Person() { FirstName = "Piotr" });
dictionaryB.Add("3", new Person() { FirstName = "Pawel" });

dictionaryA.ToExpectedObject().ShouldEqual(dictionaryB);

W przykładzie klucze się różnią, co poskutkuje:

An unhandled exception of type 'System.Exception' occurred in ExpectedObjects.dll

Additional information: For Dictionary`2[1].Key, expected "2" but found "3".

Inny scenariusz to takie same klucze ale różne wartości:

var dictionaryA = new Dictionary<string,Person>();
var dictionaryB = new Dictionary<string, Person>();

dictionaryA.Add("1",new Person() { FirstName = "Piotr" });
dictionaryA.Add("2", new Person() { FirstName = "Pawel" });

dictionaryB.Add("1", new Person() { FirstName = "Piotr" });
dictionaryB.Add("2", new Person() { FirstName = "test" });

An unhandled exception of type 'System.Exception' occurred in ExpectedObjects.dll

Additional information: For Dictionary`2[1].Value.FirstName, expected "Pawel" but found "test".

Jak widać możliwości są duże i na dodatek można je rozszerzyć implementując następujący interfejs:

public interface IComparisonStrategy
{
    bool CanCompare(Type type);
    bool AreEqual(object expected, object actual, IComparisonContext comparisonContext);
}

Potem wystarczy wstrzyknąć powyższą strategię w momencie tworzenia obiektu:

_expected = new Foo("Bar")
.ToExpectedObject()
.Configure(ctx => ctx.PushStrategy<FooComparisonStrategy>());

Przydatna biblioteka: JSON.NET

JSON.NET to darmowa i naprawdę prosta w użyciu biblioteka, która umożliwia parsowanie JSON z poziomu c#. Instalujemy ją oczywiście z NuGet:

image

Następnie API jest na tyle proste, że wystarczy tak naprawdę nam tylko IntelliSense:

var person = new Person() {FirstName = "Piotr", LastName = "Zielinski"};
string content = JsonConvert.SerializeObject(person);
Console.WriteLine(content);

Efektem będzie konwersja obiektu c# do JSON czyli:

{"FirstName":"Piotr","LastName":"Zielinski"}

Podobnie można dokonać deserializacji:

var person = new Person() {FirstName = "Piotr", LastName = "Zielinski"};
string content = JsonConvert.SerializeObject(person);

Person newPerson = JsonConvert.DeserializeObject<Person>(content);

Możliwe jest również manipulowanie na obiekcie JObject:

JObject person = JObject.Parse(@"{'FirstName':'Piotr','LastName':'Zielinski'}");

JObject eksponuje JSON i mamy do niego dostęp np. za pomącą indexer’a:

JObject person = JObject.Parse(@"{'FirstName':'Piotr','LastName':'Zielinski'}");
            Console.WriteLine(person["FirstName"]);

Jeśli dana właściwość to tablica to mamy dostęp do niej za pomocą standardowego indeksu:

JObject person = JObject.Parse(@"{'FirstName':'Piotr','LastName':'Zielinski','Numbers':['1','2','3']}");
Console.WriteLine(person["Numbers"][0]);
Console.WriteLine(person["Numbers"][1]);

Jeśli przyjrzyjmy się JObject, to zobaczymy, że jest tam wiele metod do odpytywania obiektu np.:

public IEnumerable<JToken> Descendants

JSON.NET wspiera również LINQ, co czyni wyszukiwanie danych bardzo łatwe (przykład z dokumentacji):

var categories =
    from c in rss["channel"]["item"].Children()["category"].Values<string>()
    group c by c
    into g
    orderby g.Count() descending
    select new { Category = g.Key, Count = g.Count() };

foreach (var c in categories)
{
   Console.WriteLine(c.Category + " - Count: " + c.Count);
}

Dzięki JSON.NET  możliwa jest konwersja pomiędzy XML a JSON:

XmlDocument xmlDocument=new XmlDocument();
xmlDocument.LoadXml("<Person><FirstName>Piotr</FirstName><LastName>Zielinski</LastName></Person>");
string jsonText = JsonConvert.SerializeXmlNode(xmlDocument);

Powyższe przykłady dotyczą przeszukiwania obiektów. Analogicznie jednak można je tworzyć tzn.:

JObject jObject=new JObject();
jObject["FirstName"] = "Piotr";
jObject["Numbers"]=new JArray("1","2","3");


Console.WriteLine(jObject.ToString());

Kod wygeneruje następujący JSON:

{
  "FirstName": "Piotr",
  "Numbers": [
    "1",
    "2",
    "3"
  ]
}

Od C# 4.0 mamy do dyspozycji dynamic dlatego ładniejszą składnie uzyskamy pisząc:

dynamic jObject=new JObject();
jObject.FirstName = "Piotr";
jObject.Numbers=new JArray("1","2","3");


Console.WriteLine(jObject.ToString());

WatiN–testowanie aplikacji ASP.NET

Ostatnio pokazałem jak działa WatiN na przykładzie aplikacji konsolowej. Zwykle nie jest to co chcemy uzyskać. W praktyce korzysta się z jakiegoś framework’u do unit testów. W dzisiejszym wpisie pokażę jak to zrobić oraz jak uruchomić IIS Express.

Pamiętamy również, że musieliśmy dodać parametr STAThread ponieważ w przeciwnym razie został wyrzucany wyjątek “An exception of type ‘System.Threading.ThreadStateException’ occurred in WatiN.Core.dll but was not handled in user code"”.

W tym problem, że testy jednostkowe nie są uruchamiane w STA i dostaniemy taki sam wyjątek. Z tego względu, każdą klasę testów należy oznakować atrybutem RequiresSTAAttribute np.:

[TestFixture]
[RequiresSTAAttribute]
public class Tests
{
   [Test]
   public void SampleTest()
   {     
       using (var browser = new IE("http://www.google.com"))
       {
           browser.TextField(Find.ByName("q")).TypeText("Hello World!");
           browser.Button(Find.ByName("btnG")).Click();
       }

   }      
}

W praktyce nie testuje się zewnętrznych stron. Celem tego postu jest pokazanie jak testować aplikację ASP.NET za pomocą nUnit i WatIn. Z tego względu musimy w SetUp uruchomić najpierw serwer hostujący daną stronę. Uruchamia go się za pomocą następującej komendy:

WebDev.WebServer /port:8080 /path:"C:\Website"

Z kolei proces WebDev.WebServer można znaleźć w:

C:\Program Files (x86)\Common Files\Microsoft Shared\DevServer\10.0

Jeśli uruchomimy powyższy proces to serwer będzie hostował daną aplikację. Dalej w testach możemy już posługiwać się adresem typu localhost:8080/SampleWebsite. W wpisie jednak będę używał www.google.pl bo po prostu nie chcę tworzyć specjalnie strony ASP.NET (jak już serwer IIS jest uruchomiony to nie ma różnicy już jaką stronę ładujemy).

Przykład:

using (var browser = new IE("http://www.google.pl"))
{
    browser.TextField(Find.ByName("q")).Value = "Piotr Zielinski c#";
    browser.Button(Find.ByName("btnG")).Click();

    Assert.IsTrue(browser.ContainsText("www.pzielinski.com"));
}

Powyższy test zweryfikuje, czy po wpisaniu mojego imienia i nazwiska, link do blogu znajduje się na pierwszej stronie wyników.

WatiN–testowanie aplikacji internetowych

Dzisiaj chciałbym przedstawić framework WatiN, który służy do automatyzacji testów. Symuluje on po prostu przeglądarkę internetową (dosłownie). Za pomocą niego, możemy otworzyć IE, wejść na daną stronę czy kliknąć w jakiś przycisk. Wszystkie operacje takie jak kliknięcie w link czy nawet maksymalizacja okna są możliwe dzięki WatiN.

Docelowo używa go się w połączeniu z nUnit albo SpecFlow. Zacznijmy jednak od zwyklej aplikacji konsolowej. Załóżmy, że chcemy wejść na Google.pl, wpisać tekst i kliknąć w przycisk Search.

Najpierw oczywiście instalujemy wymagany pakiet z NuGet:

image

Następnie implementujemy kod, który wykona następujące czynności:

1. Otworzy przeglądarkę IE.

2. Przejdzie do www.google.pl

3. Naciśnie przycisk “Szukaj”, aby wyświetlić wyniki.

Kod jest następujący:

using (var browser = new IE("http://www.google.com"))
{
 browser.TextField(Find.ByName("q")).TypeText("Hello World!");
 browser.Button(Find.ByName("btnG")).Click();
}

Jeśli wywołuje on poniższy wyjątek, wtedy trzeba ustawić  “Embed Interop Types” we właściwościach na false.

Additional information: Could not load file or assembly 'Interop.SHDocVw, Version=1.1.0.0, Culture=neutral, PublicKeyToken=db7cfd3acb5ad44e' or one of its dependencies. The system cannot find the file specified.

image

Wciąż jednak kod nie będzie działać i zostanie wyrzucony kolejny wyjątek:

An unhandled exception of type 'System.Threading.ThreadStateException' occurred in WatiN.Core.dll

Additional information: The CurrentThread needs to have it's ApartmentState set to ApartmentState.STA to be able to automate Internet Explorer.

Z tego względu dodajemy wspomniany atrybut (więcej informacji tutaj) i uruchamiamy program:

[STAThread]
private static void Main(string[] args)
{
  using (var browser = new IE("http://www.google.com"))
  {
      browser.TextField(Find.ByName("q")).TypeText("Hello World!");
      browser.Button(Find.ByName("gbqfsa")).Click();

  }
}

Na ekranie pojawi się przeglądarka i automatycznie zostanie wpisany tekst:

image

Prawdziwa jednak siła WatiN jest gdy korzystamy z niego w połączeniu z nUnit. Framework dostarcza naprawdę wiele metod umożliwiających walidacje i ekstrakcje danych ze stron HTML.  Bardzo łatwo możemy sprawdzić czy dany tekst występuje i co jest dokładnie wyświetlane na ekranie. O tym jednak w kolejnej części…

SignalR: Wywoływanie metod z zewnętrznych klas

W poprzednich postach zakładaliśmy, że komunikacja między Hubem, a klientem (np. przeglądarką) odbywa się zawsze w wewnątrz hub’a,  w następnie wykonania metody serwerowej.

Bardzo często jednak jest potrzeba powiadomienia klienta po np. jakiś procesie biznesowym. Może to być automatycznie wykonywane zadanie czy po prostu akcja kontrolera.

Pierwsza próba, mogłaby wyglądać następująco:

var myFirstHub=new MyFirstHub();
myFirstHub.Clients.All.newNumber(number);

W ostatnim wpisie jednak napisałem, że instancja hub’a jest przechowywana przez SignalR i nie możemy sami manipulować nim. Powyższy kod zwróci zatem wyjątek InvalidOperationException:

Using a Hub instance not created by the HubPipeline is unsupported.

Prawidłowa inicjalizacja to zwrócenie kontekstu SignalR:

public ActionResult Increment(int number)
{                                   
  IHubContext hub=GlobalHost.ConnectionManager.GetHubContext<MyFirstHub>();
  hub.Clients.All.newNUmber(number);

  return View();
}

Jedna uwaga na koniec. Operacja GetHubContext może być czasochłonna i z tego względu, zawsze lepiej buforować kontekst a nie za każdym razem go tworzyć od nowa.