Włączanie automatycznych testów jednostkowych

autor: Microsoft

Pobierz plik PDF

Jest to krok 12 bezpłatnego samouczka aplikacji "NerdDinner" , który zawiera instrukcje tworzenia małej, ale kompletnej aplikacji internetowej przy użyciu ASP.NET MVC 1.

Krok 12 pokazuje, jak opracować zestaw zautomatyzowanych testów jednostkowych, które weryfikują naszą funkcjonalność narzędzia NerdDinner, i które zapewnią nam pewność wprowadzenia zmian i ulepszeń aplikacji w przyszłości.

Jeśli używasz ASP.NET MVC 3, zalecamy skorzystanie z samouczków Wprowadzenie With MVC 3 lub MVC Music Store.

NerdDinner — krok 12: Testy jednostkowe

Opracujmy zestaw zautomatyzowanych testów jednostkowych, które weryfikują naszą funkcjonalność narzędzia NerdDinner i zapewnią nam pewność wprowadzenia zmian i ulepszeń aplikacji w przyszłości.

Dlaczego test jednostkowy?

Na dysku do pracy pewnego ranka masz nagły błysk inspiracji na temat aplikacji, nad którą pracujesz. Zdajesz sobie sprawę, że istnieje zmiana, którą można zaimplementować, co znacznie poprawi aplikację. Może to być refaktoryzacja, która czyści kod, dodaje nową funkcję lub naprawia usterkę.

Pytanie, które stoi przed tobą po przybyciu na komputer, brzmi : "jak bezpieczne jest, aby to poprawić?" Co zrobić, jeśli wprowadzenie zmiany ma skutki uboczne lub coś łamie? Zmiana może być prosta i może potrwać tylko kilka minut, ale co zrobić, jeśli ręczne przetestowanie wszystkich scenariuszy aplikacji zajmuje kilka godzin? Co zrobić, jeśli zapomnisz o scenariuszu, a uszkodzona aplikacja przejdzie do środowiska produkcyjnego? Czy to ulepszenie jest naprawdę warte wszystkich wysiłków?

Zautomatyzowane testy jednostkowe mogą zapewnić sieć bezpieczeństwa, która umożliwia ciągłe rozszerzanie aplikacji i unikanie strachu przed kodem, nad którym pracujesz. Posiadanie testów automatycznych, które szybko weryfikują funkcje, pozwala kodować z ufnością — i umożliwia wprowadzanie ulepszeń, które w przeciwnym razie nie czuły się komfortowo. Pomagają one również tworzyć rozwiązania, które są bardziej możliwe do utrzymania i mają dłuższy okres istnienia - co prowadzi do znacznie wyższego zwrotu z inwestycji.

Struktura MVC platformy ASP.NET ułatwia i naturalne testowanie jednostkowe funkcjonalności aplikacji. Umożliwia również przepływ pracy programowania opartego na testach (TDD), który umożliwia programowanie oparte na testach.

Projekt NerdDinner.Tests

Po utworzeniu aplikacji NerdDinner na początku tego samouczka zostanie wyświetlony monit z pytaniem, czy chcemy utworzyć projekt testu jednostkowego, aby przejść wraz z projektem aplikacji:

Zrzut ekranu przedstawiający okno dialogowe Tworzenie projektu testów jednostkowych. Tak, wybierz pozycję Utwórz projekt testu jednostkowego. Nerd Dinner dot Tests jest napisany jako nazwa projektu Test.

Zaznaczono przycisk radiowy "Tak, utwórz projekt testowy jednostkowy", co spowodowało dodanie projektu "NerdDinner.Tests" do naszego rozwiązania:

Zrzut ekranu przedstawiający drzewo nawigacji Eksplorator rozwiązań. Wybrano testy kropki kolacji Nerd.

Projekt NerdDinner.Tests odwołuje się do zestawu projektu aplikacji NerdDinner i umożliwia łatwe dodawanie do niego testów automatycznych, które weryfikują funkcjonalność aplikacji.

Tworzenie testów jednostkowych dla klasy modelu kolacji

Dodajmy kilka testów do projektu NerdDinner.Tests, który weryfikujemy klasę Dinner utworzoną podczas tworzenia warstwy modelu.

Zaczniemy od utworzenia nowego folderu w naszym projekcie testowym o nazwie "Modele", w którym umieścimy nasze testy związane z modelem. Następnie kliknij prawym przyciskiem myszy folder i wybierz polecenie menu Dodaj nowy> test . Spowoduje to wyświetlenie okna dialogowego "Dodawanie nowego testu".

Wybierzemy utworzenie "testu jednostkowego" i nadenie mu nazwy "DinnerTest.cs":

Zrzut ekranu przedstawiający okno dialogowe Dodawanie nowego testu. Test jednostkowy został wyróżniony. Obiad Test dot c s jest napisany jako nazwa testu.

