SpecsFor MVC

Posted July 21st, 2014 by Piotr Zieliński
Categories: ASP .NET, Testy

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

Posted July 18th, 2014 by Piotr Zieliński
Categories: C#, Testy

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

Posted July 15th, 2014 by Piotr Zieliński
Categories: Patterns & Practices, Testy

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

Posted July 12th, 2014 by Piotr Zieliński
Categories: C#, JavaScript

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

Posted July 9th, 2014 by Piotr Zieliński
Categories: Testy

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

Posted July 6th, 2014 by Piotr Zieliński
Categories: Testy

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

Posted July 3rd, 2014 by Piotr Zieliński
Categories: ASP .NET, JavaScript

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.

SignalR: Przekazywanie stanu

Posted July 1st, 2014 by Piotr Zieliński
Categories: ASP .NET, JavaScript

Obiekty Hub mają z góry określony czas życia. Ten sam obiekt nie jest współdzielony pomiędzy różne wywołania. Jeśli klient wywołuje metodę, instancja huba jest tworzona od nowa. Oznacza to, że nie możemy przechować stanu między zapytaniami w następujący sposób:

public class MyFirstHub : Hub
{
    private int _number = 0;

    public void Increment()
    {
        _number++;
        Clients.All.newNumber(_number);
    }
}

Na wyjściu takim sposobem, zawsze będzie jeden – za każdym wywołaniem instancja jest tworzona od nowa.

Pozostają inne opcje takie jak baza danych, zmienne statyczne itp. SignalR dostarcza jednak pewien mechanizm, który dla małej ilości danych jest dobry.

Każdy klient podłączony do Hub może przekazać swój stan w następujący sposób:

public class MyFirstHub : Hub
{    
   public void Increment()
   {
       Clients.Caller.number++;
       Clients.All.newNumber(Clients.Caller.number);
   }
}

Zmienna number w powyższy przykładzie to nasz stan. Z kolei Caller jak wcześniej, reprezentuje klienta, który wykonał daną metodę serwerową.

W JavaScript z kolei, możemy ustawić stan na dowolną wartość:

var myFirstHub = $.connection.myFirstHub;
myFirstHub.state.number = 0;

Oczywiście należy pamiętać, że cały stan, z każdym zapytaniem będzie wysyłany do serwera i potem zwracany. Z tego względu, lepiej ograniczyć się do małych zmiennych.

SignalR: wywoływanie metod

Posted June 25th, 2014 by Piotr Zieliński
Categories: ASP .NET, JavaScript

Dzisiaj trochę więcej szczegółów na temat, jak można wywoływać metody w Hub API. W ostatnim wpisie, zaimplementowaliśmy klasę, która wykonuje metodę po wszystkich klientach (broadcast). SignalR ma ogromne możliwości i istnieje wiele innych wzorców.

Dla przypomnienia broadcast wygląda następująco:

public void SendMessage(string message)
{
  Clients.All.newMessage(string.Format("{0}: {1}",DateTime.Now,message));
}

Clients posiada kilka metod, służących do wysyłania wiadomości w różnych sposób. Na przykład, aby wykonać metodę wyłącznie po stronie klienta, który wysłał zapytanie do serwera można:

public void SendMessage(string message)
{
  Clients.Caller.newMessage(string.Format("{0}: {1}",DateTime.Now,message));
}

Innymi słowy, gdy przeglądarka wywołuje SendMessage, newMessage zostanie wykonane wyłącznie w tej przeglądarce (a nie jak wcześniej we wszystkich podłączonych klientach).

Inną ciekawą konstrukcją jest wywołanie metody we wszystkich klientach, oprócz tego który zainicjował połączenie (wywołał metodę serwerową):

public void SendMessage(string message)
{
  Clients.Others.newMessage(string.Format("{0}: {1}",DateTime.Now,message));
}

Istnieje opcja wywołania metody dla określonego nazwą (string) połączenia:

Clients.Client("connectionID").newMessage(string.Format("{0}: {1}",DateTime.Now,message));

Analogicznie, wszystkie połączenia oprócz tych przekazanych jako parametr:

public void SendMessage(string message)
{
  Clients.AllExcept("connectionID1","connectionId2").newMessage(string.Format("{0}: {1}",DateTime.Now,message));
}

Jeśli znamy nazwę użytkownika (IUserIdProvider), możemy wywołać metodę po stronie klienta, który używa określonego loginu:

public void SendMessage(string message)
{
  Clients.User("user1").newMessage(string.Format("{0}: {1}",DateTime.Now,message));
}

