Czy jest jakaś rzeczywista wartość w jednostce testującej kontroler w ASP.NET MVC?

33

Mam nadzieję, że to pytanie daje kilka interesujących odpowiedzi, ponieważ jest to pytanie, które mnie trochę denerwowało.

Czy jest jakaś rzeczywista wartość w jednostce testującej kontroler w ASP.NET MVC?

Rozumiem przez to, że przez większość czasu (i nie jestem geniuszem), moje metody kontrolera są, nawet w ich najbardziej skomplikowanych, czymś takim:

public ActionResult Create(MyModel model)
{
    // start error list
    var errors = new List<string>();

    // check model state based on data annotations
    if(ModelState.IsValid)
    {
        // call a service method
        if(this._myService.CreateNew(model, Request.UserHostAddress, ref errors))
        {
            // all is well, data is saved, 
            // so tell the user they are brilliant
            return View("_Success");
        }
    }

    // add errors to model state
    errors.ForEach(e => ModelState.AddModelError("", e));

    // return view
    return View(model);
}

Większość ciężkich zadań jest wykonywana przez potok MVC lub bibliotekę usług.

Być może więc pytania mogą być następujące:

  • jaka byłaby wartość jednostki testującej tę metodę?
  • czy nie złamać na Request.UserHostAddressi ModelStatez NullReferenceException? Czy powinienem próbować się z nich kpić?
  • jeśli zmienię tę metodę w „pomocnika” wielokrotnego użytku (co prawdopodobnie powinienem, biorąc pod uwagę, ile razy to robię!), przetestowałbym to nawet, gdy warto przetestować głównie „potok”, który, przypuszczalnie, czy został przetestowany w ciągu swojego życia przez Microsoft?

Wydaje mi się, że moim celem jest to, że wykonanie poniższych czynności wydaje się całkowicie bezcelowe i niewłaściwe

[TestMethod]
public void Test_Home_Index()
{
    var controller = new HomeController();
    var expected = "Index";
    var actual = ((ViewResult)controller.Index()).ViewName;
    Assert.AreEqual(expected, actual);
}

Oczywiście, że mam obsesję na punkcie tego przesadnie bezcelowego przykładu, ale czy ktoś ma tutaj jakąś mądrość?

Nie mogę się doczekać ... Dzięki.

LiverpoolsNumber9
źródło
Myślę, że ROI (zwrot z inwestycji) w tym konkretnym teście nie jest wart wysiłku, chyba że masz nieskończony czas i pieniądze. Napisałbym testy, które Kevin wskazuje na sprawdzanie rzeczy, które są bardziej podatne na uszkodzenie lub pomogą ci w pewnej refaktoryzacji czegoś lub upewnieniu się, że propagacja błędów przebiega zgodnie z oczekiwaniami. Testy rurociągów w razie potrzeby można wykonać na poziomie bardziej globalnym / infrastruktury, a na poziomie poszczególnych metod będą miały niewielką wartość. Nie mówiąc, że nie mają żadnej wartości, ale „mało”. Więc jeśli w twoim przypadku zapewnia to dobre ROI, idź do niego, w przeciwnym razie najpierw złap większą rybę!
Mrchief,

Odpowiedzi:

18

Nawet dla czegoś tak prostego test jednostkowy będzie służył wielu celom

  1. Zaufanie, co zostało napisane, odpowiada oczekiwanemu wynikowi. Sprawdzenie, czy zwraca prawidłowy widok, może wydawać się trywialne, ale wynik jest obiektywnym dowodem na spełnienie wymagania
  2. Testy regresji. Jeśli metoda Create wymaga zmiany, nadal masz test jednostkowy dla oczekiwanego wyniku. Tak, dane wyjściowe mogą się zmieniać, co prowadzi do kruchego testu, ale nadal stanowi kontrolę przeciwko niezarządzanej kontroli zmian

W przypadku tej konkretnej czynności przetestuję następujące elementy

  1. Co się stanie, jeśli _myService ma wartość NULL?
  2. Co się stanie, jeśli _myService.Create zgłosi wyjątek, czy obsłuży określone?
  3. Czy pomyślne _myService.Create zwraca widok _Sukces?
  4. Czy błędy są propagowane do ModelState?

Zwróciłeś uwagę na sprawdzenie Żądania i Modelu dla NullReferenceException i myślę, że ModelState.IsValid zajmie się obsługą NullReference dla Modelu.

Egzekwowanie wniosku pozwala uchronić się przed żądaniem zerowym, które, jak sądzę, jest generalnie niemożliwe, ale może się zdarzyć podczas testu jednostkowego. W teście integracji pozwoliłby ci podać różne wartości UserHostAddress (Żądanie jest nadal danymi wejściowymi użytkownika, jeśli chodzi o kontrolę i należy je odpowiednio przetestować)

Kevin
źródło
Cześć Kevin, dzięki za poświęcenie czasu na odpowiedź. Zostawię na chwilę, aby sprawdzić, czy ktokolwiek inny wejdzie z czymkolwiek, ale jak dotąd twoje jest najbardziej logiczne / jasne.
LiverpoolsNumber9
Spifty Cieszę się, że to ci pomogło.
Kevin
3

