Dość sporo teorii mamy za sobą, nadeszła więc pora na praktyczne przykłady. Tym razem spojrzymy na konfigurację zachowania stubów w zależności od parametrów przekazywanych do ich metod (oraz analizę wartości przekazanych do mocków przez testowane obiekty, co jest scenariuszem bardzo podobnym technicznie).
Konkretne wartości
W poniższym przypadku konfigurujemy zachowanie authenticationService tylko dla jednej pary parametrów o wartościach przekazanych w zmiennych userName i password. Każde inne wartości parametrów spowodują domyślnie zachowanie stuba, czyli (jak dowiedzieliśmy się tu) zwrócenie domyślnej wartości dla typu bool => false.
1: authenticationService.Stub(x => x.Authenticate(userName, password)).Return(true);
Dowolne wartości
Możemy również zastosować pewien myk powodujący, że uzyskamy symulację zawsze poprawnego uwierzytelnienia. Aby to osiągnąć musimy poinstruować RhinoMocks, aby parametry wywołania metody nie były brane pod uwagę:
1: authenticationService.Stub(x => x.Authenticate(null, null)).IgnoreArguments().Return(true);
Jak widać służy do tego metoda IgnoreArguments() konfigurująca odpowiednie zachowanie. Nieważne co przekażemy w parametrach podczas definiowania zachowania, i tak podczas weryfikacji wartości te zostaną pominięte. Zwyczajowo jednak w takich sytuacjach przekazuje się null (bądź odpowiednie wartości domyślne w przypadku typów nie-nullowalnych).
Poniżej ta sama instrukcja, ale zastosowana w momencie weryfikacji wykonania metody (czyli fazy Assert), a nie konfiguracji stuba (czyli fazy Act). Wszystkie dalsze przykłady przepisuje się analogicznie, po prostu warunki przekazujemy jako odpowiednie wyrażenie lambda w drugim parametrze metody AssertWasCalled:
1: authenticationService.AssertWasCalled(x => x.Authenticate(null, null), opts => opts.IgnoreArguments());
Zdefiniowane ograniczenia
Omawiany mechanizm byłby bardzo ubogi, gdyby nie oferował większej swobody w sprawdzaniu wartości parametrów. Na szczęście tak nie jest i mamy do dyspozycji cały wachlarz ograniczeń jakie możemy nałożyć na parametr. Na przykład poniższa linijka kodu definiuje poprawne uwierzytelnienie dla dowolnego loginu z hasłem nie będącym nullem:
1: authenticationService.Stub(x => x.Authenticate(null, null)).Constraints(Is.Anything(), Is.NotNull()).Return(true);
Po prostu wywołujemy metodę Constraints() z tyloma ograniczeniami, ile mamy parametrów w konfigurowanej akcji.
Centralnym miejscem zbierającym zdefiniowane ograniczenia dla pojedynczych wartości jest statyczna klasa Rhino.Mocks.Constraints.Is, służąca za punkt wyjściowy do zabawy z tym mechanizmem.
Jeżeli natomiast chcemy zastosować ograniczenie ocierające się o kolekcje, czyli mamy parametr będący kolekcją bądź sprawdzamy poprawność parametru względem kolekcji, rozpoczniemy od klasy Rhino.Mocks.Constraints.List. Analogicznie: jeżeli obszarem naszych zainteresowań są napisy, to odpowiednie ograniczenie udostępni nam Rhino.Mocks.Constraints.Text.
Wykorzystajmy tą wiedzę w praktyce: kod poniżej definiuje poprawne uwierzytelnienie dla loginów zawartych w tablicy validLogins z hasłem zgodnym z wyrażeniem regularnym (minimum 6 liter lub cyfr):
1: string[] validLogins = new[] {"abc", "def"}; 2: authenticationService.Stub(x => x.Authenticate(null, null)) 3: .Constraints(List.OneOf(validLogins), Text.Like("[\\w|\\d]{6,}")).Return(true);
Dzięki przeciążonym operatorom ograniczenia możemy łączyć, jak przy konfigurowaniu poprawnego uwierzytelnienia dla loginów rozpoczynających się od “a” i kończących się na “z”:
1: authenticationService.Stub(x => x.Authenticate(null, null)) 2: .Constraints(Text.StartsWith("a") & Text.EndsWith("z"), Is.Anything()).Return(true);
Własne ograniczenia
Może się zdarzyć, że ograniczenia nie są dla nas wystarczające. Wówczas na ratunek przychodzi nam najbardziej ogólne i elastyczne z nich przyjmujące wyrażenie lambda wykonywane dla wartości parametru. Z jego pomocą jesteśmy w stanie zasymulować poprawne logowanie dla loginu, który po ucięciu na końcach białych znaków okaże się mieć długość większą niż 8:
1: authenticationService.Stub(x => x.Authenticate(null, null)) 2: .Constraints(Is.Matching<string>(s => s.Trim().Length > 8), Is.Anything()).Return(true);
Takie rozwiązanie daje nam możliwość wplecenia w ten proces dowolnej logiki.
Rozszerzanie Rhino Mocks
Poza wykorzystaniem przedstawionej konstrukcji Is.Matching mamy jeszcze jedną drogę do zastosowania własnoręcznie zdefiniowanych, dowolnie skomplikowanych zasad. Uzyskamy to poprzez napisanie własnej klasy dziedziczącej z AbstractConstraint, analogicznie do ograniczeń już istniejących w bibliotece. Zauważyć bowiem należy, że zarówno metoda Matching<T>() jak i wszystkie pozostałe “skrótowe” metody pozwalające na badanie parametrów zwracają instancje klas dziedziczących z AbstractConstraint. Dla Is.Matching<T>() jest to Rhino.Mocks.Consontraints.PredicateConstraint<T>, dla List.OneOf() – Rhino.Mocks.Consontraints.OneOf, dla Text.EndsWith – Rhino.Mocks.Constraints.EndsWith. I tak dalej.
Napiszmy więc, wzorując się na podejrzanej Reflectorem implementacji EndsWith, własne parametryzowane rozszerzenie pozwalające na to samo co przykład zastosowany wyżej:
1: public class TrimmedLongerThan : AbstractConstraint 2: { 3: private readonly int _minLength; 4: 5: public TrimmedLongerThan(int minLength) 6: { 7: _minLength = minLength; 8: } 9: 10: public override bool Eval(object obj) 11: { 12: return (obj != null) && obj.ToString().Trim().Length >= _minLength; 13: } 14: 15: public override string Message 16: { 17: get { return "trimmed is longer than " + _minLength; } 18: } 19: } 20:
A zastosujemy to tak:
1: authenticationService.Stub(x => x.Authenticate(null, null)) 2: .Constraints(new TrimmedLongerThan(8), Is.Anything()).Return(true);
Przedstawione mechanizmy oferują nam całkowitą kontrolę nad wartościami parametrów metod w testach jednostkowych. Następnym razem przyjrzymy się jakie ciekawe zachowania możemy przypisać wywoływanym w testach metodom.