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:
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.
“W przypadku jakiś zadań niskopoziomowych”
powinno być
“W przypadku jakiCHś zadań niskopoziomowych”
To są dwa różne słowa!