Iteracja 6 — Korzystanie z projektowania opartego na testach (C#)

autor: Microsoft

Pobierz kod

W tej szóstej iteracji dodamy nową funkcjonalność do naszej aplikacji, pisząc najpierw testy jednostkowe i pisząc kod względem testów jednostkowych. W tej iteracji dodajemy grupy kontaktów.

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. Po każdej iteracji stopniowo ulepszamy aplikację. Celem tego podejścia iteracji wielokrotnej jest umożliwienie zrozumienia przyczyny każdej zmiany.

  • Iteracja #1 — tworzenie aplikacji. W pierwszej iteracji utworzymy menedżera kontaktów w najprostszy możliwy sposób. Dodamy obsługę podstawowych operacji bazy danych: tworzenie, odczytywanie, 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ą ASP.NET widoku MVC i 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 połącz aplikację. W tej czwartej iteracji korzystamy z kilku wzorców projektowania 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 dodanie testów jednostkowych. Wyśmiewamy nasze klasy modelu danych i kompilujemy testy jednostkowe dla naszych kontrolerów i logiki walidacji.

  • Iteracja nr 6 — korzystanie z programowania opartego na testach. W tej szóstej iteracji dodamy nową funkcjonalność do naszej aplikacji, 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 reakcji i wydajność naszej aplikacji, dodając obsługę Ajax.

Ta iteracja

W poprzedniej iteracji aplikacji Contact Manager utworzyliśmy testy jednostkowe zapewniające sieć bezpieczeństwa dla naszego kodu. Motywacją do utworzenia testów jednostkowych było zwiększenie odporności naszego kodu na zmianę. Dzięki testom jednostkowym możemy szczęśliwie wprowadzić wszelkie zmiany w naszym kodzie i natychmiast wiedzieć, czy uszkodziliśmy istniejące funkcje.

W tej iteracji używamy testów jednostkowych do zupełnie innego celu. W tej iteracji używamy testów jednostkowych w ramach filozofii projektowania aplikacji nazywanej programowaniem opartym na testach. Podczas opracowywania opartego na testach najpierw pisać testy, a następnie pisać kod względem testów.

Dokładniej mówiąc, podczas trenowania programowania opartego na testach podczas tworzenia kodu (Red/Green/Refactor):

  1. Pisanie testu jednostkowego, który kończy się niepowodzeniem (czerwony)
  2. Pisanie kodu, który przechodzi test jednostkowy (Zielony)
  3. Refaktoryzacja kodu (refaktoryzacja)

Najpierw należy napisać test jednostkowy. Test jednostkowy powinien wyrazić zamiar zachowania kodu. Podczas pierwszego tworzenia testu jednostkowego test jednostkowy powinien zakończyć się niepowodzeniem. Test powinien zakończyć się niepowodzeniem, ponieważ nie został jeszcze napisany żaden kod aplikacji, który spełnia test.

Następnie napiszesz wystarczająco dużo kodu, aby test jednostkowy przeszedł. Celem jest napisanie kodu w leniwy, niechlujny i najszybszy możliwy sposób. Nie należy tracić czasu na myślenie o architekturze aplikacji. Zamiast tego należy skupić się na pisaniu minimalnej ilości kodu niezbędnego do spełnienia intencji wyrażonej przez test jednostkowy.

Na koniec, po napisaniu wystarczającej ilości kodu, możesz cofnąć się i rozważyć ogólną architekturę aplikacji. W tym kroku napiszesz ponownie (refaktoryzację) kodu, korzystając z wzorców projektowania oprogramowania — takich jak wzorzec repozytorium — aby kod był bardziej konserwowalny. Możesz bez obaw ponownie napisać kod w tym kroku, ponieważ kod jest objęty testami jednostkowym.

Istnieje wiele korzyści, które wynikają z praktykowania programowania opartego na testach. Najpierw programowanie oparte na testach wymusza skupienie się na kodzie, który rzeczywiście musi być napisany. Ponieważ nieustannie koncentrujesz się na pisaniu wystarczającej ilości kodu, aby przejść określony test, nie można wędrować do chwastów i pisać ogromne ilości kodu, których nigdy nie będziesz używać.

Po drugie, metodologia projektowania "test pierwszy" wymusza pisanie kodu z perspektywy sposobu użycia kodu. Innymi słowy, podczas ćwiczeń programowania opartego na testach stale piszesz testy z perspektywy użytkownika. W związku z tym programowanie oparte na testach może spowodować czystsze i bardziej zrozumiałe interfejsy API.

Na koniec programowanie oparte na testach wymusza pisanie testów jednostkowych w ramach normalnego procesu pisania aplikacji. W miarę zbliżania się terminu projektu testowanie jest zazwyczaj pierwszą rzeczą, która wychodzi przez okno. W przypadku praktykowania programowania opartego na testach z drugiej strony bardziej prawdopodobne jest, aby być bardziej cnotliwym w pisaniu testów jednostkowych, ponieważ programowanie oparte na testach sprawia, że testy jednostkowe są centralnym elementem procesu tworzenia aplikacji.

Uwaga

Aby dowiedzieć się więcej na temat programowania opartego na testach, zalecam przeczytanie książki Michael Feathers Working Effectively with Legacy Code (Praca skutecznie z starszym kodem).

W tej iteracji dodamy nową funkcję do naszej aplikacji Contact Manager. Dodamy obsługę grup kontaktów. Za pomocą grup kontaktów można organizować kontakty w kategorie, takie jak grupy Biznesowe i Znajome.

Dodamy tę nową funkcjonalność do naszej aplikacji, wykonując proces programowania opartego na testach. Najpierw napiszemy nasze testy jednostkowe i napiszemy cały nasz kod względem tych testów.

Co jest testowane

Jak wspomniano w poprzedniej iteracji, zwykle nie są zapisywane testy jednostkowe logiki dostępu do danych ani logiki wyświetlania. Nie piszesz testów jednostkowych dla logiki dostępu do danych, ponieważ uzyskiwanie dostępu do bazy danych jest stosunkowo powolną operacją. Nie można pisać testów jednostkowych dla logiki widoku, ponieważ uzyskiwanie dostępu do widoku wymaga uruchomienia serwera internetowego, który jest stosunkowo powolnym działaniem. Nie należy pisać testu jednostkowego, chyba że test można wykonać ponownie bardzo szybko

Ponieważ programowanie oparte na testach jest oparte na testach jednostkowych, koncentrujemy się początkowo na pisaniu kontrolera i logiki biznesowej. Unikamy dotykania bazy danych lub widoków. Nie zmodyfikujemy bazy danych ani nie utworzymy naszych widoków do momentu zakończenia tego samouczka. Zaczynamy od tego, co można przetestować.

Tworzenie historii użytkowników

Podczas trenowania programowania opartego na testach zawsze zaczynasz od pisania testu. Natychmiast rodzi to pytanie: Jak zdecydować, jaki test należy napisać jako pierwszy? Aby odpowiedzieć na to pytanie, należy napisać zestaw historii użytkowników.

Historia użytkownika to bardzo krótki (zwykle jeden z zdań) opis wymagania dotyczącego oprogramowania. Powinien to być nieobsługiwny opis wymagania napisanego z perspektywy użytkownika.

Oto zestaw scenariuszy użytkowników opisujących funkcje wymagane przez nową funkcję grupy kontaktów:

  1. Użytkownik może wyświetlić listę grup kontaktów.
  2. Użytkownik może utworzyć nową grupę kontaktów.
  3. Użytkownik może usunąć istniejącą grupę kontaktów.
  4. Użytkownik może wybrać grupę kontaktów podczas tworzenia nowego kontaktu.
  5. Użytkownik może wybrać grupę kontaktów podczas edytowania istniejącego kontaktu.
  6. Lista grup kontaktów jest wyświetlana w widoku Indeks.
  7. Gdy użytkownik kliknie grupę kontaktów, zostanie wyświetlona lista pasujących kontaktów.

Zwróć uwagę, że ta lista historii użytkowników jest całkowicie zrozumiała dla klienta. Nie ma wzmianki o szczegółach implementacji technicznej.

Podczas tworzenia aplikacji zestaw scenariuszy użytkowników może stać się bardziej wyrafinowany. Możesz podzielić historię użytkownika na wiele scenariuszy (wymagania). Możesz na przykład zdecydować, że utworzenie nowej grupy kontaktów powinno obejmować walidację. Przesyłanie grupy kontaktów bez nazwy powinno zwrócić błąd weryfikacji.

Po utworzeniu listy scenariuszy użytkownika możesz przystąpić do pisania pierwszego testu jednostkowego. Zaczniemy od utworzenia testu jednostkowego na potrzeby wyświetlania listy grup kontaktów.

Wyświetlanie listy grup kontaktów

Nasza pierwsza historia użytkownika polega na tym, że użytkownik powinien mieć możliwość wyświetlania listy grup kontaktów. Musimy wyrazić tę historię przy użyciu testu.

Utwórz nowy test jednostkowy, klikając prawym przyciskiem myszy folder Controllers w projekcie ContactManager.Tests, wybierając pozycję Dodaj, Nowy test i wybierając szablon Testu jednostkowego (zobacz Rysunek 1). Nazwij nowy test jednostkowy GroupControllerTest.cs i kliknij przycisk OK .

Dodawanie testu jednostkowego GroupControllerTest

Rysunek 01. Dodawanie testu jednostkowego GroupControllerTest (Kliknij, aby wyświetlić obraz pełnowymiarowy)

Nasz pierwszy test jednostkowy znajduje się na liście 1. Ten test sprawdza, czy metoda Index() kontrolera grupy zwraca zestaw grup. Test sprawdza, czy kolekcja grup jest zwracana w widoku danych.

Lista 1 — Controllers\GroupControllerTest.cs

using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Web.Mvc;
using ContactManager.Models;

namespace ContactManager.Tests.Controllers
{
    [TestClass]
    public class GroupControllerTest
    {

        [TestMethod]
        public void Index()
        {
            // Arrange
            var controller = new GroupController();

            // Act
            var result = (ViewResult)controller.Index();
        
            // Assert
            Assert.IsInstanceOfType(result.ViewData.Model, typeof(IEnumerable));
        }
    }
}

Podczas pierwszego wpisywania kodu w programie Listing 1 w programie Visual Studio otrzymasz wiele czerwonych falochłych wierszy. Nie utworzyliśmy klas GroupController ani Group.

W tym momencie nie możemy nawet skompilować naszej aplikacji, abyśmy nie mogli wykonać pierwszego testu jednostkowego. To dobre. To liczy się jako test zakończony niepowodzeniem. W związku z tym mamy teraz uprawnienia do rozpoczęcia pisania kodu aplikacji. Musimy napisać wystarczająco dużo kodu, aby wykonać nasz test.

Klasa kontrolera grupy na liście 2 zawiera minimum kodu wymaganego do wykonania testu jednostkowego. Akcja Index() zwraca statycznie zakodowaną listę grup (klasa Grupa jest zdefiniowana w liście 3).

Lista 2 — Controllers\GroupController.cs

using System.Collections.Generic;
using System.Web.Mvc;
using ContactManager.Models;

namespace ContactManager.Controllers
{
    public class GroupController : Controller
    {
        public ActionResult Index()
        {
            var groups = new List();
            return View(groups);
        }

    }
}

Lista 3 — Models\Group.cs

namespace ContactManager.Models
{
    public class Group
    {
    }
}

Po dodaniu klas GroupController i Group do naszego projektu nasz pierwszy test jednostkowy zakończy się pomyślnie (zobacz Rysunek 2). Wykonaliśmy minimalną pracę wymaganą do wykonania testu. Nadszedł czas, aby świętować.

Sukces!

Rysunek 02. Sukces! (Kliknij, aby wyświetlić obraz w pełnym rozmiarze)

Tworzenie grup kontaktów

Teraz możemy przejść do drugiego scenariusza użytkownika. Musimy mieć możliwość tworzenia nowych grup kontaktów. Musimy wyrazić tę intencję za pomocą testu.

Test w liście Lista 4 sprawdza, czy wywołanie metody Create() z nową grupą dodaje grupę do listy grup zwracanych przez metodę Index(). Innymi słowy, jeśli utworzym nową grupę, powinienem mieć możliwość przywrócenia nowej grupy z listy grup zwróconych przez metodę Index().

Lista 4 — Controllers\GroupControllerTest.cs

[TestMethod]
public void Create()
{
    // Arrange
    var controller = new GroupController();

    // Act
    var groupToCreate = new Group();
    controller.Create(groupToCreate);

    // Assert
    var result = (ViewResult)controller.Index();
    var groups = (IEnumerable<Group>)result.ViewData.Model;
    CollectionAssert.Contains(groups.ToList(), groupToCreate);
}

Test w aplikacji Listing 4 wywołuje metodę Create() kontrolera grupy z nową grupą kontaktów. Następnie test sprawdza, czy wywołanie metody Index() kontrolera grupy zwraca nową grupę w widoku danych.

Zmodyfikowany kontroler grupy na liście 5 zawiera minimalne zmiany wymagane do wykonania nowego testu.

Lista 5 — Controllers\GroupController.cs

using System.Collections.Generic;
using System.Web.Mvc;
using ContactManager.Models;
using System.Collections;

namespace ContactManager.Controllers
{
    public class GroupController : Controller
    {
        private IList<Group> _groups = new List<Group>();

        public ActionResult Index()
        {
            return View(_groups);
        }

        public ActionResult Create(Group groupToCreate)
        {
            _groups.Add(groupToCreate);
            return RedirectToAction("Index");
        }
    }
}

Kontroler grupy na liście 5 ma nową akcję Create(). Ta akcja powoduje dodanie grupy do kolekcji grup. Zwróć uwagę, że akcja Index() została zmodyfikowana w celu zwrócenia zawartości kolekcji grup.

Po raz kolejny wykonaliśmy minimalną ilość pracy wymaganą do wykonania testu jednostkowego. Po wprowadzeniu tych zmian do kontrolera grupy wszystkie nasze testy jednostkowe przechodzą pomyślnie.

Dodawanie walidacji

To wymaganie nie zostało jawnie określone w scenariuszu użytkownika. Istnieje jednak uzasadnione wymaganie, aby grupa miała nazwę. W przeciwnym razie organizowanie kontaktów w grupy nie byłoby bardzo przydatne.

Lista 6 zawiera nowy test, który wyraża tę intencję. Ten test sprawdza, czy próba utworzenia grupy bez podawania nazwy powoduje wyświetlenie komunikatu o błędzie weryfikacji w stanie modelu.

Lista 6 — Controllers\GroupControllerTest.cs

[TestMethod]
public void CreateRequiredName()
{
    // Arrange
    var controller = new GroupController();

    // Act
    var groupToCreate = new Group();
    groupToCreate.Name = String.Empty;
    var result = (ViewResult)controller.Create(groupToCreate);

    // Assert
    var error = result.ViewData.ModelState["Name"].Errors[0];
    Assert.AreEqual("Name is required.", error.ErrorMessage);
}

Aby spełnić ten test, musimy dodać właściwość Name do klasy Group (zobacz Lista 7). Ponadto musimy dodać niewielką logikę weryfikacji do akcji Create() kontrolera grupy (zobacz Lista 8).

Lista 7 — Models\Group.cs

namespace ContactManager.Models
{
    public class Group
    {
        public string Name { get; set; }
    }
}

Lista 8 — Controllers\GroupController.cs

public ActionResult Create(Group groupToCreate)
{
    // Validation logic
    if (groupToCreate.Name.Trim().Length == 0)
    {
        ModelState.AddModelError("Name", "Name is required.");
        return View("Create");
    }
    
    // Database logic
    _groups.Add(groupToCreate);
    return RedirectToAction("Index");
}

Zwróć uwagę, że akcja Utwórz() kontrolera grupy zawiera teraz zarówno walidację, jak i logikę bazy danych. Obecnie baza danych używana przez kontroler grupy składa się z niczego więcej niż kolekcji w pamięci.

Czas refaktoryzacji

Trzecim krokiem w obszarze Red/Green/Refactor jest część Refaktoryzacja. W tym momencie musimy wrócić z naszego kodu i zastanowić się, jak możemy refaktoryzować naszą aplikację, aby ulepszyć jej projekt. Etap refaktoryzacji to etap, na którym trudno jest myśleć o najlepszym sposobie implementowania zasad i wzorców projektowania oprogramowania.

Możemy zmodyfikować nasz kod w dowolny sposób, aby ulepszyć projektowanie kodu. Mamy siatkę bezpieczeństwa testów jednostkowych, które uniemożliwiają nam przerwanie istniejących funkcji.

W tej chwili kontroler grupy jest bałaganem z perspektywy dobrego projektowania oprogramowania. Kontroler grupy zawiera splątane bałagan weryfikacji i kodu dostępu do danych. Aby uniknąć naruszenia zasady o pojedynczej odpowiedzialności, musimy podzielić te obawy na różne klasy.

Nasza refaktoryzowana klasa kontrolera grupy znajduje się na liście 9. Kontroler został zmodyfikowany w celu korzystania z warstwy usługi ContactManager. Jest to ta sama warstwa usługi, która jest używana z kontrolerem kontaktów.

Lista 10 zawiera nowe metody dodane do warstwy usługi ContactManager w celu obsługi walidacji, wyświetlania listy i tworzenia grup. Interfejs IContactManagerService został zaktualizowany w celu uwzględnienia nowych metod.

Lista 11 zawiera nową klasę FakeContactManagerRepository, która implementuje interfejs IContactManagerRepository. W przeciwieństwie do klasy EntityContactManagerRepository, która implementuje również interfejs IContactManagerRepository, nasza nowa klasa FakeContactManagerRepository nie komunikuje się z bazą danych. Klasa FakeContactManagerRepository używa kolekcji w pamięci jako serwera proxy dla bazy danych. Użyjemy tej klasy w naszych testach jednostkowych jako fałszywej warstwy repozytorium.

Lista 9 — Controllers\GroupController.cs

using System.Web.Mvc;
using ContactManager.Models;

namespace ContactManager.Controllers
{
    public class GroupController : Controller
    {

        private IContactManagerService _service;

        public GroupController()
        {
            _service = new ContactManagerService(new ModelStateWrapper(this.ModelState));
        }

        public GroupController(IContactManagerService service)
        {
            _service = service;
        }

        public ActionResult Index()
        {
            return View(_service.ListGroups());
        }

        public ActionResult Create(Group groupToCreate)
        {
            if (_service.CreateGroup(groupToCreate))
                return RedirectToAction("Index");
            return View("Create");
        }
    }
}

Lista 10 — Controllers\ContactManagerService.cs

public bool ValidateGroup(Group groupToValidate)
{
    if (groupToValidate.Name.Trim().Length == 0)
       _validationDictionary.AddError("Name", "Name is required.");
    return _validationDictionary.IsValid;
}

public bool CreateGroup(Group groupToCreate)
{
    // Validation logic
    if (!ValidateGroup(groupToCreate))
        return false;

    // Database logic
    try
    {
        _repository.CreateGroup(groupToCreate);
    }
    catch
    {
        return false;
    }
    return true;
}

public IEnumerable<Group> ListGroups()
{
    return _repository.ListGroups();
}

Lista 11 — Controllers\FakeContactManagerRepository.cs

using System;
using System.Collections.Generic;
using ContactManager.Models;

namespace ContactManager.Tests.Models
{
    public class FakeContactManagerRepository : IContactManagerRepository
    {
        private IList<Group> _groups = new List<Group>(); 
        
        #region IContactManagerRepository Members

        // Group methods

        public Group CreateGroup(Group groupToCreate)
        {
            _groups.Add(groupToCreate);
            return groupToCreate;
        }

        public IEnumerable<Group> ListGroups()
        {
            return _groups;
        }

        // Contact methods
        
        public Contact CreateContact(Contact contactToCreate)
        {
            throw new NotImplementedException();
        }

        public void DeleteContact(Contact contactToDelete)
        {
            throw new NotImplementedException();
        }

        public Contact EditContact(Contact contactToEdit)
        {
            throw new NotImplementedException();
        }

        public Contact GetContact(int id)
        {
            throw new NotImplementedException();
        }

        public IEnumerable<Contact> ListContacts()
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}

Zmodyfikowanie interfejsu IContactManagerRepository wymaga użycia metody CreateGroup() i ListGroups() w klasie EntityContactManagerRepository. Najbardziej leniwym i najszybszym sposobem na to jest dodanie metod wycinków, które wyglądają następująco:

public Group CreateGroup(Group groupToCreate)
{
    throw new NotImplementedException();
}

public IEnumerable<Group> ListGroups()
{
    throw new NotImplementedException();
}

Na koniec te zmiany w projekcie aplikacji wymagają wprowadzenia pewnych modyfikacji w naszych testach jednostkowych. Teraz musimy użyć repozytorium FakeContactManager podczas wykonywania testów jednostkowych. Zaktualizowana klasa GroupControllerTest znajduje się na liście 12.

Lista 12 — Controllers\GroupControllerTest.cs

using System.Collections.Generic;
using System.Web.Mvc;
using ContactManager.Controllers;
using ContactManager.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Collections;
using System.Linq;
using System;
using ContactManager.Tests.Models;

namespace ContactManager.Tests.Controllers
{
    [TestClass]
    public class GroupControllerTest
    {
        private IContactManagerRepository _repository;
        private ModelStateDictionary _modelState;
        private IContactManagerService _service;

        [TestInitialize]
        public void Initialize()
        {
            _repository = new FakeContactManagerRepository();
            _modelState = new ModelStateDictionary();
            _service = new ContactManagerService(new ModelStateWrapper(_modelState), _repository);

        }

        [TestMethod]
        public void Index()
        {
            // Arrange
            var controller = new GroupController(_service);

            // Act
            var result = (ViewResult)controller.Index();
        
            // Assert
            Assert.IsInstanceOfType(result.ViewData.Model, typeof(IEnumerable));
        }

        [TestMethod]
        public void Create()
        {
            // Arrange
            var controller = new GroupController(_service);

            // Act
            var groupToCreate = new Group();
            groupToCreate.Name = "Business";
            controller.Create(groupToCreate);

            // Assert
            var result = (ViewResult)controller.Index();
            var groups = (IEnumerable)result.ViewData.Model;
            CollectionAssert.Contains(groups.ToList(), groupToCreate);
        }

        [TestMethod]
        public void CreateRequiredName()
        {
            // Arrange
            var controller = new GroupController(_service);

            // Act
            var groupToCreate = new Group();
            groupToCreate.Name = String.Empty;
            var result = (ViewResult)controller.Create(groupToCreate);

            // Assert
            var error = _modelState["Name"].Errors[0];
            Assert.AreEqual("Name is required.", error.ErrorMessage);
        }
    
    }
}

Po wprowadzeniu wszystkich tych zmian ponownie wszystkie nasze testy jednostkowe przechodzą pomyślnie. Ukończyliśmy cały cykl refaktoryzacji Red/Green/Refactor. Zaimplementowaliśmy dwa pierwsze scenariusze użytkownika. Mamy teraz obsługę testów jednostkowych dla wymagań wyrażonych w scenariuszach użytkownika. Zaimplementowanie pozostałej części scenariuszy użytkownika obejmuje powtórzenie tego samego cyklu Red/Green/Refactor.

Modyfikowanie bazy danych

Niestety, mimo że spełniliśmy wszystkie wymagania wyrażone przez nasze testy jednostkowe, nasza praca nie jest wykonywana. Nadal musimy zmodyfikować naszą bazę danych.

Musimy utworzyć nową tabelę bazy danych grupy. Wykonaj następujące kroki:

  1. W oknie Eksplorator serwera kliknij prawym przyciskiem myszy folder Tables i wybierz opcję menu Dodaj nową tabelę.
  2. Wprowadź dwie kolumny opisane poniżej w Projektant tabeli.
  3. Oznacz kolumnę Id jako klucz podstawowy i kolumnę Tożsamość.
  4. Zapisz nową tabelę o nazwie Grupy, klikając ikonę dyskietki.

Nazwa kolumny Typ danych Zezwalaj na wartości null
Id int Fałsz
Nazwa nvarchar(50) Fałsz

Następnie musimy usunąć wszystkie dane z tabeli Contacts (w przeciwnym razie nie będziemy mogli utworzyć relacji między tabelami Kontakty i Grupy). Wykonaj następujące kroki:

  1. Kliknij prawym przyciskiem myszy tabelę Kontakty i wybierz opcję menu Pokaż dane tabeli.
  2. Usuń wszystkie wiersze.

Następnie musimy zdefiniować relację między tabelą bazy danych Grupy a istniejącą tabelą bazy danych Kontakty. Wykonaj następujące kroki:

  1. Kliknij dwukrotnie tabelę Kontakty w oknie Eksplorator serwera, aby otworzyć Projektant tabeli.
  2. Dodaj nową kolumnę całkowitą do tabeli Kontakty o nazwie GroupId.
  3. Kliknij przycisk Relacja, aby otworzyć okno dialogowe Relacje kluczy obcych (zobacz Rysunek 3).
  4. Kliknij przycisk Dodaj.
  5. Kliknij przycisk wielokropka, który zostanie wyświetlony obok przycisku Specyfikacja tabeli i kolumn.
  6. W oknie dialogowym Tabele i kolumny wybierz pozycję Grupy jako tabelę klucza podstawowego i identyfikator jako kolumnę klucza podstawowego. Wybierz pozycję Kontakty jako tabelę kluczy obcych i GroupId jako kolumnę klucza obcego (zobacz Rysunek 4). Kliknij przycisk OK.
  7. W obszarze INSERT and UPDATE Specification (Specyfikacja wstawiania i aktualizacji) wybierz wartość Cascade for Delete Rule (Kaskada usuwania reguły).
  8. Kliknij przycisk Zamknij, aby zamknąć okno dialogowe Relacje kluczy obcych.
  9. Kliknij przycisk Zapisz, aby zapisać zmiany w tabeli Kontakty.

Tworzenie relacji tabeli bazy danych

Rysunek 03. Tworzenie relacji tabeli bazy danych (kliknij, aby wyświetlić obraz o pełnym rozmiarze)

Określanie relacji tabeli

Rysunek 04. Określanie relacji tabeli (kliknij, aby wyświetlić obraz pełnowymiarowy)

Aktualizowanie naszego modelu danych

Następnie musimy zaktualizować nasz model danych, aby reprezentować nową tabelę bazy danych. Wykonaj następujące kroki:

  1. Kliknij dwukrotnie plik ContactManagerModel.edmx w folderze Models, aby otworzyć Projektant jednostki.
  2. Kliknij prawym przyciskiem myszy powierzchnię Projektant i wybierz opcję menu Aktualizuj model z bazy danych.
  3. W Kreatorze aktualizacji wybierz tabelę Grupy i kliknij przycisk Zakończ (zobacz Rysunek 5).
  4. Kliknij prawym przyciskiem myszy jednostkę Grupy i wybierz opcję menu Zmień nazwę. Zmień nazwę jednostki Grupy na Grupowanie (pojedyncza).
  5. Kliknij prawym przyciskiem myszy właściwość nawigacji Grupy, która jest wyświetlana w dolnej części jednostki Contact. Zmień nazwę właściwości nawigacji Grupy na Grupowanie (pojedyncza).

Aktualizowanie modelu programu Entity Framework z bazy danych

Rysunek 05. Aktualizowanie modelu programu Entity Framework z bazy danych (kliknij, aby wyświetlić obraz w pełnym rozmiarze)

Po wykonaniu tych kroków model danych będzie reprezentować tabele Kontakty i Grupy. Jednostka Projektant powinna zawierać obie jednostki (zobacz Rysunek 6).

Projektant jednostki wyświetla grupę i kontakt

Rysunek 06. Jednostka Projektant wyświetlania pozycji Grupa i Kontakt(Kliknij, aby wyświetlić obraz w pełnym rozmiarze)

Tworzenie klas repozytorium

Następnie musimy zaimplementować naszą klasę repozytorium. W trakcie tej iteracji dodaliśmy kilka nowych metod do interfejsu IContactManagerRepository podczas pisania kodu spełniającego nasze testy jednostkowe. Ostateczna wersja interfejsu IContactManagerRepository jest zawarta na liście 14.

Lista 14 — Models\IContactManagerRepository.cs

using System.Collections.Generic;

namespace ContactManager.Models
{
    public interface IContactManagerRepository
    {
        // Contact methods
        Contact CreateContact(int groupId, Contact contactToCreate);
        void DeleteContact(Contact contactToDelete);
        Contact EditContact(int groupId, Contact contactToEdit);
        Contact GetContact(int id);

        // Group methods
        Group CreateGroup(Group groupToCreate);
        IEnumerable<Group> ListGroups();
        Group GetGroup(int groupId);
        Group GetFirstGroup();
        void DeleteGroup(Group groupToDelete);
    }
}

W rzeczywistości nie zaimplementowaliśmy żadnej z metod związanych z pracą z grupami kontaktów. Obecnie klasa EntityContactManagerRepository zawiera metody wycinka dla każdej z metod grupy kontaktów wymienionych w interfejsie IContactManagerRepository. Na przykład metoda ListGroups() wygląda obecnie następująco:

public IEnumerable<Group> ListGroups()
{
    throw new NotImplementedException();
}

Metody wycinka umożliwiły kompilowanie aplikacji i przekazywanie testów jednostkowych. Jednak teraz nadszedł czas, aby rzeczywiście zaimplementować te metody. Ostateczna wersja klasy EntityContactManagerRepository znajduje się w liście 13.

Lista 13 — Models\EntityContactManagerRepository.cs

using System.Collections.Generic;
using System.Linq;
using System;

namespace ContactManager.Models
{
    public class EntityContactManagerRepository : ContactManager.Models.IContactManagerRepository
    {
        private ContactManagerDBEntities _entities = new ContactManagerDBEntities();

        // Contact methods

        public Contact GetContact(int id)
        {
            return (from c in _entities.ContactSet.Include("Group")
                    where c.Id == id
                    select c).FirstOrDefault();
        }

        public Contact CreateContact(int groupId, Contact contactToCreate)
        {
            // Associate group with contact
            contactToCreate.Group = GetGroup(groupId);

            // Save new contact
            _entities.AddToContactSet(contactToCreate);
            _entities.SaveChanges();
            return contactToCreate;
        }

        public Contact EditContact(int groupId, Contact contactToEdit)
        {
            // Get original contact
            var originalContact = GetContact(contactToEdit.Id);
            
            // Update with new group
            originalContact.Group = GetGroup(groupId);
            
            // Save changes
            _entities.ApplyPropertyChanges(originalContact.EntityKey.EntitySetName, contactToEdit);
            _entities.SaveChanges();
            return contactToEdit;
        }

        public void DeleteContact(Contact contactToDelete)
        {
            var originalContact = GetContact(contactToDelete.Id);
            _entities.DeleteObject(originalContact);
            _entities.SaveChanges();
        }

        public Group CreateGroup(Group groupToCreate)
        {
            _entities.AddToGroupSet(groupToCreate);
            _entities.SaveChanges();
            return groupToCreate;
        }

        // Group Methods

        public IEnumerable<Group> ListGroups()
        {
            return _entities.GroupSet.ToList();
        }

        public Group GetFirstGroup()
        {
            return _entities.GroupSet.Include("Contacts").FirstOrDefault();
        }

        public Group GetGroup(int id)
        {
            return (from g in _entities.GroupSet.Include("Contacts")
                       where g.Id == id
                       select g).FirstOrDefault();
        }

        public void DeleteGroup(Group groupToDelete)
        {
            var originalGroup = GetGroup(groupToDelete.Id);
            _entities.DeleteObject(originalGroup);
            _entities.SaveChanges();

        }

    }
}

Tworzenie widoków

ASP.NET aplikacji MVC podczas korzystania z domyślnego aparatu wyświetlania ASP.NET. W związku z tym nie tworzy się widoków w odpowiedzi na określony test jednostkowy. Jednak ponieważ aplikacja byłaby bezużyteczna bez widoków, nie można ukończyć tej iteracji bez tworzenia i modyfikowania widoków zawartych w aplikacji Contact Manager.

Musimy utworzyć następujące nowe widoki do zarządzania grupami kontaktów (zobacz Rysunek 7):

  • Views\Group\Index.aspx — wyświetla listę grup kontaktów
  • Views\Group\Delete.aspx — wyświetla formularz potwierdzenia usuwania grupy kontaktów

Widok Indeks grupy

Rysunek 07. Widok indeksu grupy (kliknij, aby wyświetlić obraz pełnowymiarowy)

Musimy zmodyfikować następujące istniejące widoki, aby obejmowały grupy kontaktów:

  • Views\Home\Create.aspx
  • Views\Home\Edit.aspx
  • Views\Home\Index.aspx

Zmodyfikowane widoki można wyświetlić, przeglądając aplikację programu Visual Studio, która towarzyszy temu samouczkowi. Na przykład rysunek 8 przedstawia widok Indeks kontaktów.

Widok Indeks kontaktów

Rysunek 08. Widok indeksu kontaktu (kliknij, aby wyświetlić obraz pełnowymiarowy)

Podsumowanie

W tej iteracji dodaliśmy nowe funkcje do naszej aplikacji Contact Manager, postępując zgodnie z metodologią projektowania aplikacji programistycznych opartą na testach. Zaczęliśmy od utworzenia zestawu scenariuszy użytkownika. Utworzyliśmy zestaw testów jednostkowych odpowiadający wymaganiom wyrażonym przez scenariusze użytkownika. Na koniec napisaliśmy wystarczająco dużo kodu, aby spełnić wymagania wyrażone przez testy jednostkowe.

Po zakończeniu pisania wystarczającej ilości kodu, aby spełnić wymagania wyrażone przez testy jednostkowe, zaktualizowaliśmy bazę danych i widoki. Dodaliśmy nową tabelę Grupy do bazy danych i zaktualizowaliśmy model danych programu Entity Framework. Utworzyliśmy również i zmodyfikowaliśmy zestaw widoków.

W następnej iteracji — ostatniej iteracji — ponownie napiszemy aplikację, aby skorzystać z technologii Ajax. Korzystając z technologii Ajax, poprawimy czas odpowiedzi i wydajność aplikacji Contact Manager.