Testowanie infrastruktury WebAPI z MSpec

5

W poprzednim poście poruszyłem temat mojego side-projectu (który BTW nie powinien zająć więcej niż 2-3 dni, ale lubię dawkować przyjemności więc pewnie jeszcze trochę to potrwa;) ) i MSpec. Teraz zobaczymy jak można w bardzo ciekawy sposób przetestować WebAPI emulując dosłownie całego requesta z kodu, co miło przejedzie przez kompletny stack i sprawdzi nie tylko logikę, ale również m.in. konfigurację routingu… a docelowo także komunikację z bazą.

Na początku wypada napisać o “credits” – pomysł i część kodu pochodzi z fajnego kursu “Outside-In Test-Driven Development“, chociaż wystarczy choć trochę poczytać o WebAPI żeby takie zastosowanie “wykminić”.

Co jest do osiągnięcia: przygotowanie infrastruktury dla testów kontrolerów WebAPI pozwalajacej na jak najprostsze wygenerowanie żądania bez potrzeby IISa etc. Ma to być prawdziwy request HTTP a nie bezpośrednie odwołanie się do obiektu kontrolera. Po zerknięciu na artykuły “Self-Host a Web API“, “HTTP Message Handlers” oraz “HttpClient Message Handlers” widzimy, że możemy spróbować najpierw stworzyć instancję własnego serwera i nawet bez rozpoczęcia nasłuchiwania na nim przekazać ów obiekt do HttpClient. Co się wtedy stanie? Otrzymamy możliwość generowania requestów in-process i bawienia się bezpośrednio HTTP bez przejmowania się wątkami, procesami, serwerami itd. Bardzo wygodne.

Jak zatem cuś takiego zakodować? Oto moja klasa bazowa dla testów rzucających żądania do WebAPI:

public abstract class making_request
{
    Establish ctx = () =>
        {
            var address = new Uri("http://localhost:8355");
            var config = new HttpSelfHostConfiguration(address);

            // configuring routing in assembly under test 
            Bootstraper.ConfigureWebApi(config);

            var server = new HttpSelfHostServer(config);

            _httpClient = new HttpClient(server);
            _httpClient.BaseAddress = address;
        };

    Because of = () => _response = _httpClient.GetAsync(_url).Result;

    Cleanup client = () => _httpClient.Dispose();

    static HttpClient _httpClient;

    protected static string _url;
    protected static HttpResponseMessage _response;
}

W jednym miejscu mam zdefiniowaną całą pożądaną funkcjonalność, więc utworzenie testów z niej korzystających ogranicza się do dziedziczenia i ustawienia pola “_url”.

Tutaj bardzo dobrze widać trzy z czterech etapów wykonywania każdego testu jednostkowego: Arrange (“establish context”), Act (“because of”) oraz Cleanup. Widzimy jak ładnie MSpec wymusza rozbicie tego na osobne fragmenty kodu. Establish, Because i Cleanup to delegaty zdefiniowane w MSpec uruchamiane przez test runner w odpowiedniej kolejności. Na samym początku taka proponowana składnia może wydawać się dziwna i odrzucająca, ale po chwili przyzwyczajenia (oraz być może zdefiniowaniu przydatnych snippetów/live templates w R#) staje się przejrzystym, normalnym zapisem.

W tym momencie mogę dodać swój pierwszy prawdziwy test sprawdzający czy w ogóle mam działające środowisko, które mogę testować i rozwijać dalej:

[Subject("Environment check")]
public class when_requesting_root
    : making_request
{
    Establish ctx = () => _url = "";

    It returns_success_response = () => _response.IsSuccessStatusCode
        .ShouldBeTrue();
}

I to jest w MSpec piękne: klasy testów wyglądają w większości właśnie tak. Krótkie, czytelne, proste. W celu wykonania scenariusza “żądania roota aplikacji” musimy jedynie zdefiniować wartość URLa z klasy bazowej. A że poszczególne kroki “Establish” wykonywane są po kolei, idąc od góry hierarchii dziedziczenia, w samej klasie z testem możemy bezpiecznie korzystać z pól zdefiniowanych wyżej.

Przy tak ogólnym teście mającym na celu tak naprawdę sprawdzenie wyłącznie poprawnego poinstalowania nugetów, zainicjalizowania konfiguracji WebAPI czy stworzenie pierwszego kontrolera (przydatne na początek poznawania tej biblioteki) oczekujemy tylko jednego: odpowiedzi HTTP z ustawionym kodem “200 OK”. Które to kroki zostawię zainteresowanym do przejścia na własną rękę:).

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.

5 Comments

  1. Pingback: dotnetomaniak.pl

  2. Lech Pawłasek on

    Przyznam się, że nazwa klasy z małej litery oraz _ trochę kłują w oczy :) Skąd wzięła się ta notacja?

  3. nice.

    btw, gdy uzywasz caly pipeline in-memory, nie ma tutaj nawet potrzeby uzywac selfhosta, to samo mozna zapisac prosciej:

    var config = new HttpConfiguration();
    Bootstraper.ConfigureWebApi(config);
    var server = new HttpServer(config);
    _httpClient = new HttpClient(server);

    to sie przydaje sie nie tylko w integration tests ale i przy request batching.
    Trzeba jeszcze dodac ze przy in-memory hosting masz do czynienia z buffered requests. Jesli twoje API przyjmuje non-buffered requests (i.e. przez custom IHostBufferPolicySelector etc) to in-memory host nie nadaje sie do testowania.

  4. F,
    Dzięki, elegancko:). To kod z podlinkowanego kursu na pluralsight, widocznie @ploeh nie zainteresował się zbyt głęboko tematem a i ja nie wnikałem bardziej:)