Bardzo ciekawym rozwiązaniem są grupy. Umożliwiają one implementacje wzorca polegającym na publish\subscribe. Dzięki grupom, można wywoływać metody, wyłącznie w tych klientach, które nalezą do konkretnej grupy.

public void SendMessage(string message)
{
  Clients.Group("group_name").newMessage(string.Format("{0}: {1}",DateTime.Now,message));
}

Powyższy kod wywoła newMessage w klientach należących do group_name. Pozostaje pytanie, jak dodać użytkownika (połączenie) do grupy? Wystarczy:

public class MyFirstHub : Hub
{
   public void AddToGroup(string groupName)
   {
       Groups.Add(Context.ConnectionId, groupName);
   }
   public void RemoveFromGroup(string groupName)
   {
       Groups.Remove(Context.ConnectionId, groupName);
   }
}

Add automatycznie stworzy grupę jeśli takowa nie istnieje. Kod dodaje połączenie skojarzone z klientem, który wywołuje właśnie serwerową metodę.

Powyższy kod jest jednak nie do końca poprawny. Zaglądając do dokumentacji, zobaczymy:

 Task Add(string connectionId, string groupName);

Oznacza to, że jest to metoda asynchroniczna. W praktyce, wywołanie powyższego kodu, zwróci rezultat natychmiast, mimo, że nie ma pewności, że połączenie zostało dodane do konkretnej grupy.

Z tego względu dużo lepiej jest:

public class MyFirstHub : Hub
{
   public async void AddToGroup(string groupName)
   {
       await Groups.Add(Context.ConnectionId, groupName);
   }
   public async void RemoveFromGroup(string groupName)
   {
       await Groups.Remove(Context.ConnectionId, groupName);
   }
}

W ostatnim poście również wspomniałem, że należy dodać następującą referencję:

<script src="~/signalr/hubs"></script>    

Tak naprawdę, jest to referencja do automatycznie wygenerowanego kodu JavaScript. Sprawdźmy, co dokładnie jest wygenerowane:

/*!
 * ASP.NET SignalR JavaScript Library v2.1.0
 * http://signalr.net/
 *
 * Copyright Microsoft Open Technologies, Inc. All rights reserved.
 * Licensed under the Apache 2.0
 * https://github.com/SignalR/SignalR/blob/master/LICENSE.md
 *
 */

/// <reference path="..\..\SignalR.Client.JS\Scripts\jquery-1.6.4.js" />
/// <reference path="jquery.signalR.js" />
(function ($, window, undefined) {
    /// <param name="$" type="jQuery" />
    "use strict";

    if (typeof ($.signalR) !== "function") {
        throw new Error("SignalR: SignalR is not loaded. Please ensure jquery.signalR-x.js is referenced before ~/signalr/js.");
    }

    var signalR = $.signalR;

    function makeProxyCallback(hub, callback) {
        return function () {
            // Call the client hub method
            callback.apply(hub, $.makeArray(arguments));
        };
    }

    function registerHubProxies(instance, shouldSubscribe) {
        var key, hub, memberKey, memberValue, subscriptionMethod;

        for (key in instance) {
            if (instance.hasOwnProperty(key)) {
                hub = instance[key];

                if (!(hub.hubName)) {
                    // Not a client hub
                    continue;
                }

                if (shouldSubscribe) {
                    // We want to subscribe to the hub events
                    subscriptionMethod = hub.on;
                } else {
                    // We want to unsubscribe from the hub events
                    subscriptionMethod = hub.off;
                }

                // Loop through all members on the hub and find client hub functions to subscribe/unsubscribe
                for (memberKey in hub.client) {
                    if (hub.client.hasOwnProperty(memberKey)) {
                        memberValue = hub.client[memberKey];

                        if (!$.isFunction(memberValue)) {
                            // Not a client hub function
                            continue;
                        }

                        subscriptionMethod.call(hub, memberKey, makeProxyCallback(hub, memberValue));
                    }
                }
            }
        }
    }

    $.hubConnection.prototype.createHubProxies = function () {
        var proxies = {};
        this.starting(function () {
            // Register the hub proxies as subscribed
            // (instance, shouldSubscribe)
            registerHubProxies(proxies, true);

            this._registerSubscribedHubs();
        }).disconnected(function () {
            // Unsubscribe all hub proxies when we "disconnect".  This is to ensure that we do not re-add functional call backs.
            // (instance, shouldSubscribe)
            registerHubProxies(proxies, false);
        });

        proxies['myFirstHub'] = this.createHubProxy('myFirstHub'); 
        proxies['myFirstHub'].client = { };
        proxies['myFirstHub'].server = {
            addToGroup: function (groupName) {
                return proxies['myFirstHub'].invoke.apply(proxies['myFirstHub'], $.merge(["AddToGroup"], $.makeArray(arguments)));
             },

            removeFromGroup: function (groupName) {
                return proxies['myFirstHub'].invoke.apply(proxies['myFirstHub'], $.merge(["RemoveFromGroup"], $.makeArray(arguments)));
             }
        };

        return proxies;
    };

    signalR.hub = $.hubConnection("/signalr", { useDefaultPath: false });
    $.extend(signalR, signalR.hub.createHubProxies());

}(window.jQuery, window));

