Code Review: Kilka pułapek ze SpecFlow

Specflow jest według mnie znakomitym framework’iem do pisania testów BDD. Bardzo często dodawanie nowych scenariuszy sprowadza się do przekopiowania przypadków użycia z user story. Oczywiście wymaga to trochę praktyki, ponieważ definiowanie za pomocą GWT na początku może wydawać się nienaturalne.

Na blogu o podstawach SpecFlow pisałem już wiele razy. Dzisiaj chciałbym pokazać pewną pułapkę, na którą w przypadku wielu scenariuszy trzeba uważać. Wiele osób narzeka, że praca ze SpecFlow jest trudna dla skomplikowanych projektów. Moim zdaniem również łatwo popełnić błędy i po pewnym czasie zestaw testów jest trudny w utrzymaniu.

Załóżmy, że tworzymy następujący plik ze scenariuszem (proszę nie zwracać uwagi na sens i opis testu Uśmiech):

Feature: NewPost Jakis opis tutaj... Scenario: Create a new post Given I have logged into CMS When I press the create a post button Then article should be created.

Wygenerowane kroki testu, będą wyglądać tak:

[Binding] public class NewPostSteps { [Given(@"I have logged into CMS")] public void GivenIHaveLoggedIntoCMS() { ScenarioContext.Current.Pending(); } [When(@"I press the create a post button")] public void WhenIPressTheCreateAPostButton() { ScenarioContext.Current.Pending(); } [Then(@"article should be created\.")] public void ThenArticleShouldBeCreated_() { ScenarioContext.Current.Pending(); } }

Wszystko fajnie, mamy trzy wygenerowane metody dla GWT. Następnie, być może po kilku miesiącach, dodajemy analogiczny scenariusz, ale dla edycji postów:

Feature: EditPost Jakis opis tutaj... Scenario: Edit a post Given I have logged into CMS When I press the edit a post button Then article should be updated.

Po wygenerowaniu kroków dostaniemy:

[Binding] public class EditPostSteps { [When(@"I press the edit a post button")] public void WhenIPressTheEditAPostButton() { ScenarioContext.Current.Pending(); } [Then(@"article should be updated\.")] public void ThenArticleShouldBeUpdated_() { ScenarioContext.Current.Pending(); } }

Na początku może wydawać się to dziwne. Nie został wygenerowany krok Given. W Specflow kroki są globalne i w naszym przypadku given będzie współdzielony. Zwróćmy uwagę, że “Given I have logged into CMS” jest takie same dla obydwu testów. Specflow automatycznie wykryje to i nie będzie generował duplikatu w EditPostSteps. Ma to sens, jeśli faktycznie krok jest taki sam. Często kroki o takiej samej nazwie, mają kompletnie inny kod, w zależności od kontekstu. Z tego względu należy być uważnym i mieć świadomość tego.

W naszym przypadku, ma sens współdzielenie given, bo inicjalizacja będzie taka sama. Niestety,  aktualny kod jest brzydki, ponieważ Given dla EditPostSteps, znajduje się w NewPostSteps. Z tego względu lepiej przenieść to do osobnej klasy, np. CmsSteps:

[Binding] public class CmsSteps { [Given(@"I have logged into CMS")] public void GivenIHaveLoggedIntoCMS() { } }

Rozwiązanie jest od razu czytelniejsze. CmsSteps może zawierać kroki współdzielone przez inne scenariusze. Wcześniejsza sytuacja, gdzie jeden plik scenariusza zawierał kroki dla innych był zdecydowanie nieelegancki.

Pozostaje jednak kolejna zagadka, jak współdzielić stan między różnymi klasami. W końcu Given coś inicjalizuje, co potem powinno być dostępne w EditPostSteps oraz NewPostSteps.

Najprostszym rozwiązaniem jest użycie ScenarioContext:

[Binding] public class CmsSteps { [Given(@"I have logged into CMS")] public void GivenIHaveLoggedIntoCMS() { ScenarioContext.Current["text"] = "logged in"; } } [Binding] public class EditPostSteps { [When(@"I press the edit a post button")] public void WhenIPressTheEditAPostButton() { string text = ScenarioContext.Current["text"].ToString(); } [Then(@"article should be updated\.")] public void ThenArticleShouldBeUpdated_() { ScenarioContext.Current.Pending(); } }

Niestety powstałe w taki sposób testy będą wcześniej czy później trudne w utrzymaniu. ScenarioContext to globalny kontener i na dodatek, dane przekazuje się za pomocą słownika (“text”). Zmiana w jednym miejscu, spowoduje problem w innym. Z tego względu zawsze lepiej tworzyć wrappery. Może mieć to formę następującej klasy:

public class CmsLoginPage { public string Login { get { return ScenarioContext.Current["Login"].ToString(); } set { ScenarioContext.Current["Login"] = value; } } public string Password { get { return ScenarioContext.Current["Password"].ToString(); } set { ScenarioContext.Current["Password"] = value; } } }

Odradzam bezpośrednie modyfikowanie kontekstu w krokach testu. Zawsze lepiej tworzyć właściwość, która opakowuje operacje na słowniku.

Mój preferowany sposób to dependency injection. Najpierw tworzę klasę z informacjami, które chce przekazać z jednego kroku (klasy) do drugiej:

public class CmsLoginPage { public string Login { get; set; } public string Password { get; set; } }

Następnie za pomocą konstruktora przekazuje referencję:

[Binding] public class CmsSteps { private readonly CmsLoginPage _loginPage; public CmsSteps(CmsLoginPage loginPage) { _loginPage = loginPage; } [Given(@"I have logged into CMS")] public void GivenIHaveLoggedIntoCMS() { _loginPage.Login = "piotr"; _loginPage.Password = "password"; } }

SpecFlow już o to zadba, aby została ona przekazana do jakichkolwiek innych klas:

[Binding] public class EditPostSteps { private readonly CmsLoginPage _loginPage; public EditPostSteps(CmsLoginPage loginPage) { _loginPage = loginPage; } [When(@"I press the edit a post button")] public void WhenIPressTheEditAPostButton() { string login = _loginPage.Login; string passwsord = _loginPage.Password; } [Then(@"article should be updated\.")] public void ThenArticleShouldBeUpdated_() { ScenarioContext.Current.Pending(); } }

W kolejnych postach mam zamiar również poruszać temat SpecFlow. Oczywiście jeśli wymagania nie są przygotowywane przez osoby z biznesu (np. przez PO), to nie docenimy prawdziwej przydatności framework’u. W takich przypadkach warto również rozważyć narzędzia typu SpecFor, które często są preferowane, gdy to programiści (bez udziału osób nietechnicznych) piszą scenariusze.

Leave a Reply

Your email address will not be published.