OK – mamy niektóre “odpowiedzialności” wyjęte do osobnych klas. Ale co dalej? Czy UsersController powinien sam, ot, tak sobie, tworzyć nowe instancje których aktualnie potrzebuje?
Nie!
Pójdę o krok dalej i powiem więcej: użycie słowa kluczowego “new” w kodzie aplikacji uznaję za anti-pattern. Howgh, rzekłem.
Jeżeli klasa potrzebuje innej klasy do działania, to ta zależność powinna być jawnie wyeksponowana w kodzie. Najlepiej przez parametry konstruktora. A jak już coś staje się zależnością, to “wypada” nałożyć na to coś interfejs (zaraz zobaczymy czemu). Po zmianach (tag demo3-start) kod wygląda następująco:
public interface IEmailValidator { bool Validate(string email); } public class EmailValidator : IEmailValidator { // ...
(https://github.com/maniserowicz/di-talk/blob/demo3-start/src/app/EmailValidator.cs)
public interface IActivationLinkGenerator { string GenerateLink(string token, string email); } public class ActivationLinkGenerator : IActivationLinkGenerator { // ...
(https://github.com/maniserowicz/di-talk/blob/demo3-start/src/app/ActivationLinkGenerator.cs)
public class UsersController { private readonly IEmailValidator _emailValidator; private readonly IActivationLinkGenerator _activationLinkGenerator; public UsersController( IEmailValidator emailValidator , IActivationLinkGenerator activationLinkGenerator ) { _emailValidator = emailValidator; _activationLinkGenerator = activationLinkGenerator; }
(https://github.com/maniserowicz/di-talk/blob/demo3-start/src/app/UsersController.cs)
Co ta zmiana oznacza? Że odpowiedzialność za tworzenie instancji klas wykorzystanych podczas przetwarzania żądania ląduje tam gdzie powinna: w kodzie infrastruktury!
public class WebServer { public void RegisterUser(string email) { var emailValidator = new EmailValidator(); var activationLinkGenerator = new ActivationLinkGenerator(); var controller = new UsersController(emailValidator, activationLinkGenerator); controller.RegisterUser(email); } }
(https://github.com/maniserowicz/di-talk/blob/demo3-start/src/app/WebServer.cs)
Przejdźmy do taga demo3.1, bo teraz… po co te interfejsy?
Trochę uproszczę. Między innymi po to, żebyśmy mogli przetestować naszą aplikację bez uruchamiania jej – dla mnie jest to niezmiernie istotne. Z pierwotnym kodem fakt wywalenia błędu w momencie podania niewłaściwego adresu e-mail najwygodniej byłoby przetestować… startując system i ręcznie wklepując adres, który WIEM że jest zły. To oznacza, że muszę pamiętać jakie reguły są stosowane przy walidacji oraz że muszę mieć jakiś UI. Teraz mogę napisać:
public class UserController_RegisterUser_Tests { readonly UsersController _controller; readonly IEmailValidator _emailValidator; string _email; public UserController_RegisterUser_Tests() { _emailValidator = Substitute.For<IEmailValidator>(); var linkGenerator = Substitute.For<IActivationLinkGenerator>(); _controller = new UsersController(_emailValidator, linkGenerator); _email = "email"; } void execute() { _controller.RegisterUser(_email); } [Fact] public void throws_when_email_not_valid() { _emailValidator.Validate(_email).Returns(false); Assert.Throws<ArgumentException>( () => execute() ); } }
Zamockowałem sobie walidację, na tym poziomie nie obchodzą mnie reguły świadczące o poprawności adresu.
Ale walidacja e-maila to nie największy problem. Problemem większym jest przetestowanie faktu poprawnego wysłania (bądź nie) wiadomości tym kanałem. Z obecnymi instrukcjami byłbym zmuszony do faktycznego wysłania maila i sprawdzania skrzynki, co niesie za sobą konieczność posiadania już na tym etapie odpowiedniej konfiguracji SMTP (lub postawienia sobie lokalnie jakiejś “fałszywki”). Ale teraz, widząc jakie to proste, mogę wysyłacza wiadomości schować za interfejsem, przekazać jako zależność i zamockować. Wsio.
Gdzie jesteśmy? W punkcie, w którym oprócz testów czysto jednostkowych możemy zacząć pisać również testy interakcji pomiędzy poszczególnymi komponentami. Tutaj należy zachować szczególną ostrożność, bo mockowanie jest często oznaką nieidealnej architektury (chociaż zauważcie, że nigdzie nie twierdziłem, że przedstawiany kod jest “dobrą architekturą”) lub gorszego niż perfekcyjne rozplanowania modelu i odpowiedzialności między klasami. Dodatkowo do rozważenia pozostaje kwestia “korzystać z bibliotek do mockowania czy nie”, ale to dyskusja na inny raz.
DI: IoC & explicit dependencies & interfaces | Maciej Aniserowicz o programowaniu…
Dziękujemy za dodanie artykułu – Trackback z dotnetomaniak.pl…
Dlaczego ” mockowanie jest często oznaką nieidealnej architektury” ? Przecież predzej czy później cos wymockować trzeba – wkońcu, żeby trzymać się SRP trzeba część logiki wyrzucić do innej klasy/modułu (tak mi się wydaje :P).
fex,
Dodanie mocków do testów od razu je komplikuje, dlatego też staram się tak projektować komponenty żeby takich sytuacji uniknąć. Pomagają wspomniane już wcześniej “mikrokontrakty” ( http://www.maciejaniserowicz.com/2014/02/03/o-mikro-kontraktach/ ) – dzięki nim mam sporo oderwanych od siebie klas, które są “orkiestrowane” (z braku lepszego słowa) w jednej nadrzędnej klasie. Testy tej nadrzędnej klasy faktycznie zawierają sporo mocków, ale często jedyną jej odpowiedzialnością jest wywołanie metod w opowiedniej kolejności, więc czasami wystarczy jeden test potwierdzający wykonanie całego “flow”.
Maćku, dlaczego przeniosłeś odpowiedzialność za rozwiązanie zależności do klasy Web Server, zamiast użyć jakiejś biblioteki do DI?
markone,
Co nagle to po diable, cierpliwości ;)
Czy, aby na pewno trzeba tutaj wstrzykiwać EmailValidator? Nie będą istnieć inne walidatory i bardzo łatwo sprawić, aby w teście prawdziwy Validator zwracał true/false. Wystarczy przekazać poprawny/błędny email, a wiemy jakie to są. (EmailValidator jest testowany, więc wiemy, że działa poprawnie) Pewnie kiedyś użyłbym stuba, jak Ty, ale ostatnio się zastanawiam. Beck i Fowler mówili, że nie używają mock’ów/stubów.
Wiem że Beck i Fowler tak mówili i też staram się redukować używanie mocków (chociaż nie dlatego że oni mówili że nie używają). Jednak w tym przypadku implementacji “prawdziwej” nie chcę użyć, ponieważ wtedy zmiana implementacji walidatora rozwaliłaby mi testy kontrolera, a to się stać nie powinno.
Niezależnie od odpowiedzi na pytanie “używać mocków czy nie” – IEmailValidator i tak powinien być wstrzykiwany, pozostaje jedynie kwestia czy w teście wstrzykiwać mock czy prawdziwy obiekt.
W jaki sposób zmiana implementacji walidatora miała by zepsuć testy? Jedynie w przypadku zmiany interfejsu musisz zmodyfikować testy. Ale to jest konieczne, czy używasz stub-a, czy nie. A jeżeli ten kontroler ma N metod, a tylko ta jedna wymaga walidatora? Nadal wstrzykujesz?
Piotr, rozważ taką systuację. Nie wstrzykujemy walidatora, a w teście tworzymy odpowiedni mail tak jak napisałeś. W przypadku zmiany logiki walidacji maila (lub refactoringu, np. wprowadzenie błędu) nie przeszłyby dwa testy:
– test dla EmailValidator,
– test dla UsersController.
Tutaj coś jest nie tak. Dlaczego test kontrolera nie przechodzi, skoro działa on prawdiłowo? Kożystając z fake-owego walidatora (jak w przykładzie na tym blogu), takiej sytuacji nie będzie.
Piotr,
Mad odpowiedział na pytanie “dlaczego zepsują się testy kontrolera” i dlaczego tak być nie powinno.
Co do wstrzykiwania jeśli tylko jedna metoda kontrolera potrzebuje danej zależności: tak, tak, tak. Podsumowanie kodu po zmianach napiszę na zakończenie całego cyklu.
Chociaż już teraz mogę zdradzić, że coś takiego jak “kontroler” jest moim zdaniem sztuczne i niepotrzebne. Nie widzę przeciwwskazań aby każdy kontroler miał tylko jedną akcję. Albo żeby się kontrolerów całkowicie pozbyć.
Ja mam inne pytanie:
Dlaczego właściwie EmailValidator nie jest klasą statyczną? Przecież on nie posiada czegoś takiego jak stan. Czy odpowiedzią jest “bo miał być prosty przykład obiektu”, czy to też byłby jakiś anty- pattern, o którym nie wiem?
Sprzet,
To byłby antipattern, chociaż nie wiem czy taki o którym nie wiesz. Klasy statyczne z logiką to zło. One powodują to, z czym walczy dependency injection – niekontrolowane i niezarządzalne zależności między komponentami. To one są często powodem tego, że jedna mała zmiana prowadzi do całej lawiny bugów w systemie, nawet w teoretycznie niepowiązanych miejscach.
Dodatkowo, o czym m.in. traktuje ten wpis: użycie klasy statycznej uniemożliwiałoby efektywne przetestowanie kontrolera.