Jak powszechnie wiadomo – wielką zaletą wzorca MVC jest umożliwienie testowania jednostkowego logiki “wyciągniętej” z klas odpowiedzialnych za interakcję z użytkownikiem. Swego czasu śledziłem w internecie dyskusje na temat “Jak testować kontrolery, aby możliwie najbardziej odizolować je od reszty aplikacji”. O to przecież chodzi w Unit Testing…
Do rzeczy.
Przedstawienie problemu
Kontroler pełni rolę pośrednika pomiędzy widokiem i modelem (View & Model). Najprostszy przykład:
1: public class FooController : ControllerBase 2: { 3: IFooView _view; 4: FooModel _model; 5: 6: public FooController(IFooView view) 7: { 8: _view = view; 9: _model = new FooModel(); 10: }
Jeżeli chodzi o normalne wykorzystanie tej klasy to wszystko jest ok. Zgodnie z założeniami wzorca widok tworzy instancję kontrolera, przekazując mu jednocześnie referencję do samego siebie. I niczym się dalej nie przejmując.
Pisząc testy jednostkowe można jednak odczuć pewien niedosyt. Biblioteka testowa powinna mieć możliwość pełniejszej konfiguracji kontrolera. Co zatem robimy? Dodajemy kolejny konstruktor, co wyposaży nas w moc full-zastosowania wzorca o jakże dumnie brzmiącej nazwie Dependency Injection.Teraz nasz kontroler wygląda tak:
1: public FooController(IFooView view) 2: : this(view, new FooModel()) 3: { 4: } 5: 6: public FooController(IFooView view, FooModel model) 7: { 8: _view = view; 9: _model = model; 10: }
Super – na tym etapie możemy karmić kontroler czym nam się żywnie podoba. Pojawia się jednak nowy problem: właśnie umożliwiliśmy klasom widoków robienie tego samego! Oczywiście można opatrzyć drugi konstruktor odpowiednim komentarzem (“FOR TESTING PURPOSES ONLY!!!”), ale czy nie lepiej całkiem wyeliminować taką ewentualność?
Moja propozycja
Wykorzystajmy atrybut InternalsVisibleTo. Jego zadanie to udostępnienie konkretnym – i tylko tym – bibliotekom elementów z modyfikatorem widoczności internal. Po kolei:
1) oznaczamy kłopotliwy konstruktor jako internal, ukrywając go w ten sposób przed bibliotekami widoków
internal FooController(IFooView view, FooModel model)
2) bibliotekę zawierającą kontrolery ozdabiamy odpowiednim atrybutem “zaprzyjaźniając” ją tym samym z dllką zawierającą testy:
[assembly: InternalsVisibleTo("Controllers.Tests")]
3) w testach wykorzystujemy ów pełny konstruktor, podczas gdy dla widoków jest on nadal niedostępny:
FooController controller = new FooController(mockView, mockModel);
Finito.
Moim zdaniem widok nie powinnien inicjalizować kontrolera, dlatego, że doprowadzi to do pewnych komplikacji w skalowalności systemu. Moim zdaniem to Application Controller powinien być odpowiedzialny za uruchomienie Controllera, zaś widok będzie utworzony w ramach inicijalizacji Application Shell. Stworzenie takiego rozwiązania może być problematyczne w przydaku aplikacji ASP.NET, gdzie nie zmieniając HttpHandlera nie jesteśmy w stanie zmienić przepiegu tworzenia obiektów(ratunkiem tutaj jest ASP.NET MVC lub MonoRail). W przypdaku aplikacji ASP.NET warto spojrzeć na Web Client Software Factory, gdzie HttpModule jest odpowiedzialny za tworzenie Controllera(Presentera). Takie podejście umożliwi nam wyseperowanie odpowiedzialności tworzenia obiektów(Controllera) z poziomu widoku. W celu tworzenia obiektów warto wykorzystać Inversion Of Control container, ja preferuje Windsor Container. Takie podejście całkowicie wyeliminuje, korzystanie z wszelkiego rodzaju inicjalizacji obiektów, co wpływa na luźniejsze sprżerzenie klas.