Jak widać, plik zawiera głównie proxy. Dzięki niemu, możemy w łatwy sposób w JavaScript wywołać kod C# znajdujący się na serwerze:

 proxies['myFirstHub'].server = {
            addToGroup: function (groupName) {
                return proxies['myFirstHub'].invoke.apply(proxies['myFirstHub'], $.merge(["AddToGroup"], $.makeArray(arguments)));
             },

            removeFromGroup: function (groupName) {
                return proxies['myFirstHub'].invoke.apply(proxies['myFirstHub'], $.merge(["RemoveFromGroup"], $.makeArray(arguments)));
             }
        };

ASP.NET: SignalR i Hub API

Posted June 22nd, 2014 by Piotr Zieliński
Categories: ASP .NET, JavaScript

W poprzednim poście przedstawiłem framework SignalR oraz niskopoziomową klasę PersistentConnection. Zwykle jest ona trudna w użyciu i dlatego w praktyce dużo częściej korzysta się z Hub API.  Jak sama nazwa wskazuje, stanowi on koncentrator czyli miejsce gdzie składujemy metody zarówno wykonywane po stronie serwera jak i klienta. Dzięki niemu możemy wywołać metodę C# (serwer) jak i JavaScript (klient). Umożliwia on zatem komunikacje obustronną.

Spróbujmy zatem przerobić przykład z poprzedniego postu, aby używał Hub zamiast PersistentConnection. Najpierw oczywiście usuwamy MyFirstConnection i zastępujemy ją hub’em:

public class MyFirstHub : Hub
{
   public void SendMessage(string message)
   {
       Clients.All.newMessage(string.Format("{0}: {1}",DateTime.Now,message));
   }
}

Nasz hub ma jedną metodę teraz – Send Message. Proszę zauważyć, że All jest typu dynamic i dlatego możemy jakąkolwiek metodę wykonać. Metody są definiowane w JavaScript dlatego naturalne jest, że taka metoda nie zostanie automatycznie wykryta przez VS.

Powyższy kod jest implementacją serwerowej metody  SendMessage.  Po odebraniu wiadomości jest ona wysyła do wszystkich klientów (broadcast). newMessage musi zatem zostać zdefiniowana w JavaScript po stronie klienta:

<script type="text/javascript">
    $(function () {

        var myFirstHub = $.connection.myFirstHub;
        myFirstHub.client.newMessage = function (message) {
            $('#messages').append('<li>' + message + '</li>');
        };

        $.connection.hub.start();
        
        $("#broadcast").click(function () {
            myFirstHub.server.sendMessage($('#msg').val());
        });

    });
    </script>

<input type="text" id="msg" />
<input type="button" id="broadcast" value="broadcast" />

<ul id="messages"></ul>

Najpierw tworzymy hub’a a potem za pomocą obiektu server możemy wysyłać wiadomości do serwera. Analogicznie, istnieje możliwość definiowania metod po stronie klienta, które mogą być wykonywane przez serwer.

Należy koniecznie dodać referencje do automatycznie wygenerowanych hubów w JS:

<script src="~/Scripts/jquery-1.10.2.js" type="text/javascript"></script>
<script src="~/Scripts/jquery.signalR-2.1.0.js" type="text/javascript"></script>
<script src="~/signalr/hubs"></script>    

Na zakończenie wystarczy tylko zmapować hub’a do URL:

[assembly: OwinStartupAttribute(typeof(WebApplication9.Startup))]
namespace WebApplication9
{
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            ConfigureAuth(app);
            app.MapSignalR();
        }
    }
}

W kolejnej części zajmiemy się bardziej szczegółowo wywoływaniem metod w SignalR.