Dzisiaj oprogramowanie jest naprawdę skomplikowane. W większości przypadków, skomplikowanie oprogramowania nie polega na zaawansowanych algorytmach. W praktyce, algorytmy tworzone są przez wąską grupę specjalistów i potem są po prostu wykorzystywane w formie DLL przez innych użytkowników (programistów). Oczywiście każda aplikacja ma jakieś algorytmy, ale w aplikacjach biznesowych, zwykle większym problemem jest utrzymanie całości i możliwość szybkiego dostarczenia produktu. Słaby kod cechuje się długimi cyklami testowania regresyjnego. W dobrym projekcie, z dużą liczbą automatycznych testów, taki czas jest znaczenie skrócony.
Innym poważnym problemem jest zgromadzenie wymaganiach i ich poprawna interpretacja. Wiele projektów cierpi ze zbyt częstych zmian i niejasnych wymaganiach. Jest to jednak nieodłączna część towarzyszącą programistom i po prostu trzeba znaleźć sposób, aby minimalizować problemy komunikacyjne między programistami a np. ekspertami z danej dziedziny.
W dzisiejszym wpisie chciałbym przestawić framework SpecFlow, ułatwiający pisanie testów BDD – Behaviour Driven Development. Podejście bardzo popularne, w szczególności w aplikacjach biznesowych, gdzie testy są tak naprawdę opisane przez user story.
Nie będę opisywał tutaj BDD bo to temat prawdopodobnie na inny wpis. Skupie się głównie na samym framework’u i podam tylko kilka różnić między BDD a klasycznym testowaniem.
Przede wszystkim należy wiedzieć jak działa Test-Driven-Development. W skrócie jest to podejście, gdzie najpierw piszę się testy jednostkowe, a potem dopiero implementacje interfejsów. Takim sposobem, każdy nowy kod, powinien być już pokryty testami. Daje to taką przewagę, że programiści są zmuszeni do pisania elastycznego kodu – inaczej ciężko będzie napisać jakiekolwiek testy jednostkowe. Kolejną zaletą jest większa świadomość możliwych parametrów wejściowych i nieoczekiwanych scenariuszy.
Niestety bardzo często tworzone testy są ciężkie w utrzymaniu. Wiele testów to kontenery na metody, które z nazwy nic nie mówią. Skupiają się one na testowaniu API, a nie realnych scenariuszy. Jeśli mamy w kodzie metody o nazwach Method1NullTest to niewiele one mówią użytkownikowom czy testerom.
W BDD skupiamy się zatem na scenariuszach użycia aplikacji. Dlatego stanowią one tak naprawdę cześć opisu każdej, dobrze zdefiniowanej user story. Nie zapominajmy, że testy jednostkowe to black-box testing – nie interesuje nas jak coś jest zaimplementowane. Skupiamy się na tym, jak użytkownik może korzystać z naszej aplikacji. Rozważamy workflow’y, a nie konkretne gałęzie IF’a – bo jak zostało wspomniane, w black-box testing nie analizujemy kodu. Oczywiście metryki pokrycia kodu wciąż są pożytecznie i należy z nich korzystać. Chodzi mi bardziej, że programista powinien myśleć w kategoriach scenariuszy użycia, a nie pokrycia każdej gałęzi IF, przekazując tym samym wszystkie możliwe parametry wejściowe. Naturalnie, że w pewnych sytuacjach (algorytmy) sprawa wygląda inaczej i tam faktycznie testowanie polega na po prostu definiowaniu wejścia i wyjścia. W praktyce jednak, często błędy występującą w UI i w sposobie nawigacji (workflows).
BDD czy TDD to moim zdaniem bardzo naturalne podejścia – po prostu najpierw myślimy jak kod ma działać, a potem go implementujemy. Jeśli mamy sprzeczne wymagania to dowiemy się o tym na etapie definiowania testów, a nie na koniec, gdy 90% kodu jest już zaimplementowane.
W SpecFlow, zachowania czyli scenariusze opisane są za pomocą trzech kroków Given, When, Then (GWT):
1. Given stanowią preconditions czyli warunki wstępne. Innymi słowy, wyjaśniają one, jaki stan mamy przed danym scenariuszem.
2. When – zdarzenie, które powoduje wywołanie danego zachowania. Może być to np. kliknięcie w link na stronie internetowej czy wywołanie jakieś metody WebAPI.
3. Then – etap, w którym mamy jakiś rezultat do testowania.
Podsumowując, mając warunki Given, w momencie zdarzenia When, otrzymujemy jakieś dane, które chcemy zweryfikować.
SpecFlow integruje się ze środowiskiem Visual Studio. Musimy najpierw zainstalować odpowiedni plugin. Klikamy więc Tools->Extensions and Updates:
Następnie tworzymy nowy projekt (Windows Library) i instalujemy pakiet z NuGet, który zintegruje SpecFlow z nUnit:
Czas zacząć napisać nasz pierwszy scenariusz. W tym celu, przechodzimy do Add New Item i wybieramy SpecFlow Feature File:
Domyślnie zostanie wygenerowany przykładowy scenariusz:
Feature: SpecFlowFeature1 In order to avoid silly mistakes As a math idiot I want to be told the sum of two numbers @mytag Scenario: Add two numbers Given I have entered 50 into the calculator And I have entered 70 into the calculator When I press add Then the result should be 120 on the screen
Jak widać, jest on opisany naturalnym językiem. Oznacza to, że taki scenariusz jest zrozumiały nie tyko dla programistów czy testerów, ale również osoby nietechnicznej. W tym wpisie, wykorzystamy powyższy, automatycznie wygenerowany scenariusz. W kolejnym poście, zajmiemy się ASP.NET MVC i jakimś bardziej praktycznym przykładem.
SpecFlow, jak wspomniałem integruje się z Visual Studio. Dzięki temu możemy kliknąć w Generate Step Definitions w kontekstowym menu i zobaczymy następujące okno:
Po naciśnięciu Generate, zostanie wygenerowany nowy plik z testami:
[Binding] public class SpecFlowFeature1Steps { [Given(@"I have entered (.*) into the calculator")] public void GivenIHaveEnteredIntoTheCalculator(int p0) { ScenarioContext.Current.Pending(); } [When(@"I press add")] public void WhenIPressAdd() { ScenarioContext.Current.Pending(); } [Then(@"the result should be (.*) on the screen")] public void ThenTheResultShouldBeOnTheScreen(int p0) { ScenarioContext.Current.Pending(); } }
Na razie testy są oczywiście puste. Możemy przejść do pliku feature i wybrać z menu kontekstowego “Run SpecFlow Scenarios”:
Implementacja takiego testu mogłaby wyglądać tak:
[Given(@"I have entered (.*) into the calculator")] public void GivenIHaveEnteredIntoTheCalculator(int p0) { _calculator.EnterNumber(p0); } [When(@"I press add")] public void WhenIPressAdd() { _calculator.Add(); } [Then(@"the result should be (.*) on the screen")] public void ThenTheResultShouldBeOnTheScreen(int p0) { Assert.AreEqual(p0, _calculator.Output); }
Proszę zwrócić uwagę na (.*)- to są parametry przekazane z pliku features tzn.:
Given I have entered 50 into the calculator And I have entered 70 into the calculator When I press add Then the result should be 120 on the screen
W kolejnym wpisie dokładnie przyjrzyjmy się jak parametry są rozpoznawalne i przekazywane oraz jak wykonywany jest scenariusz. Na razie warto zapamiętać kroki GWT – Given, When, Then ponieważ są one najczęściej stosowane.
Moim zdaniem, najtrudniejsze w tym jest poprawne opisywanie testów. Tego oczywiście nie da się nauczyć z dokumentacji i po prostu trzeba praktykować. Złe testy to marnowanie czasu i zwiększanie kosztów na utrzymanie tego. Testy po prostu muszą redukować czas przeznaczany na manualne testowanie i naprawianie bug’ów. Jeśli poprawa nie jest zauważalna to oznacza, że coś źle robimy. W momencie zgłoszenia bug’a oprócz jego naprawienia powinno również się uzupełnić brakujący zestaw testów jednostkowych – jasne jest, że coś zostało w nich pominięte skoro błąd został zgłoszony.
Jestem pod wrażeniem talentu pedagogicznego. Najlepiej wytłumaczone w sieci. Byłbym szczęśliwy, gdybym mógł zwrócić się do Pana w razie jakiś pytań czy wątpliwości.
@Rafał:
Dzieki za mile slowa. Pytania najlepiej w formie komentarzy albo przez email…
Pozdrawiam
Piotr
Mam takie pytanie.Jak można użyć [BeforeScenario] i [AfterScenario] ? Kiedy Tworzę pod adnotacją klasę, aby zainicjalizować tam obiekt, dzięki czemu mógłbym wywołać funkcję, mam błąd przy starcie, a bez klasy nie będę mógł wywołać obiektu. Jak to zrobić ?
[BeforeScenario]
public class Open {
Obiekt zmienna = new obiekt();
public Otwórz(){
zienna.OtwórzPrzeglądarkę();
}
}
Pozdrawiam i z góry dziękuję
@Rafal:
Mozesz jasniej bo nie bardzo rozumiem przyklad?
Chodzi mi o to, że nie wiem jak użyć hooków [BeforeScenario] i [afterScenario]. Mógłbyś opisać jak to technicznie zaimplementować ? Widzę, że [BeforeScenario] nie może posiadać klasy i to przy obiektówce wydaje się ciężkie do przeskoczenia. Mógłbyś po prostu opisać jak to zrobić dokładnie ?
@Rafal:
Sorry za opoznienie – zycie przeszkadzalo:)
To jest atrybut dla statycznych metod, a nie klas, jak to pokazales w poprzednim przykladzie.
Przyklad:
[BeforeScenario()]
public static void BeforeWebScenario()
{
}
Dziękuję za odpowiedź 🙂 Jutro w pracy przetestuje i dam znać 🙂 A prowadzisz Piotrze może jakieś szkolenia ?
Wszystko działa jak należy. Fajnie jak byś kiedyś napisał post o “scope” – jak je wykorzystać w “Scenario Outline” przy danych po wprowadzeniu których inna akcja jest pożądaną asercją.
@Rafal:
Postaram sie cos napisac w przyszlosci ale narazie w kolejcie stoi c# 6.0 i NSB:)
A szkolen nie prowadze, preferuje aktywnosc blogowa i MSDN’nowa 🙂