"Test reuse" w MSpec

1

Pisząc testy jednostkowe dość często spodziewamy się identycznego zachowania w różnych testowanych scenariuszach. “Gdy zajdzie X, ma wydarzyć się A, B i C”. Z kolei “gdy zajdzie Y, ma wydarzyć się A, B i D”. W takich przypadkach, wykorzystując standardowe biblioteki do unit testów, mamy do wyboru kilka rozwiązań:

  • wspólna klasa bazowa
  • współdzielone metody “asercji” w ramach jednej klasy
  • copy/paste testów pomiędzy klasami
  • … pewnie jeszcze coś innego

Ja osobiście najczęściej wykorzystywałem wielokrotne wykorzystanie tych samych metod zawierających odpowiednie asercje, niejednokrotnie zamykając je w dedykowane extension methods.

MSpec oferuje o wiele ciekawsze podejście do tego problemu. Taka charakterystyka kodu testów została zauważona i jawnie wyeksponowana w API jako [Behaviors]. Można było to zaobserwować w moim pierwszym poście o MSpec. Określiłem tam dwie możliwe ścieżki prawidłowego wykorzystania “url-shortenera” / Redirectora. Scenariusz pierwszy: ktoś może po raz pierwszy odwołać się do jakiegoś linku. Scenariusz drugi: link może zostać zażądany po raz kolejny. Chciałbym, aby oba te przypadki zachowywały się podobnie:

  • zwróć status HTTP 302
  • ustaw nagłówek “Location” na docelowy URL

Scenariusze te różnią się tylko jednym oczekiwanym rezultatem. Link odwiedzany po raz pierwszy nie ma na początku żadnych przypisanych do siebie “wizyt”, więc po zakończeniu żądania powinien pojawić się jeden, i tylko jeden, obiekt wizyty podpięty pod zdefiniowany “skrót”. Z kolei link odwiedzany po raz N-ty na początku posiada już N-1 wizyt, i żądanie kolejne powinno skutkować dodaniem jeszcze jednej wizyty bez usuwania poprzednich.

Dzięki mechanizmowi obecnemu w MSpec w bardzo prosty sposób “wyciągam” część wspólną do dedykowanej klasy opatrzonej atrybutem [Behaviors]:

[Behaviors]
public class valid_redirection
{
    It returns_redirect = () => _response.StatusCode
	.ShouldEqual(HttpStatusCode.Redirect);

    It redirects_to_target_url = () => _response.Headers.Location.AbsoluteUri
	.ShouldEqual(_link.Target);

    protected static HttpResponseMessage _response;
    protected static Link _link;
}

To wszystko, klasa ta nie ma żadnych zależności. Jedyne czego muszę dopilnować to odpowiednie definicje obiektów wykorzystywanych podczas tworzenia takich “oderwanych” od wszystkiego testów. Pola _reponse i _link muszą być zadeklarowane jako “protected static”, inaczej zostaną przez MSpec zignorowane… ot, taka konwencja. Dodatkowy wymóg to zgodność nazw tych pól z polami klas wykorzystujących te Behaviors.

Najlepiej jednak, oczywiście, będzie zerknąć w sam kod klas opisujących i testujących żądania linka:

[Subject(typeof(RedirectController))]
public class when_requesting_existing_link_for_the_first_time
    : requesting_existing_link
{
    Behaves_like<valid_redirection> valid_redirection;

    It adds_new_visit = () => load_link().Visits.ShouldNotBeEmpty();
}

[Subject(typeof(RedirectController))]
public class when_requesting_existing_link_for_nth_time
    : requesting_existing_link
{
    Establish ctx = () =>
    {
        var link = load_link();
        link.Visits.Add(new Visit());
        link.Visits.Add(new Visit());

        using (var session = _store.OpenSession())
        {
            session.Store(link);
            session.SaveChanges();
        }
    };

    Behaves_like<valid_redirection> valid_redirection;

    It adds_another_visit = () => load_link().Visits.Count.ShouldEqual(3);
}

Prawda że proste i, po chwili przyzwyczajenia, przejrzyste? Wykorzystuję tutaj dodatkowo RavenDB, ale nie ma to w danym momencie znaczenia. Wszelkie “skomplikowanie”, czyli kod inicjalizacyjny, jest schowany w bazowej klasie requesting_existing_link.

Jako bonus dorzucę test zachowania systemu w przypadku, gdy przyjdzie żądanie niezdefiniowanego linka:

[Subject(typeof(RedirectController))]
public class when_requesting_nonexisting_link
    : requesting_link
{
    Establish ctx = () => _alias = "nonexisting-link";

    It returns_not_found = () => _response.StatusCode.ShouldEqual(HttpStatusCode.NotFound);
}

Czyta się to bardzo dobrze, pisze też w porządku. Jakieś spostrzeżenia/uwagi?

Share.

About Author

Programista, trener, prelegent, pasjonat, blogger. Autor podcasta programistycznego: DevTalk.pl. Jeden z liderów Białostockiej Grupy .NET i współorganizator konferencji Programistok. Od 2008 Microsoft MVP w kategorii .NET. Więcej informacji znajdziesz na stronie O autorze. Napisz do mnie ze strony Kontakt. Dodatkowo: Twitter, Facebook, YouTube.

1 Comment

  1. Pingback: dotnetomaniak.pl

Newsletter devstyle!
Dołącz do 2000 programistów!
  Zero spamu. Tylko ciekawe treści.
Dzięki za zaufanie!
Do przeczytania wkrótce!
Niech DEV będzie z Tobą!