Po kliknięciu przycisku "ok" program Visual Studio doda (i otworzy) plik DinnerTest.cs do projektu:

Zrzut ekranu przedstawiający plik Dinner Test dot c s w programie Visual Studio.

Domyślny szablon testu jednostkowego programu Visual Studio ma w nim kilka kodów płytek kotłowych, które znajdę trochę niechlujnie. Wyczyśćmy go tak, aby zawierał poniższy kod:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NerdDinner.Models;

namespace NerdDinner.Tests.Models {
 
    [TestClass]
    public class DinnerTest {

    }
}

Atrybut [TestClass] w klasie DinnerTest powyżej identyfikuje go jako klasę, która będzie zawierać testy, a także opcjonalne inicjowanie testów i kod usuwania. Możemy zdefiniować w nim testy, dodając publiczne metody, które mają atrybut [TestMethod] na nich.

Poniżej przedstawiono pierwszy z dwóch testów, które dodamy, aby wykonać ćwiczenie w naszej klasie Dinner. Pierwszy test sprawdza, czy nasza kolacja jest nieprawidłowa, jeśli nowa kolacja została utworzona bez poprawnego ustawienia wszystkich właściwości. Drugi test sprawdza, czy nasza kolacja jest prawidłowa, gdy w kolacji ustawiono wszystkie właściwości z prawidłowymi wartościami:

[TestClass]
public class DinnerTest {

    [TestMethod]
    public void Dinner_Should_Not_Be_Valid_When_Some_Properties_Incorrect() {

        //Arrange
        Dinner dinner = new Dinner() {
            Title = "Test title",
            Country = "USA",
            ContactPhone = "BOGUS"
        };

        // Act
        bool isValid = dinner.IsValid;

        //Assert
        Assert.IsFalse(isValid);
    }

    [TestMethod]
    public void Dinner_Should_Be_Valid_When_All_Properties_Correct() {
        
        //Arrange
        Dinner dinner = new Dinner {
            Title = "Test title",
            Description = "Some description",
            EventDate = DateTime.Now,
            HostedBy = "ScottGu",
            Address = "One Microsoft Way",
            Country = "USA",
            ContactPhone = "425-703-8072",
            Latitude = 93,
            Longitude = -92,
        };

        // Act
        bool isValid = dinner.IsValid;

        //Assert
        Assert.IsTrue(isValid);
    }
}

Zauważysz powyżej, że nasze nazwy testów są bardzo wyraźne (i nieco pełne). Robimy to, ponieważ możemy utworzyć setki lub tysiące małych testów i chcemy ułatwić szybkie określenie intencji i zachowania każdego z nich (zwłaszcza gdy analizujemy listę błędów w module uruchamiającym testy). Nazwy testów powinny mieć nazwę po testowych funkcjach. Powyżej używamy wzorca nazewnictwa "Noun_Should_Verb".

Przeprowadzamy struktury testów przy użyciu wzorca testowania "AAA", który oznacza "Arrange, Act, Assert":

  • Rozmieszczanie: Konfigurowanie testowanej jednostki
  • Działanie: Ćwiczenie lekcji w ramach testu i przechwytywanie wyników
  • Potwierdzenie: Weryfikowanie zachowania

Kiedy piszemy testy, chcemy uniknąć zbyt dużej liczby testów. Zamiast tego każdy test powinien zweryfikować tylko jedną koncepcję (co znacznie ułatwi określenie przyczyny awarii). Dobrą wskazówką jest wypróbowanie tylko jednej instrukcji potwierdzenia dla każdego testu. Jeśli masz więcej niż jedną instrukcję potwierdzenia w metodzie testowej, upewnij się, że są one używane do testowania tej samej koncepcji. W razie wątpliwości wykonaj kolejny test.

Uruchamianie testów

Program Visual Studio 2008 Professional (i nowsze wersje) zawiera wbudowany moduł uruchamiający testy, który może służyć do uruchamiania projektów Visual Studio Unit Test w środowisku IDE. Możemy wybrać polecenie menu Test-Run-All>> w menu rozwiązania (lub wpisać Ctrl R, A), aby uruchomić wszystkie nasze testy jednostkowe. Alternatywnie możemy umieścić kursor w określonej klasie testowej lub metodzie testowej i użyć polecenia Test-Run-Tests>> w bieżącym menu kontekstowym (lub wpisać Ctrl R, T), aby uruchomić podzbiór testów jednostkowych.

Umieśćmy kursor w klasie DinnerTest i wpiszmy "Ctrl R, T", aby uruchomić dwa zdefiniowane testy. Gdy to zrobimy, zostanie wyświetlone okno "Wyniki testów" w programie Visual Studio i zobaczymy wyniki naszego przebiegu testu wymienione w nim:

Zrzut ekranu przedstawiający okno Wyniki testu w programie Visual Studio. Wyniki przebiegu testu znajdują się na liście.

Uwaga: w oknie wyników testu programu VS domyślnie nie jest wyświetlana kolumna Nazwa klasy. Możesz to dodać, klikając prawym przyciskiem myszy w oknie Wyniki testu i używając polecenia menu Dodaj/Usuń kolumny.

Nasze dwa testy miały tylko ułamek sekundy do uruchomienia — i jak widać, oba te testy przeszły. Teraz możemy je włączyć i rozszerzyć, tworząc dodatkowe testy, które weryfikują określone walidacje reguł, a także obejmują dwie metody pomocnicze — IsUserHost() i IsUserRegistered() — które dodaliśmy do klasy Dinner. Posiadanie wszystkich tych testów dla klasy Dinner znacznie ułatwi i bezpieczniejsze dodawanie do niej nowych reguł biznesowych i walidacji w przyszłości. Możemy dodać nową logikę reguły do kolacji, a następnie w ciągu kilku sekund sprawdzić, czy nie uszkodziła żadnej z naszych poprzednich funkcji logiki.

Zwróć uwagę, że użycie opisowej nazwy testu ułatwia szybkie zrozumienie, co sprawdza każdy test. Zalecamy użycie polecenia menu Narzędzia opcje>, otwarcie ekranu konfiguracji Test Tools-Test> Execution i zaznaczenie pola wyboru "Dwukrotne kliknięcie nieudanego lub niejednoznacznego wyniku testu jednostkowego wyświetla punkt niepowodzenia w teście". Umożliwi to dwukrotne kliknięcie błędu w oknie wyników testu i natychmiastowe przejście do błędu potwierdzenia.

Tworzenie testów jednostkowych dinnersController

Teraz utwórzmy kilka testów jednostkowych, które weryfikują naszą funkcjonalność DinnersController. Zaczniemy od kliknięcia prawym przyciskiem myszy folderu "Controllers" w projekcie Test, a następnie wybrania polecenia menu Dodaj nowy> test . Utworzymy "Test jednostkowy" i nadamy mu nazwę "DinnersControllerTest.cs".

Utworzymy dwie metody testowe, które weryfikują metodę akcji Details() w kontrolerze DinnersController. Pierwszy z nich sprawdzi, czy widok jest zwracany po zażądaniu istniejącej kolacji. Drugi sprawdzi, czy jest zwracany widok "NotFound", gdy zażądano nieistniejącej kolacji:

[TestClass]
public class DinnersControllerTest {

    [TestMethod]
    public void DetailsAction_Should_Return_View_For_ExistingDinner() {

        // Arrange
        var controller = new DinnersController();

        // Act
        var result = controller.Details(1) as ViewResult;

        // Assert
        Assert.IsNotNull(result, "Expected View");
    }

