To co teraz czytacie jest początkiem serii postów o tworzeniu testów jednostkowych z użyciem mocków (btw: zna ktoś polskie tłumaczenie tego słowa?). Jak na początek przystało – dziś krótkie wprowadzenie.
Testy jednostkowe badające poprawność zależności i integracji na poziomie pojedynczych klas wykorzystują specjalnie spreparowane, proste i na swój sposób “głupie” implementacje wymaganych przez dany komponent części systemu. Między innymi stąd tyle mówi się o Inversion of Control, czyli praktyce wymuszającej dostarczanie zależności danej klasie z zewnątrz, zdejmując z tejże klasy odpowiedzialność za samodzielne ich tworzenie.
Ale, żeby już na początku nie popaść w zamotane plątanie się po masie definicji, spójrzmy na przykład. Składać się na niego będą dwa elementy:
1) IAuthenticationService: interfejs definiujący operację uwierzytelnienia użytkownika
1: public interface IAuthenticationService 2: { 3: bool Authenticate(string userName, string password); 4: }
2) AccountController: komponent odpowiedzialny za odpowiednią reakcję na akcje użytkownika na stronie www, a konkretnie: kliknięcie przycisku “Log in” po wprowadzeniu danych niezbędnych do zalogowania (wzięte częściowo z domyślnie tworzonego szkieletu aplikacji ASP.NET MVC)
1: public class AccountController 2: { 3: private IAuthenticationService _authenticationService; 4: 5: public AccountController(IAuthenticationService authenticationService) 6: { 7: _authenticationService = authenticationService; 8: } 9: 10: public ActionResult LogOn(string userName, string password) 11: { 12: _authenticationService.Authenticate(userName, password); 13: // rest of the implementation
Najprostszy test jednostkowy w tej sytuacji powinien sprawdzić, czy akcja LogOn faktycznie prowadzi do logiki wykonującej czynności związane z uwierzytelnieniem użytkownika. W tym konkretnym przypadku sprowadza się to do wywołania metody Authenticate na posiadanej implementacji interfejsu IAuthenticationService i przekazaniu jej odpowiednich parametrów. Czy chcemy jednak teraz testować zachowanie systemu przy podaniu niepoprawnej pary username/pass? Albo przy niedostępnej bazie danych? Albo weryfikować w jakikolwiek inny sposób poprawność tej logiki? Nie! To nie leży w gestii kontrolera. Takie testy powinny znaleźć się w kodzie testującym wykorzystywaną podczas działania aplikacji implementację interfejsu.
I właśnie tutaj wykorzystamy możliwość dostarczenia w konstruktorze kontrolerowi takiej usługi uwierzytelniającej, jaka nam jest aktualnie najwygodniejsza. Tak wygląda test:
1: [Test] 2: public void LogOn_Authenticates() 3: { 4: var mockAuthService = new MockAuthenticationService(); 5: 6: var controller = new AccountController(mockAuthService); 7: string userName = "testUserName"; 8: string password = "testPassword"; 9: 10: controller.LogOn(userName, password); 11: 12: Assert.IsTrue(mockAuthService.AuthenticateWasCalled); 13: Assert.AreEqual(userName, mockAuthService.PassedUserName); 14: Assert.AreEqual(password, mockAuthService.PassedPassword); 15: }
A to jest nasz MOCK:
1: public class MockAuthenticationService : IAuthenticationService 2: { 3: public bool AuthenticateWasCalled; 4: public string PassedUserName; 5: public string PassedPassword; 6: 7: public bool Authenticate(string userName, string password) 8: { 9: PassedUserName = userName; 10: PassedPassword = password; 11: AuthenticateWasCalled = true; 12: 13: return true; 14: } 15: }
Co daje nam takie podejście? Otóż zalet jest wiele: testujemy zachowanie jednej konkretnej klasy – kontrolera – bez dotykania bazy danych, bez mieszania w logice faktycznie odpowiedzialnej za logowanie. Tworzymy ściśle kontrolowane warunki kompletnego odizolowania od całego systemu – i hulaj dusza. Badanie i testowanie odpowiedzialności konkretnej klasy w ten sposób jest wygodne i wolne od wielu uciążliwości, które towarzyszyłyby nam przy używaniu prawdziwej, działającej implementacji interfejsu uwierzytelniania.
Oczywiście sam fakt PRZETESTOWANIA wywołania metody nie daje nam w tym przypadku zbyt wiele korzyści – przecież gdyby ta metoda nie została wywołana, to system wywaliłby się już na pierwszej stronie, zatem i tak niepoprawne jego działanie wykrylibyśmy bardzo szybko. Siła takich testów leży moim zdaniem w dwóch miejscach:
– po pierwsze (tak, po PIERWSZE!): testy takie pomagają stworzyć “poprawny” (cokolwiek to znaczy) design architektury aplikacji; jeżeli coś się ciężko testuje, ponieważ wymaga dodatkowej konfiguracji/istnienia określonego katalogu/uruchomionej bazy danych/połączenia z internetem, a podstawowe czynności testowanego komponentu nie polegają właśnie na integracji z wymienionymi zależnościami, to COŚ ZROBILIŚMY ŹLE; łatwość napisania testów jednostkowych dla systemu gwarantuje (no, to może zbyt mocne słowo…) nam, że mamy poprawnie rozłożone odpowiedzialności pomiędzy klasami
– testy nie służą sprawdzeniu poprawności kodu w momencie ich pisania; przecież tworząc ten test doskonale zdawaliśmy sobie sprawę, że kontroler WYWOŁUJE metodę; czas poświęcony na napisanie tego testu nie był jednak czasem straconym, ponieważ regularne, automatyczne jego wykonywanie podczas ewolucji i rozwoju systemu gwarantuje nam, że podczas jakichś modyfikacji nikt nam tego zachowania nie zmieni; a jeśli stanie się inaczej, zostaniemy o tym niezwłocznie poinformowani poprzez niespełnione warunki testu; ba, jeśli inny programista zmieni to zachowanie bo “wydaje mu się że powinno być inaczej”, to od razu dostanie testowym obuchem po łbie i będzie się musiał jeszcze raz zastanowić, czy aby na pewno jego poczynania mają sens
Ale samo to jest tematem nawet nie na osobną notkę, ale całkiem pokaźną dyskusję…
Wróćmy do przykładu, który obejrzeliśmy powyżej. Wyobrażacie sobie ręczne tworzenie takiej klasy “mockującej” na potrzeby każdego testu? A testów może być “ile gwiazd na niebie, ile skwarek w kaszy”. To byłaby głupia, bezsensowna i niczym nieuzasadniona STRATA CZASU. Na szczęście z pomocą przychodzą tzw. “mocking frameworks” zdejmujące z naszych barków czynności związane z takimi pierdołami i oddające w nasze rency mechanizmy pozwalające na wiele wiele więcej niż tu zobaczyliśmy.
I właśnie o tym wkrótce. Będzie między innymi o tym jak krok po kroku stworzyć pierwszy test z mockami, o charakterystykach mocking frameworks, o różnych podejściach do pisania testów z wykorzystaniem mocków, o pojęciach takich jak stub, strict mock i dynamic mock. A później zagłębimy się w możliwości używanego przeze mnie od dawna RhinoMocks… Mi samemu ciężko przewidzieć w jak długi cykl to się rozrośnie, ale może być całkiem ciekawie, szczególnie jeśli w komentarzach pojawiać się będą cenne uwagi i spostrzeżenia. Jeśli zatem masz sugestię, propozycję tematu wartego poruszenia, chcesz podzielić się własnymi praktykami – śmiało!
A póki co… po raz już ósmy (oł jea!) wzywa mnie zew “Trylogii”, więc tymczasem borem lasem.
Fajny artykuł (naprawdę bardzo prosto tłumaczy rzeczy, które nie przychodzą tak łatwo) i arcyciekawie zapowiadający się cykl :) Oby tak dalej!
Mock to po angielsku atrapa :)
@Łukasz:
Dzięki. Sam pamiętam, że początki proste nie były, ale zdecydowanie warto się tego nauczyć:).
@kombain:
Pasuje, ale "Testy jednostkowe z wykorzystaniem obiektów-atrap" brzmi trochę głupio:) Zostanę przy "mock".
Polska wersja słowo mock to namiastka. Śmieszne, ale w sumie sensowne.
Polska wersja słowa mock to namiastka. Śmieszne, ale w sumie sensowne.
Moja sugestia: wyedytuj każdy z postów w cyklu i dodaj linka do następnej części.
@jj:
Na takie okazje zebralem wszystko w osobnym poscie: http://www.maciejaniserowicz.com/post/2009/09/29/Spis-tresci-Cykl-o-mock-objects-i-Rhino-Mocks.aspx
wiem, przez niego wchodziłem. Ot takie małe usprawnienie :) Ale widzę, że Twoje lenistwo jednak nie zna granic, no trudno :)