Moje kontrolery są również bardzo małe. Większość „logiki” w kontrolerach jest obsługiwana przy użyciu atrybutów filtru (wbudowanych i odręcznych). Więc mój kontroler zwykle ma tylko kilka zadań:

  • Twórz modele z ciągów zapytań HTTP, wartości formularzy itp.
  • Wykonaj podstawowe sprawdzenie poprawności
  • Zadzwoń do moich danych lub warstwy biznesowej
  • Wygeneruj a ActionResult

Większość powiązań modelu jest wykonywana automatycznie przez ASP.NET MVC. DataAnnotations obsługuje także większość sprawdzania poprawności.

Nawet z tak małą ilością do przetestowania, nadal zazwyczaj je piszę. Zasadniczo sprawdzam, czy moje repozytoria są wywoływane i czy ActionResultzwracany jest właściwy typ. Mam wygodną metodę ViewResultsprawdzania, czy zwrócona jest właściwa ścieżka widoku, a model widoku wygląda tak, jak tego oczekuję. Mam inny do sprawdzenia, czy ustawiony jest odpowiedni kontroler / akcja RedirectToActionResult. Mam inne testy JsonResultitp. Itp.

Niefortunnym wynikiem podklasowania Controllerklasy jest to, że zapewnia ona wiele wygodnych metod, które wykorzystują HttpContextwewnętrznie. Utrudnia to jednostkowe przetestowanie kontrolera. Z tego powodu zazwyczaj umieszczam HttpContextwywołania -zależne za interfejsem i przekazuję ten interfejs do konstruktora kontrolera (używam rozszerzenia internetowego Ninject do tworzenia moich kontrolerów). W interfejsie tym zwykle umieszczam właściwości pomocnika w celu uzyskania dostępu do sesji, ustawień konfiguracji, IPrinciple i pomocników URL.

Wymaga to dużej staranności, ale myślę, że warto.

Travis Parks
źródło
Dziękujemy za poświęcenie czasu na odpowiedź, ale od razu 2 problemy. Po pierwsze, „metody pomocnicze” w testach jednostkowych są niebezpieczne. Po drugie: „sprawdź, jak nazywają się moje repozytoria” - czy masz na myśli iniekcję zależności?
LiverpoolsNumber9
Dlaczego metody wygody byłyby niebezpieczne? Mam BaseControllerTestsklasę, w której wszyscy mieszkają. Kpię z moich repozytoriów. Podłączam je za pomocą Ninject.
Travis Parks
Co się stanie, jeśli popełniłeś błąd lub błędne założenie pomocnika? Inną sprawą było to, że tylko test integracji (tj. End-to-end) może „przetestować”, czy wywoływane są nasze repozytoria. W teście jednostkowym i tak „odświeżysz” lub wyśmiejesz swoje repozytoria.
LiverpoolsNumber9
Przekazujesz repozytorium do konstruktora. Kpisz sobie z niego podczas testu. Upewnij się, że próbka działa zgodnie z oczekiwaniami. Pomocnicy prostu dekonstrukcji ActionResultcelu kontroli przekazywane adresy URL, modele itp
Travis Parks
Ok, dość uczciwie - nieco źle zrozumiałem, co miałeś na myśli, mówiąc „przetestuj, że moje repozytoria są nazywane”.
LiverpoolsNumber9
2

Oczywiście niektóre kontrolery są znacznie bardziej złożone, ale oparte wyłącznie na twoim przykładzie:

Co się stanie, jeśli myService zgłosi wyjątek?

Na marginesie.

Ponadto kwestionowałbym zasadność przekazywania listy przez referencję (nie jest to konieczne, ponieważ c # i tak przechodzi przez referencję, ale nawet jeśli nie była) - przekazywanie akcji errorAction (akcji), którą usługa może następnie wykorzystać do pompowania komunikatów o błędach do które można następnie obsłużyć w dowolny sposób (być może chcesz dodać go do listy, może chcesz dodać błąd modelu, a może chcesz go zalogować).

W twoim przykładzie:

zamiast błędów ref, na przykład do (string s) => ModelState.AddModelError ("", s).

Michał
źródło
Warto wspomnieć, że zakłada to, że Twoja usługa znajduje się w tej samej aplikacji, w przeciwnym razie pojawią się problemy z serializacją.
Michael
Usługa będzie w osobnej bibliotece dll. Ale i tak prawdopodobnie masz rację, jeśli chodzi o „ref”. Z drugiej strony nie ma znaczenia, czy myService zgłosi wyjątek. Nie testuję mojej usługi - przetestowałbym w niej metody osobno. Mówię o czystym testowaniu „jednostki” ActionResult z (prawdopodobnie) wyśmiewaną myService.
LiverpoolsNumber9
Czy masz mapowanie 1: 1 między usługą a kontrolerem? Jeśli nie, to czy niektóre kontrolery używają wielu zgłoszeń serwisowych? Jeśli tak, możesz przetestować te interakcje?
Michael
Nie. Pod koniec dnia metody serwisowe pobierają dane wejściowe (zwykle model widoku lub nawet ciągi / inty), „robią rzeczy”, a następnie zwracają wartość bool / błędy, jeśli są fałszywe. Nie ma „bezpośredniego” połączenia między kontrolerami a warstwą usług. Są całkowicie oddzielone.
LiverpoolsNumber9
Tak, rozumiem to, staram się zrozumieć model relacyjny między kontrolerami a warstwą usług - zakładając, że każdy kontroler nie ma odpowiedniej metody usługi, byłoby uzasadnione, że niektórzy kontrolery mogą potrzebować więcej niż jedna metoda usługi?
Michael