    [TestMethod]
    public void DetailsAction_Should_Return_NotFoundView_For_BogusDinner() {

        // Arrange
        var controller = new DinnersController();

        // Act
        var result = controller.Details(999) as ViewResult;

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

Powyższy kod kompiluje czystą kompilację. Jednak po uruchomieniu testów oba te testy kończą się niepowodzeniem:

Zrzut ekranu przedstawiający kod. Oba testy zakończyły się niepowodzeniem.

Jeśli przyjrzymy się komunikatom o błędach, zobaczymy, że przyczyną niepowodzenia testów było to, że nasza klasa DinnersRepository nie mogła nawiązać połączenia z bazą danych. Nasza aplikacja NerdDinner używa parametrów połączenia do lokalnego pliku SQL Server Express, który znajduje się w katalogu \App_Data projektu aplikacji NerdDinner. Ponieważ nasz projekt NerdDinner.Tests kompiluje i działa w innym katalogu, wówczas projekt aplikacji, względna lokalizacja ścieżki naszych parametrów połączenia jest niepoprawna.

Możemy rozwiązać ten problem, kopiując plik bazy danych SQL Express do projektu testowego, a następnie dodając do niego odpowiednie parametry połączenia testowego w App.config naszego projektu testowego. Spowoduje to odblokowanie i uruchomienie powyższych testów.

Testowanie jednostkowe kodu korzystającego z rzeczywistej bazy danych wiąże się jednak z wieloma wyzwaniami. W szczególności:

  • Znacznie spowalnia czas wykonywania testów jednostkowych. Im dłużej trwa uruchamianie testów, tym mniej prawdopodobne jest częste ich wykonywanie. Najlepiej, aby testy jednostkowe mogły być uruchamiane w sekundach i mieć coś, co robisz tak naturalnie, jak kompilowanie projektu.
  • Komplikuje konfigurację i logikę oczyszczania w ramach testów. Chcesz, aby każdy test jednostkowy był izolowany i niezależny od innych (bez skutków ubocznych lub zależności). Podczas pracy z rzeczywistą bazą danych należy pamiętać o stanie i zresetować ją między testami.

Przyjrzyjmy się wzorowi projektowemu o nazwie "wstrzykiwanie zależności", który może pomóc nam obejść te problemy i uniknąć konieczności używania prawdziwej bazy danych z naszymi testami.

Wstrzykiwanie zależności

W tej chwili DinnersController jest ściśle "powiązany" z klasą DinnerRepository. "Sprzęganie" odnosi się do sytuacji, w której klasa jawnie opiera się na innej klasie, aby pracować:

public class DinnersController : Controller {

    DinnerRepository dinnerRepository = new DinnerRepository();

    //
    // GET: /Dinners/Details/5

    public ActionResult Details(int id) {

        Dinner dinner = dinnerRepository.FindDinner(id);

        if (dinner == null)
            return View("NotFound");

        return View(dinner);
    }

Ponieważ klasa DinnerRepository wymaga dostępu do bazy danych, ściśle połączona zależność, która klasa DinnersController ma w repozytorium DinnerRepository, kończy się wymaganiem posiadania bazy danych w celu przetestowania metod akcji DinnersController.

Możemy obejść ten problem, stosując wzorzec projektowy o nazwie "wstrzykiwanie zależności" — czyli podejście polegające na tym, że zależności (takie jak klasy repozytorium zapewniające dostęp do danych) nie są już niejawnie tworzone w klasach, które z nich korzystają. Zamiast tego zależności można jawnie przekazać do klasy, która używa ich przy użyciu argumentów konstruktora. Jeśli zależności są definiowane przy użyciu interfejsów, mamy możliwość przekazania "fałszywych" implementacji zależności dla scenariuszy testów jednostkowych. Dzięki temu możemy tworzyć implementacje zależności specyficzne dla testów, które w rzeczywistości nie wymagają dostępu do bazy danych.

Aby zobaczyć to w działaniu, zaimplementujmy wstrzykiwanie zależności za pomocą elementu DinnersController.

Wyodrębnianie interfejsu IDinnerRepository

Pierwszym krokiem będzie utworzenie nowego interfejsu IDinnerRepository, który hermetyzuje kontrakt repozytorium, którego kontrolery wymagają pobrania i zaktualizowania kolacji.

Ten kontrakt interfejsu można zdefiniować ręcznie, klikając prawym przyciskiem myszy folder \Models, a następnie wybierając polecenie menu Dodaj nowy> element i tworząc nowy interfejs o nazwie IDinnerRepository.cs.

Alternatywnie możemy użyć wbudowanych narzędzi refaktoryzacji Visual Studio Professional (i nowszych wersji), aby automatycznie wyodrębnić i utworzyć interfejs dla nas z istniejącej klasy DinnerRepository. Aby wyodrębnić ten interfejs przy użyciu programu VS, po prostu umieść kursor w edytorze tekstów w klasie DinnerRepository, a następnie kliknij prawym przyciskiem myszy i wybierz polecenie menu Refaktoryzacja wyodrębniania> interfejsu :

Zrzut ekranu przedstawiający pozycję Wyodrębnij interfejs wybrany w podmenu Refaktoryzacja.

Spowoduje to uruchomienie okna dialogowego "Wyodrębnij interfejs" i wyświetlenie monitu o nazwę interfejsu do utworzenia. Ustawienie domyślne to IDinnerRepository i automatyczne wybranie wszystkich metod publicznych w istniejącej klasie DinnerRepository w celu dodania do interfejsu:

Zrzut ekranu przedstawiający okno Wyniki testu w programie Visual Studio.

Po kliknięciu przycisku "ok" program Visual Studio doda nowy interfejs IDinnerRepository do naszej aplikacji:

public interface IDinnerRepository {

    IQueryable<Dinner> FindAllDinners();
    IQueryable<Dinner> FindByLocation(float latitude, float longitude);
    IQueryable<Dinner> FindUpcomingDinners();
    Dinner             GetDinner(int id);

    void Add(Dinner dinner);
    void Delete(Dinner dinner);
    
    void Save();
}

A nasza istniejąca klasa DinnerRepository zostanie zaktualizowana tak, aby implementować interfejs:

public class DinnerRepository : IDinnerRepository {
   ...
}

Aktualizowanie elementu DinnersController w celu obsługi iniekcji konstruktora

Teraz zaktualizujemy klasę DinnersController, aby używać nowego interfejsu.

Obecnie Element DinnersController jest zakodowany na stałe, tak aby jego pole "dinnerRepository" było zawsze klasą DinnerRepository:

public class DinnersController : Controller {

    DinnerRepository dinnerRepository = new DinnerRepository();

    ...
}

Zmienimy to tak, aby pole "dinnerRepository" było typu IDinnerRepository zamiast DinnerRepository. Następnie dodamy dwa publiczne konstruktory DinnersController. Jeden z konstruktorów umożliwia przekazanie repozytorium IDinnerRepository jako argumentu. Drugi jest domyślnym konstruktorem, który używa naszej istniejącej implementacji DinnerRepository:

public class DinnersController : Controller {

    IDinnerRepository dinnerRepository;

    public DinnersController()
        : this(new DinnerRepository()) {
    }

    public DinnersController(IDinnerRepository repository) {
        dinnerRepository = repository;
    }
    ...
}

Ponieważ ASP.NET MVC domyślnie tworzy klasy kontrolerów przy użyciu konstruktorów domyślnych, nasza klasa DinnersController w czasie wykonywania będzie nadal używać klasy DinnerRepository do wykonywania dostępu do danych.

Teraz możemy zaktualizować nasze testy jednostkowe, aby przekazać "fałszywą" implementację repozytorium kolacji przy użyciu konstruktora parametrów. To "fałszywe" repozytorium kolacji nie będzie wymagać dostępu do rzeczywistej bazy danych, a zamiast tego użyje przykładowych danych w pamięci.

Tworzenie klasy FakeDinnerRepository

Utwórzmy klasę FakeDinnerRepository.

Zaczniemy od utworzenia katalogu "Fakes" w projekcie NerdDinner.Tests, a następnie dodania do niego nowej klasy FakeDinnerRepository (kliknij prawym przyciskiem myszy folder i wybierz polecenie Dodaj> nową klasę):

Zrzut ekranu przedstawiający element menu Dodaj nową klasę. Pozycja Dodaj nowy element jest wyróżniona.

Zaktualizujemy kod tak, aby klasa FakeDinnerRepository implementuje interfejs IDinnerRepository. Następnie możemy kliknąć go prawym przyciskiem myszy i wybrać polecenie menu kontekstowego "Implementuj interfejs IDinnerRepository":

Zrzut ekranu przedstawiający polecenie menu kontekstowego Implementuj interfejs I Dinner Repository.

Spowoduje to, że program Visual Studio automatycznie doda wszystkie elementy członkowskie interfejsu IDinnerRepository do klasy FakeDinnerRepository z domyślnymi implementacjami "wycinków":

public class FakeDinnerRepository : IDinnerRepository {

    public IQueryable<Dinner> FindAllDinners() {
        throw new NotImplementedException();
    }

    public IQueryable<Dinner> FindByLocation(float lat, float long){
        throw new NotImplementedException();
    }

    public IQueryable<Dinner> FindUpcomingDinners() {
        throw new NotImplementedException();
    }

    public Dinner GetDinner(int id) {
        throw new NotImplementedException();
    }

    public void Add(Dinner dinner) {
        throw new NotImplementedException();
    }

    public void Delete(Dinner dinner) {
        throw new NotImplementedException();
    }

    public void Save() {
        throw new NotImplementedException();
    }
}

Następnie możemy zaktualizować implementację FakeDinnerRepository, aby wyłączyć zbieranie kolacji listy<> w pamięci przekazanej do niej jako argument konstruktora:

public class FakeDinnerRepository : IDinnerRepository {

    private List<Dinner> dinnerList;

    public FakeDinnerRepository(List<Dinner> dinners) {
        dinnerList = dinners;
    }

    public IQueryable<Dinner> FindAllDinners() {
        return dinnerList.AsQueryable();
    }

    public IQueryable<Dinner> FindUpcomingDinners() {
        return (from dinner in dinnerList
                where dinner.EventDate > DateTime.Now
                select dinner).AsQueryable();
    }

    public IQueryable<Dinner> FindByLocation(float lat, float lon) {
        return (from dinner in dinnerList
                where dinner.Latitude == lat && dinner.Longitude == lon
                select dinner).AsQueryable();
    }

    public Dinner GetDinner(int id) {
        return dinnerList.SingleOrDefault(d => d.DinnerID == id);
    }

    public void Add(Dinner dinner) {
        dinnerList.Add(dinner);
    }

    public void Delete(Dinner dinner) {
        dinnerList.Remove(dinner);
    }

    public void Save() {
        foreach (Dinner dinner in dinnerList) {
            if (!dinner.IsValid)
                throw new ApplicationException("Rule violations");
        }
    }
}

Mamy teraz fałszywą implementację IDinnerRepository, która nie wymaga bazy danych, i zamiast tego można pracować z listą obiektów obiadowych w pamięci.

Używanie narzędzia FakeDinnerRepository z testami jednostkowymi

Wróćmy do testów jednostkowych DinnersController, które zakończyły się niepowodzeniem wcześniej, ponieważ baza danych nie była dostępna. Możemy zaktualizować metody testowania, aby użyć repozytorium FakeDinnerRepository wypełnionego przykładowymi danymi obiadowymi w pamięci do elementu DinnersController przy użyciu poniższego kodu:

[TestClass]
public class DinnersControllerTest {

    List<Dinner> CreateTestDinners() {

        List<Dinner> dinners = new List<Dinner>();

        for (int i = 0; i < 101; i++) {

            Dinner sampleDinner = new Dinner() {
                DinnerID = i,
                Title = "Sample Dinner",
                HostedBy = "SomeUser",
                Address = "Some Address",
                Country = "USA",
                ContactPhone = "425-555-1212",
                Description = "Some description",
                EventDate = DateTime.Now.AddDays(i),
                Latitude = 99,
                Longitude = -99
            };
            
            dinners.Add(sampleDinner);
        }
        
        return dinners;
    }

    DinnersController CreateDinnersController() {
        var repository = new FakeDinnerRepository(CreateTestDinners());
        return new DinnersController(repository);
    }

    [TestMethod]
    public void DetailsAction_Should_Return_View_For_Dinner() {

        // Arrange
        var controller = CreateDinnersController();

        // Act
        var result = controller.Details(1);

        // Assert
        Assert.IsInstanceOfType(result, typeof(ViewResult));
    }

    [TestMethod]
    public void DetailsAction_Should_Return_NotFoundView_For_BogusDinner() {

        // Arrange
        var controller = CreateDinnersController();

        // Act
        var result = controller.Details(999) as ViewResult;

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

A teraz, gdy uruchomimy te testy, oba te testy przejdą:

Zrzut ekranu przedstawiający testy jednostkowe, które przeszły oba testy.

Co najlepsze, zajmują tylko ułamek sekundy do uruchomienia i nie wymagają skomplikowanej logiki konfiguracji/czyszczenia. Teraz możemy przetestować cały kod metody akcji DinnersController (w tym wyświetlanie, stronicowanie, szczegóły, tworzenie, aktualizowanie i usuwanie) bez konieczności nawiązywania połączenia z rzeczywistą bazą danych.

Temat boczny: struktury wstrzykiwania zależności
Wykonanie ręcznego wstrzykiwania zależności (jak powyżej) działa prawidłowo, ale staje się trudniejsze do utrzymania w miarę wzrostu liczby zależności i składników w aplikacji. Istnieje kilka struktur wstrzykiwania zależności dla platformy .NET, które mogą pomóc w zapewnieniu jeszcze większej elastyczności zarządzania zależnościami. Te struktury, czasami nazywane również "Inversion of Control" (IoC) kontenery, zapewniają mechanizmy, które umożliwiają dodatkowy poziom obsługi konfiguracji do określania i przekazywania zależności do obiektów w czasie wykonywania (najczęściej przy użyciu iniekcji konstruktora). Niektóre z bardziej popularnych struktur wstrzykiwania zależności systemu operacyjnego / IOC na platformie .NET to: AutoFac, Ninject, Spring.NET, StructureMap i Windsor. ASP.NET MVC uwidacznia interfejsy API rozszerzalności, które umożliwiają deweloperom uczestnictwo w rozwiązywaniu i utworzeniu wystąpień kontrolerów oraz umożliwia bezproblemowe integrowanie struktur Wstrzykiwanie zależności /IoC w ramach tego procesu. Użycie struktury DI/IOC umożliwiłoby również usunięcie domyślnego konstruktora z elementu DinnersController — co całkowicie spowodowałoby usunięcie sprzężenia między nim a repozytorium DinnerRepository. Nie będziemy używać struktury wstrzykiwania zależności /IOC w naszej aplikacji NerdDinner. Ale jest to coś, co moglibyśmy rozważyć na przyszłość, jeśli NerdDinner code-base i możliwości rosły.

Tworzenie edycji testów jednostkowych akcji

Teraz utwórzmy kilka testów jednostkowych, które weryfikują funkcję Edit (Edytuj) kontrolki DinnersController. Zaczniemy od przetestowania wersji HTTP-GET akcji Edytuj:

//
// GET: /Dinners/Edit/5

[Authorize]
public ActionResult Edit(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (!dinner.IsHostedBy(User.Identity.Name))
        return View("InvalidOwner");

    return View(new DinnerFormViewModel(dinner));
}

Utworzymy test sprawdzający, czy obiekt DinnerFormViewModel jest renderowany z powrotem w widoku wspieranym przez obiekt DinnerFormViewModel po zażądaniu prawidłowej kolacji:

[TestMethod]
public void EditAction_Should_Return_View_For_ValidDinner() {

    // Arrange
    var controller = CreateDinnersController();

    // Act
    var result = controller.Edit(1) as ViewResult;

    // Assert
    Assert.IsInstanceOfType(result.ViewData.Model, typeof(DinnerFormViewModel));
}

Gdy jednak uruchomimy test, stwierdzimy, że kończy się niepowodzeniem, ponieważ jest zgłaszany wyjątek odwołania o wartości null, gdy metoda Edit uzyskuje dostęp do właściwości User.Identity.Name w celu wykonania sprawdzania Dinner.IsHostedBy().

Obiekt User w klasie bazowej Kontroler hermetyzuje szczegóły dotyczące zalogowanego użytkownika i jest wypełniany przez ASP.NET MVC podczas tworzenia kontrolera w czasie wykonywania. Ponieważ testujemy element DinnersController poza środowiskiem serwera internetowego, obiekt User nie jest ustawiony (stąd wyjątek odwołania o wartości null).

Pozorowanie właściwości User.Identity.Name

Pozorowanie struktur ułatwia testowanie, umożliwiając nam dynamiczne tworzenie fałszywych wersji obiektów zależnych, które obsługują nasze testy. Na przykład możemy użyć platformy pozorowania w naszym teście akcji Edytuj, aby dynamicznie utworzyć obiekt Użytkownika, którego nasz kontroler DinnersController może użyć do wyszukania symulowanej nazwy użytkownika. Pozwoli to uniknąć zgłaszania odwołania o wartości null podczas uruchamiania testu.

Istnieje wiele platform pozorowania platform .NET, których można używać z ASP.NET MVC (można je wyświetlić tutaj: http://www.mockframeworks.com/).

Po pobraniu dodamy odwołanie do projektu NerdDinner.Tests do zestawu Moq.dll:

Zrzut ekranu przedstawiający drzewo nawigacji Nerd Dinner. Plik Moq został wyróżniony.

Następnie dodamy metodę pomocnika "CreateDinnersControllerAs(username)" do klasy testowej, która przyjmuje nazwę użytkownika jako parametr, a następnie "kpi" z właściwości User.Identity.Name w wystąpieniu DinnersController:

DinnersController CreateDinnersControllerAs(string userName) {

    var mock = new Mock<ControllerContext>();
    mock.SetupGet(p => p.HttpContext.User.Identity.Name).Returns(userName);
    mock.SetupGet(p => p.HttpContext.Request.IsAuthenticated).Returns(true);

    var controller = CreateDinnersController();
    controller.ControllerContext = mock.Object;

    return controller;
}

Powyżej używamy narzędzia Moq do utworzenia obiektu mock, który sfałszuje obiekt ControllerContext (co jest tym, co ASP.NET MVC przekazuje do klas Kontroler w celu uwidocznienia obiektów środowiska uruchomieniowego, takich jak Użytkownik, Żądanie, Odpowiedź i Sesja). Wywołujemy metodę "SetupGet" na makiecie, aby wskazać, że właściwość HttpContext.User.Identity.Name w obiekcie ControllerContext powinna zwrócić ciąg nazwy użytkownika przekazany do metody pomocniczej.

Możemy wyśmiewać dowolną liczbę właściwości i metod ControllerContext. Aby to zilustrować, dodano również wywołanie SetupGet() dla właściwości Request.IsAuthenticated (która nie jest rzeczywiście potrzebna w poniższych testach — co pomaga zilustrować, jak można wyśmiewać właściwości żądania). Po zakończeniu przypisujemy wystąpienie makiety ControllerContext do elementu DinnersController zwracana jest metoda pomocnika.

Teraz możemy napisać testy jednostkowe, które używają tej metody pomocniczej do testowania scenariuszy edycji obejmujących różnych użytkowników:

[TestMethod]
public void EditAction_Should_Return_EditView_When_ValidOwner() {

    // Arrange
    var controller = CreateDinnersControllerAs("SomeUser");

    // Act
    var result = controller.Edit(1) as ViewResult;

    // Assert
    Assert.IsInstanceOfType(result.ViewData.Model, typeof(DinnerFormViewModel));
}

[TestMethod]
public void EditAction_Should_Return_InvalidOwnerView_When_InvalidOwner() {

    // Arrange
    var controller = CreateDinnersControllerAs("NotOwnerUser");

    // Act
    var result = controller.Edit(1) as ViewResult;

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

A teraz, gdy uruchamiamy testy, które przechodzą:

Zrzut ekranu przedstawiający testy jednostkowe korzystające z metody pomocniczej. Testy zostały zakończone.

Testowanie scenariuszy UpdateModel()

Utworzyliśmy testy obejmujące wersję HTTP-GET akcji Edytuj. Teraz utwórzmy kilka testów, które weryfikują wersję HTTP-POST akcji Edytuj:

//
// POST: /Dinners/Edit/5

[AcceptVerbs(HttpVerbs.Post), Authorize]
public ActionResult Edit (int id, FormCollection collection) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (!dinner.IsHostedBy(User.Identity.Name))
        return View("InvalidOwner");

    try {
        UpdateModel(dinner);

        dinnerRepository.Save();

        return RedirectToAction("Details", new { id=dinner.DinnerID });
    }
    catch {
        ModelState.AddModelErrors(dinner.GetRuleViolations());
        
        return View(new DinnerFormViewModel(dinner));
    }
}

Ciekawym nowym scenariuszem testowania dla nas obsługiwanym za pomocą tej metody akcji jest użycie metody pomocnika UpdateModel() w klasie podstawowej Kontroler. Używamy tej metody pomocniczej do powiązania wartości form-post z wystąpieniem obiektu Dinner.

Poniżej przedstawiono dwa testy, które pokazują, jak możemy podać wartości opublikowane formularzy dla metody pomocnika UpdateModel() do użycia. W tym celu utworzymy i wypełnimy obiekt FormCollection, a następnie przypiszemy go do właściwości "ValueProvider" na kontrolerze.

Pierwszy test sprawdza, czy po pomyślnym zapisaniu przeglądarki nastąpi przekierowanie do akcji szczegółów. Drugi test sprawdza, czy po wysłaniu nieprawidłowych danych wejściowych akcja ponownie odtwarza widok edycji z komunikatem o błędzie.

[TestMethod]
public void EditAction_Should_Redirect_When_Update_Successful() {

    // Arrange      
    var controller = CreateDinnersControllerAs("SomeUser");

    var formValues = new FormCollection() {
        { "Title", "Another value" },
        { "Description", "Another description" }
    };

    controller.ValueProvider = formValues.ToValueProvider();
    
    // Act
    var result = controller.Edit(1, formValues) as RedirectToRouteResult;

    // Assert
    Assert.AreEqual("Details", result.RouteValues["Action"]);
}

[TestMethod]
public void EditAction_Should_Redisplay_With_Errors_When_Update_Fails() {

    // Arrange
    var controller = CreateDinnersControllerAs("SomeUser");

    var formValues = new FormCollection() {
        { "EventDate", "Bogus date value!!!"}
    };

    controller.ValueProvider = formValues.ToValueProvider();

    // Act
    var result = controller.Edit(1, formValues) as ViewResult;

    // Assert
    Assert.IsNotNull(result, "Expected redisplay of view");
    Assert.IsTrue(result.ViewData.ModelState.Count > 0, "Expected errors");
}

Testowanie Wrap-Up

Omówiliśmy podstawowe pojęcia związane z klasami kontrolerów testów jednostkowych. Za pomocą tych technik można łatwo tworzyć setki prostych testów, które weryfikują zachowanie naszej aplikacji.

Ponieważ nasze testy kontrolera i modelu nie wymagają rzeczywistej bazy danych, są bardzo szybkie i łatwe do uruchomienia. Będziemy mogli wykonać setki testów automatycznych w ciągu kilku sekund i natychmiast uzyskać opinię na temat tego, czy wprowadzono zmianę. Pomoże to zapewnić nam pewność, że będziemy stale ulepszać, refaktoryzować i udoskonalać naszą aplikację.

W tym rozdziale omówiliśmy testowanie jako ostatni temat , ale nie dlatego, że testowanie jest czymś, co należy zrobić na końcu procesu programowania. Wręcz przeciwnie, należy pisać testy automatyczne tak wcześnie, jak to możliwe w procesie programowania. Dzięki temu możesz uzyskać natychmiastową opinię podczas opracowywania, ułatwia przemyślane przemyślenie scenariuszy przypadków użycia aplikacji i prowadzi cię do projektowania aplikacji z myślą o czystym warstwie i sprzężeniu.

W dalszej części książki omówiono programowanie oparte na testach (TDD) i sposób używania go z ASP.NET MVC. TDD to iteracyjna praktyka kodowania, w której najpierw napiszesz testy, które spełni wynikowy kod. Przy użyciu funkcji TDD można rozpocząć każdą funkcję, tworząc test weryfikujący funkcje, które mają być implementowane. Napisanie testu jednostkowego najpierw pomaga upewnić się, że dobrze rozumiesz tę funkcję i sposób jej działania. Dopiero po zapisaniu testu (i sprawdzeniu, czy kończy się niepowodzeniem), należy zaimplementować rzeczywistą funkcjonalność sprawdzaną przez test. Ponieważ już poświęciliśmy czas na zastanowienie się nad przypadkiem użycia funkcji, który ma działać, lepiej zrozumiesz wymagania i jak najlepiej je zaimplementować. Po zakończeniu implementacji możesz ponownie uruchomić test i uzyskać natychmiastową opinię na temat tego, czy funkcja działa prawidłowo. Omówimy TDD więcej w rozdziale 10.

Następny krok

Niektóre końcowe podsuń komentarze.