Iteracja 5 — Tworzenie testów jednostkowych (C#)

autor: Microsoft

Pobierz kod

W piątej iteracji ułatwiamy konserwację i modyfikowanie aplikacji przez dodawanie testów jednostkowych. Wyśmiewamy nasze klasy modelu danych i tworzymy testy jednostkowe dla naszych kontrolerów i logiki walidacji.

Tworzenie aplikacji MVC do zarządzania kontaktami ASP.NET (C#)

W tej serii samouczków utworzymy całą aplikację do zarządzania kontaktami od początku do końca. Aplikacja Contact Manager umożliwia przechowywanie informacji kontaktowych — nazw, numerów telefonów i adresów e-mail — dla listy osób.

Tworzymy aplikację za pośrednictwem wielu iteracji. Wraz z każdą iteracją stopniowo ulepszamy aplikację. Celem tego podejścia iteracji wielokrotnej jest umożliwienie zrozumienia przyczyny każdej zmiany.

  • Iteracja #1 — tworzenie aplikacji. W pierwszej iteracji tworzymy Menedżera kontaktów w najprostszy możliwy sposób. Dodajemy obsługę podstawowych operacji bazy danych: tworzenie, odczyt, aktualizowanie i usuwanie (CRUD).

  • Iteracja #2 — sprawia, że aplikacja wygląda ładnie. W tej iteracji poprawiamy wygląd aplikacji, modyfikując domyślną stronę wzorcową widoku MVC ASP.NET oraz kaskadowy arkusz stylów.

  • Iteracja 3 — dodawanie walidacji formularza. W trzeciej iteracji dodamy podstawową walidację formularza. Uniemożliwiamy użytkownikom przesyłanie formularza bez wypełniania wymaganych pól formularza. Weryfikujemy również adresy e-mail i numery telefonów.

  • Iteracja #4 — luźno sprzężenie aplikacji. W tej czwartej iteracji korzystamy z kilku wzorców projektowych oprogramowania, aby ułatwić konserwację i modyfikowanie aplikacji Contact Manager. Na przykład refaktoryzujemy naszą aplikację, aby używać wzorca repozytorium i wzorca wstrzykiwania zależności.

  • Iteracja 5 — tworzenie testów jednostkowych. W piątej iteracji ułatwiamy konserwację i modyfikowanie aplikacji przez dodawanie testów jednostkowych. Wyśmiewamy nasze klasy modelu danych i tworzymy testy jednostkowe dla naszych kontrolerów i logiki walidacji.

  • Iteracja 6 — używanie programowania opartego na testach. W tej szóstej iteracji do naszej aplikacji dodamy nowe funkcje, pisząc najpierw testy jednostkowe i pisząc kod względem testów jednostkowych. W tej iteracji dodajemy grupy kontaktów.

  • Iteracja #7 — dodawanie funkcji Ajax. W siódmej iteracji poprawiamy czas odpowiedzi i wydajność naszej aplikacji, dodając obsługę Ajax.

Ta iteracja

W poprzedniej iteracji aplikacji Contact Manager refaktoryzowaliśmy aplikację, aby była luźniej sprzężona. Aplikacja została rozdzielona na odrębne warstwy kontrolera, usługi i repozytorium. Każda warstwa współdziała z warstwą pod nią za pośrednictwem interfejsów.

Refaktoryzowaliśmy aplikację, aby ułatwić konserwację i modyfikowanie aplikacji. Jeśli na przykład musimy użyć nowej technologii dostępu do danych, możemy po prostu zmienić warstwę repozytorium bez dotykania kontrolera lub warstwy usługi. Dzięki luźnej połączeniu menedżera kontaktów wprowadziliśmy aplikację bardziej odporną na zmiany.

Ale co się stanie, gdy musimy dodać nową funkcję do aplikacji Contact Manager? A co się stanie, gdy naprawimy usterkę? Smutna, ale dobrze sprawdzona prawda pisania kodu polega na tym, że za każdym razem, gdy dotkniesz kodu, stwarzasz ryzyko wprowadzenia nowych usterek.

Na przykład pewnego dnia menedżer może poprosić Cię o dodanie nowej funkcji do Menedżera kontaktów. Chce dodać obsługę grup kontaktów. Chce, aby umożliwić użytkownikom organizowanie swoich kontaktów w grupach, takich jak Przyjaciele, Biznes itd.

Aby zaimplementować tę nową funkcję, należy zmodyfikować wszystkie trzy warstwy aplikacji Contact Manager. Należy dodać nowe funkcje do kontrolerów, warstwy usługi i repozytorium. Zaraz po rozpoczęciu modyfikowania kodu ryzykujesz niezgodność funkcji, które wcześniej działały.

Refaktoryzacja aplikacji na oddzielne warstwy, tak jak w poprzedniej iteracji, była dobrą rzeczą. To było dobre, ponieważ umożliwia nam wprowadzanie zmian w całych warstwach bez dotykania reszty aplikacji. Jeśli jednak chcesz ułatwić konserwację i modyfikowanie kodu w obrębie warstwy, należy utworzyć testy jednostkowe dla kodu.

Test jednostkowy służy do testowania pojedynczej jednostki kodu. Te jednostki kodu są mniejsze niż całe warstwy aplikacji. Zazwyczaj test jednostkowy służy do sprawdzania, czy określona metoda w kodzie zachowuje się w oczekiwany sposób. Można na przykład utworzyć test jednostkowy dla metody CreateContact() uwidocznionej przez klasę ContactManagerService.

Testy jednostkowe aplikacji działają tak samo jak sieć bezpieczeństwa. Za każdym razem, gdy modyfikujesz kod w aplikacji, możesz uruchomić zestaw testów jednostkowych, aby sprawdzić, czy modyfikacja przerywa istniejące funkcje. Testy jednostkowe umożliwiają bezpieczne modyfikowanie kodu. Testy jednostkowe sprawiają, że cały kod w aplikacji jest bardziej odporny na zmiany.

W tej iteracji dodajemy testy jednostkowe do naszej aplikacji Contact Manager. Dzięki następnej iteracji możemy dodać grupy kontaktów do naszej aplikacji bez obaw o przerywanie istniejących funkcji.

Uwaga

Istnieją różne struktury testowania jednostkowego, w tym NUnit, xUnit.net i MbUnit. W tym samouczku używamy platformy testów jednostkowych zawartych w programie Visual Studio. Można jednak równie łatwo użyć jednej z tych alternatywnych struktur.

Co jest testowane

W idealnym świecie cały kod będzie objęty testami jednostkowym. W idealnym świecie miałbyś idealną siatkę bezpieczeństwa. Możesz zmodyfikować dowolny wiersz kodu w aplikacji i natychmiast wiedzieć, wykonując testy jednostkowe, czy zmiana przerwała istniejącą funkcjonalność.

Jednak nie żyjemy w idealnym świecie. W praktyce podczas pisania testów jednostkowych koncentrujesz się na pisaniu testów dla logiki biznesowej (na przykład logiki weryfikacji). W szczególności nie należy pisać testów jednostkowych dla logiki dostępu do danych ani logiki widoku.

Aby być przydatnym, testy jednostkowe muszą być wykonywane bardzo szybko. Można łatwo gromadzić setki (a nawet tysiące) testów jednostkowych dla aplikacji. Jeśli testy jednostkowe trwają długo, należy unikać ich wykonywania. Innymi słowy, długotrwałe testy jednostkowe są bezużyteczne do codziennych celów kodowania.

Z tego powodu zwykle nie są zapisywane testy jednostkowe kodu, który wchodzi w interakcję z bazą danych. Uruchamianie setek testów jednostkowych dla aktywnej bazy danych byłoby zbyt powolne. Zamiast tego wyśmiewasz bazę danych i piszesz kod, który współdziała z pozorną bazą danych (omawiamy pozorowanie poniższej bazy danych).

Z podobnej przyczyny zazwyczaj nie są zapisywane testy jednostkowe dla widoków. Aby przetestować widok, należy uruchomić serwer internetowy. Ponieważ tworzenie serwera internetowego jest stosunkowo powolnym procesem, tworzenie testów jednostkowych dla widoków nie jest zalecane.

Jeśli widok zawiera skomplikowaną logikę, rozważ przeniesienie logiki do metod pomocnika. Testy jednostkowe dla metod pomocnika, które są wykonywane bez uruchamiania serwera internetowego.

Uwaga

Podczas pisania testów logiki dostępu do danych lub logiki wyświetlania nie jest dobrym pomysłem podczas pisania testów jednostkowych, te testy mogą być bardzo przydatne podczas kompilowania testów funkcjonalnych lub integracji.

Uwaga

ASP.NET MVC jest aparatem Web Forms View. Aparat widoków Web Forms jest zależny od serwera internetowego, ale inne aparaty widoków mogą nie być.

Używanie makiety struktury obiektów

Podczas kompilowania testów jednostkowych prawie zawsze trzeba korzystać z platformy Mock Object. Platforma Mock Object umożliwia tworzenie makietów i wycinków dla klas w aplikacji.

Na przykład można użyć platformy Mock Object, aby wygenerować pozorną wersję klasy repozytorium. W ten sposób można użyć pozornej klasy repozytorium zamiast rzeczywistej klasy repozytorium w testach jednostkowych. Użycie makiety repozytorium umożliwia uniknięcie wykonywania kodu bazy danych podczas wykonywania testu jednostkowego.

Program Visual Studio nie zawiera platformy Mock Object. Istnieje jednak kilka komercyjnych i open source platform obiektów mock dostępnych dla platformy .NET Framework:

  1. Moq — ta struktura jest dostępna w ramach licencji open source BSD. Możesz pobrać plik Moq z witryny https://code.google.com/p/moq/.
  2. Mocks Rhino — ta struktura jest dostępna w ramach licencji BSD open source. Możesz pobrać Mocks Rhino z .http://ayende.com/projects/rhino-mocks.aspx
  3. Isolator typemock — jest to platforma komercyjna. Możesz pobrać wersję próbną z witryny http://www.typemock.com/.

W tym samouczku postanowiłem użyć Moq. Można jednak równie łatwo użyć Mocks Rhino lub Typemock Isolator, aby utworzyć obiekty Mock dla aplikacji Contact Manager.

Przed rozpoczęciem korzystania z narzędzia Moq należy wykonać następujące kroki:

  1. .
  2. Przed rozpakowywaniem pobierania upewnij się, że kliknij go prawym przyciskiem myszy i kliknij przycisk z etykietą Odblokuj (zobacz Rysunek 1).
  3. Rozpakuj pobieranie.
  4. Dodaj odwołanie do zestawu Moq, klikając prawym przyciskiem myszy folder References w projekcie ContactManager.Tests i wybierając polecenie Dodaj odwołanie. Na karcie Przeglądaj przejdź do folderu, w którym rozpakujesz plik Moq i wybierz zestaw Moq.dll. Kliknij przycisk OK .
  5. Po wykonaniu tych kroków folder References powinien wyglądać jak Rysunek 2.

Odblokowywanie Moq

Rysunek 01. Odblokowywanie aplikacji Moq(Kliknij, aby wyświetlić obraz w pełnym rozmiarze)

Odwołania po dodaniu moq

Rysunek 02. Odwołania po dodaniu pliku Moq(Kliknij, aby wyświetlić obraz w pełnym rozmiarze)

Tworzenie testów jednostkowych dla warstwy usługi

Zacznijmy od utworzenia zestawu testów jednostkowych dla warstwy usługi aplikacji Contact Manager. Użyjemy tych testów, aby zweryfikować naszą logikę walidacji.

Utwórz nowy folder o nazwie Models w projekcie ContactManager.Tests. Następnie kliknij prawym przyciskiem myszy folder Models i wybierz polecenie Dodaj, Nowy test. Zostanie wyświetlone okno dialogowe Dodawanie nowego testu pokazane na rysunku 3. Wybierz szablon Unit Test i nadaj nowemu testowi nazwę ContactManagerServiceTest.cs. Kliknij przycisk OK , aby dodać nowy test do projektu testowego.

Uwaga

Ogólnie rzecz biorąc, struktura folderów projektu testowego ma być zgodna ze strukturą folderów projektu ASP.NET MVC. Można na przykład umieścić testy kontrolera w folderze Controllers, testy modelu w folderze Models itd.

Models\ContactManagerServiceTest.cs

Rysunek 03. Models\ContactManagerServiceTest.cs(Kliknij, aby wyświetlić obraz w pełnym rozmiarze)

Początkowo chcemy przetestować metodę CreateContact() uwidoczniną przez klasę ContactManagerService. Utworzymy następujące pięć testów:

  • CreateContact() — testy, które createContact() zwracają wartość true, gdy do metody jest przekazywany prawidłowy kontakt.
  • CreateContactRequiredFirstName() — sprawdza, czy komunikat o błędzie jest dodawany do stanu modelu, gdy kontakt z brakującą nazwą zostanie przekazany do metody CreateContact().
  • CreateContactRequiredLastName() — sprawdza, czy komunikat o błędzie jest dodawany do stanu modelu, gdy kontakt z brakującą nazwą zostanie przekazany do metody CreateContact().
  • CreateContactInvalidPhone() — sprawdza, czy komunikat o błędzie jest dodawany do stanu modelu, gdy kontakt z nieprawidłowym numerem telefonu jest przekazywany do metody CreateContact().
  • CreateContactInvalidEmail() — sprawdza, czy komunikat o błędzie jest dodawany do stanu modelu, gdy kontakt z nieprawidłowym adresem e-mail jest przekazywany do metody CreateContact().

Pierwszy test sprawdza, czy prawidłowy kontakt nie generuje błędu weryfikacji. Pozostałe testy sprawdzają każdą z reguł walidacji.

Kod tych testów znajduje się na liście 1.

Lista 1 — Models\ContactManagerServiceTest.cs

using System.Web.Mvc;
using ContactManager.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace ContactManager.Tests.Models
{
    [TestClass]
    public class ContactManagerServiceTest
    {
        private Mock<IContactManagerRepository> _mockRepository;
        private ModelStateDictionary _modelState;
        private IContactManagerService _service;

        [TestInitialize]
        public void Initialize()
        {
            _mockRepository = new Mock<IContactManagerRepository>();
            _modelState = new ModelStateDictionary();
            _service = new ContactManagerService(new ModelStateWrapper(_modelState), _mockRepository.Object);
        }

        [TestMethod]
        public void CreateContact()
        {
            // Arrange
            var contact = Contact.CreateContact(-1, "Stephen", "Walther", "555-5555", "steve@somewhere.com");

            // Act
            var result = _service.CreateContact(contact);
        
            // Assert
            Assert.IsTrue(result);
        }

        [TestMethod]
        public void CreateContactRequiredFirstName()
        {
            // Arrange
            var contact = Contact.CreateContact(-1, string.Empty, "Walther", "555-5555", "steve@somewhere.com");

            // Act
            var result = _service.CreateContact(contact);

            // Assert
            Assert.IsFalse(result);
            var error = _modelState["FirstName"].Errors[0];
            Assert.AreEqual("First name is required.", error.ErrorMessage);
        }

        [TestMethod]
        public void CreateContactRequiredLastName()
        {
            // Arrange
            var contact = Contact.CreateContact(-1, "Stephen", string.Empty, "555-5555", "steve@somewhere.com");

            // Act
            var result = _service.CreateContact(contact);

            // Assert
            Assert.IsFalse(result);
            var error = _modelState["LastName"].Errors[0];
            Assert.AreEqual("Last name is required.", error.ErrorMessage);
        }

        [TestMethod]
        public void CreateContactInvalidPhone()
        {
            // Arrange
            var contact = Contact.CreateContact(-1, "Stephen", "Walther", "apple", "steve@somewhere.com");

            // Act
            var result = _service.CreateContact(contact);

            // Assert
            Assert.IsFalse(result);
            var error = _modelState["Phone"].Errors[0];
            Assert.AreEqual("Invalid phone number.", error.ErrorMessage);
        }

        [TestMethod]
        public void CreateContactInvalidEmail()
        {
            // Arrange
            var contact = Contact.CreateContact(-1, "Stephen", "Walther", "555-5555", "apple");

            // Act
            var result = _service.CreateContact(contact);

            // Assert
            Assert.IsFalse(result);
            var error = _modelState["Email"].Errors[0];
            Assert.AreEqual("Invalid email address.", error.ErrorMessage);
        }
    }
}

Ponieważ używamy klasy Contact na liście 1, musimy dodać odwołanie do programu Microsoft Entity Framework do projektu Test. Dodaj odwołanie do zestawu System.Data.Entity.

Lista 1 zawiera metodę o nazwie Initialize(), która jest ozdobiona atrybutem [TestInitialize]. Ta metoda jest wywoływana automatycznie przed uruchomieniem każdego z testów jednostkowych (jest wywoływana 5 razy przed każdym testem jednostkowym). Metoda Initialize() tworzy pozorne repozytorium z następującym wierszem kodu:

_mockRepository = new Mock<IContactManagerRepository>();

Ten wiersz kodu używa platformy Moq do generowania makiety repozytorium z interfejsu IContactManagerRepository. Pozorne repozytorium jest używane zamiast rzeczywistego repozytorium EntityContactManager, aby uniknąć uzyskiwania dostępu do bazy danych po uruchomieniu każdego testu jednostkowego. Makiety repozytorium implementuje metody interfejsu IContactManagerRepository, ale metody w rzeczywistości nic nie robią.

Uwaga

W przypadku korzystania z platformy Moq istnieje rozróżnienie między _mockRepository i _mockRepository.Object. Pierwsza z tych metod odwołuje się do klasy Mock<IContactManagerRepository> , która zawiera metody określania sposobu działania makiety repozytorium. Ten ostatni odnosi się do rzeczywistego makiety repozytorium, które implementuje interfejs IContactManagerRepository.

Makiety repozytorium jest używane w metodzie Initialize() podczas tworzenia wystąpienia klasy ContactManagerService. Wszystkie poszczególne testy jednostkowe używają tego wystąpienia klasy ContactManagerService.

Lista 1 zawiera pięć metod odpowiadających każdemu testowi jednostkowemu. Każda z tych metod jest ozdobiona atrybutem [TestMethod]. Po uruchomieniu testów jednostkowych wywoływana jest dowolna metoda, która ma ten atrybut. Innymi słowy, każda metoda ozdobiona atrybutem [TestMethod] jest testem jednostkowym.

Pierwszy test jednostkowy o nazwie CreateContact() sprawdza, czy wywołanie metody CreateContact() zwraca wartość true, gdy do metody przekazano prawidłowe wystąpienie klasy Contact. Test tworzy wystąpienie klasy Contact, wywołuje metodę CreateContact() i sprawdza, czy metoda CreateContact() zwraca wartość true.

Pozostałe testy sprawdzają, czy gdy metoda CreateContact() jest wywoływana z nieprawidłowym kontaktem, metoda zwraca wartość false, a oczekiwany komunikat o błędzie weryfikacji jest dodawany do stanu modelu. Na przykład test CreateContactRequiredFirstName() tworzy wystąpienie klasy Contact z pustym ciągiem dla właściwości FirstName. Następnie metoda CreateContact() jest wywoływana z nieprawidłowym kontaktem. Na koniec test sprawdza, czy funkcja CreateContact() zwraca wartość false, a stan modelu zawiera komunikat o błędzie oczekiwanej weryfikacji "Imię jest wymagane".

Testy jednostkowe można uruchomić na liście 1, wybierając opcję menu Test, Uruchom, Wszystkie testy w rozwiązaniu (CTRL+R, A). Wyniki testów są wyświetlane w oknie Wyniki testu (zobacz Rysunek 4).

Wyniki testu

Rysunek 04. Wyniki testów (kliknij, aby wyświetlić obraz pełnowymiarowy)

Tworzenie testów jednostkowych dla kontrolerów

ASP. Aplikacja NETMVC kontroluje przepływ interakcji użytkownika. Podczas testowania kontrolera chcesz sprawdzić, czy kontroler zwraca prawidłowy wynik akcji i wyświetli dane. Możesz również sprawdzić, czy kontroler wchodzi w interakcje z klasami modeli w oczekiwany sposób.

Na przykład lista 2 zawiera dwa testy jednostkowe metody Create() kontrolera kontaktów. Pierwszy test jednostkowy sprawdza, czy po przekazaniu prawidłowego kontaktu do metody Create() metoda Create() przekierowuje do akcji Indeks. Innymi słowy, po przekazaniu prawidłowego kontaktu metoda Create() powinna zwrócić wartość RedirectToRouteResult reprezentującą akcję Indeks.

Nie chcemy testować warstwy usługi ContactManager podczas testowania warstwy kontrolera. W związku z tym wyśmiewamy warstwę usługi następującym kodem w metodzie Initialize:

_service = new Mock();

W teście jednostkowym CreateValidContact() wyśmiewamy zachowanie wywoływania metody CreateContact() warstwy usługi za pomocą następującego wiersza kodu:

_service.Expect(s => s.CreateContact(contact)).Returns(true);

Ten wiersz kodu powoduje, że pozorna usługa ContactManager zwraca wartość true po wywołaniu metody CreateContact(). Pozorując warstwę usługi, możemy przetestować zachowanie kontrolera bez konieczności wykonywania kodu w warstwie usługi.

Drugi test jednostkowy sprawdza, czy akcja Create() zwraca widok Utwórz po przekazaniu nieprawidłowego kontaktu do metody . Powodujemy, że metoda CreateContact() warstwy usługi zwraca wartość false z następującym wierszem kodu:

_service.Expect(s => s.CreateContact(contact)).Returns(false);

Jeśli metoda Create() zachowuje się zgodnie z oczekiwaniami, powinna zwrócić widok Utwórz, gdy warstwa usługi zwraca wartość false. Dzięki temu kontroler może wyświetlać komunikaty o błędach walidacji w widoku Tworzenie, a użytkownik ma szansę poprawić, że nieprawidłowe właściwości kontaktu.

Jeśli planujesz kompilowanie testów jednostkowych dla kontrolerów, musisz zwrócić jawne nazwy widoków z akcji kontrolera. Na przykład nie zwracaj widoku w następujący sposób:

return View();

Zamiast tego zwróć widok w następujący sposób:

return View("Create");

Jeśli nie jesteś jawny podczas zwracania widoku, właściwość ViewResult.ViewName zwraca pusty ciąg.

Lista 2 — Controllers\ContactControllerTest.cs

using System.Web.Mvc;
using ContactManager.Controllers;
using ContactManager.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace ContactManager.Tests.Controllers
{
    [TestClass]
    public class ContactControllerTest
    {
        private Mock<IContactManagerService> _service;

        [TestInitialize]
        public void Initialize()
        {
            _service = new Mock<IContactManagerService>();
        }

        [TestMethod]
        public void CreateValidContact()
        {
            // Arrange
            var contact = new Contact();
            _service.Expect(s => s.CreateContact(contact)).Returns(true);
            var controller = new ContactController(_service.Object);
        
            // Act
            var result = (RedirectToRouteResult)controller.Create(contact);

            // Assert
            Assert.AreEqual("Index", result.RouteValues["action"]);
        }

        [TestMethod]
        public void CreateInvalidContact()
        {
            // Arrange
            var contact = new Contact();
            _service.Expect(s => s.CreateContact(contact)).Returns(false);
            var controller = new ContactController(_service.Object);

            // Act
            var result = (ViewResult)controller.Create(contact);

            // Assert
            Assert.AreEqual("Create", result.ViewName);
        }

    }
}

Podsumowanie

W tej iteracji utworzyliśmy testy jednostkowe dla naszej aplikacji Contact Manager. Możemy uruchomić te testy jednostkowe w dowolnym momencie, aby sprawdzić, czy nasza aplikacja nadal zachowuje się w oczekiwany sposób. Testy jednostkowe działają jako siatka bezpieczeństwa dla naszej aplikacji, umożliwiając nam bezpieczne modyfikowanie naszej aplikacji w przyszłości.

Utworzyliśmy dwa zestawy testów jednostkowych. Najpierw przetestowaliśmy naszą logikę walidacji, tworząc testy jednostkowe dla naszej warstwy usług. Następnie przetestowaliśmy logikę sterowania przepływem, tworząc testy jednostkowe dla warstwy kontrolera. Podczas testowania warstwy usługi izolowaliśmy nasze testy dla naszej warstwy usługi od warstwy repozytorium przez wyśmiewanie warstwy repozytorium. Podczas testowania warstwy kontrolera izolowaliśmy nasze testy dla warstwy kontrolera przez wyśmiewanie warstwy usługi.

W następnej iteracji zmodyfikujemy aplikację Contact Manager tak, aby obsługiwała grupy kontaktów. Dodamy tę nową funkcjonalność do naszej aplikacji przy użyciu procesu projektowania oprogramowania nazywanego programowaniem opartym na testach.