Iteracja 6 — Korzystanie z projektowania opartego na testach (VB)

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 ASP.NET aplikacji MVC do zarządzania kontaktami (VB)

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.vb 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.vb

Imports Microsoft.VisualStudio.TestTools.UnitTesting
Imports System.Web.Mvc

<TestClass()> _
Public Class GroupControllerTest

    <TestMethod()> _
    Public Sub Index()
        ' Arrange
        Dim controller = New GroupController()

        ' Act
        Dim result = CType(controller.Index(), ViewResult)

        ' Assert
        Assert.IsInstanceOfType(result.ViewData.Model, GetType(IEnumerable(Of Group)))
    End Sub
End Class

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.vb

Public Class GroupController
    Inherits System.Web.Mvc.Controller

    Function Index() As ActionResult
        Dim groups = new List(Of Group)
        Return View(groups)
    End Function

End Class

Lista 3 — Models\Group.vb

Public Class Group

End Class

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.vb

<TestMethod> _
Public Sub Create()
    ' Arrange
    Dim controller = New GroupController()

    ' Act
    Dim groupToCreate = New Group()
    controller.Create(groupToCreate)

    ' Assert
    Dim result = CType(controller.Index(), ViewResult)
    Dim groups = CType(result.ViewData.Model, IEnumerable(Of Group))
    CollectionAssert.Contains(groups.ToList(), groupToCreate)
End Sub

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.vb

Public Class GroupController
Inherits Controller

Private _groups As IList(Of Group) = New List(Of Group)()

Public Function Index() As ActionResult
    Return View(_groups)
End Function

Public Function Create(ByVal groupToCreate As Group) As ActionResult
    _groups.Add(groupToCreate)
    Return RedirectToAction("Index")

End Function
End Class

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.vb

<TestMethod> _
Public Sub CreateRequiredName()
    ' Arrange
    Dim controller = New GroupController()

    ' Act
    Dim groupToCreate As New Group()
    groupToCreate.Name = String.Empty
    Dim result = CType(controller.Create(groupToCreate), ViewResult)

    ' Assert
    Dim [error] = result.ViewData.ModelState("Name").Errors(0)
    Assert.AreEqual("Name is required.", [error].ErrorMessage)
End Sub

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.vb

Public Class Group

    Private _name As String

    Public Property Name() As String
    Get
        Return _name
    End Get
    Set(ByVal value As String)
        _name = value
    End Set
End Property

End Class

Lista 8 — Controllers\GroupController.vb

Public Function Create(ByVal groupToCreate As Group) As ActionResult
    ' Validation logic
    If groupToCreate.Name.Trim().Length = 0 Then
    ModelState.AddModelError("Name", "Name is required.")
    Return View("Create")
    End If

    ' Database logic
    _groups.Add(groupToCreate)
    Return RedirectToAction("Index")
End Function

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.vb

Public Class GroupController
Inherits Controller

Private _service As IContactManagerService

Public Sub New()
    _service = New ContactManagerService(New ModelStateWrapper(Me.ModelState))

End Sub

Public Sub New(ByVal service As IContactManagerService)
    _service = service
End Sub

Public Function Index() As ActionResult
    Return View(_service.ListGroups())
End Function


Public Function Create(ByVal groupToCreate As Group) As ActionResult
    If _service.CreateGroup(groupToCreate) Then
        Return RedirectToAction("Index")
    End If
    Return View("Create")
End Function

End Class

Lista 10 — Controllers\ContactManagerService.vb

Public Function ValidateGroup(ByVal groupToValidate As Group) As Boolean
If groupToValidate.Name.Trim().Length = 0 Then
    _validationDictionary.AddError("Name", "Name is required.")
End If
Return _validationDictionary.IsValid
End Function

Public Function CreateGroup(ByVal groupToCreate As Group) As Boolean Implements IContactManagerService.CreateGroup
    ' Validation logic
    If Not ValidateGroup(groupToCreate) Then
        Return False
    End If

    ' Database logic
    Try
        _repository.CreateGroup(groupToCreate)
    Catch
        Return False
    End Try
    Return True
End Function

Public Function ListGroups() As IEnumerable(Of Group) Implements IContactManagerService.ListGroups
    Return _repository.ListGroups()
End Function

Lista 11 — Controllers\FakeContactManagerRepository.vb

Public Class FakeContactManagerRepository
Implements IContactManagerRepository

Private _groups As IList(Of Group) = New List(Of Group)()

#Region "IContactManagerRepository Members"

' Group methods

Public Function CreateGroup(ByVal groupToCreate As Group) As Group Implements IContactManagerRepository.CreateGroup
    _groups.Add(groupToCreate)
    Return groupToCreate
End Function

Public Function ListGroups() As IEnumerable(Of Group) Implements IContactManagerRepository.ListGroups
    Return _groups
End Function

' Contact methods

Public Function CreateContact(ByVal contactToCreate As Contact) As Contact Implements IContactManagerRepository.CreateContact
    Throw New NotImplementedException()
End Function

Public Sub DeleteContact(ByVal contactToDelete As Contact) Implements IContactManagerRepository.DeleteContact
    Throw New NotImplementedException()
End Sub

Public Function EditContact(ByVal contactToEdit As Contact) As Contact Implements IContactManagerRepository.EditContact
    Throw New NotImplementedException()
End Function

Public Function GetContact(ByVal id As Integer) As Contact Implements IContactManagerRepository.GetContact
    Throw New NotImplementedException()
End Function

Public Function ListContacts() As IEnumerable(Of Contact) Implements IContactManagerRepository.ListContacts
    Throw New NotImplementedException()
End Function

#End Region
End Class

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 Function CreateGroup(groupToCreate As Group) As Group Implements IContactManagerRepository.CreateGroup

    throw New NotImplementedException()

End Function 

Public Function ListGroups() As IEnumerable(Of Group) Implements IContactManagerRepository.ListGroups

    throw New NotImplementedException()

End Function

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.vb

Imports Microsoft.VisualStudio.TestTools.UnitTesting
Imports System.Web.Mvc

<TestClass()> _
Public Class GroupControllerTest

    Private _repository As IContactManagerRepository
    Private _modelState As ModelStateDictionary
    Private _service As IContactManagerService

    <TestInitialize()> _
    Public Sub Initialize()
        _repository = New FakeContactManagerRepository()
        _modelState = New ModelStateDictionary()
        _service = New ContactManagerService(New ModelStateWrapper(_modelState), _repository)
    End Sub

    <TestMethod()> _
    Public Sub Index()
        ' Arrange
        Dim controller = New GroupController(_service)

        ' Act
        Dim result = CType(controller.Index(), ViewResult)

        ' Assert
        Assert.IsInstanceOfType(result.ViewData.Model, GetType(IEnumerable(Of Group)))
    End Sub

    <TestMethod()> _
    Public Sub Create()
        ' Arrange
        Dim controller = New GroupController(_service)

        ' Act
        Dim groupToCreate = New Group()
        groupToCreate.Name = "Business"
        controller.Create(groupToCreate)

        ' Assert
        Dim result = CType(controller.Index(), ViewResult)
        Dim groups = CType(result.ViewData.Model, IEnumerable(Of Group))
        CollectionAssert.Contains(groups.ToList(), groupToCreate)
    End Sub

    <TestMethod()> _
    Public Sub CreateRequiredName()
        ' Arrange
        Dim controller = New GroupController(_service)

        ' Act
        Dim groupToCreate = New Group()
        groupToCreate.Name = String.Empty
        Dim result = CType(controller.Create(groupToCreate), ViewResult)

        ' Assert
        Dim nameError = _modelState("Name").Errors(0)
        Assert.AreEqual("Name is required.", nameError.ErrorMessage)
    End Sub

End Class

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.vb

Public Interface IContactManagerRepository
' Contact methods
Function CreateContact(ByVal groupId As Integer, ByVal contactToCreate As Contact) As Contact
Sub DeleteContact(ByVal contactToDelete As Contact)
Function EditContact(ByVal groupId As Integer, ByVal contactToEdit As Contact) As Contact
Function GetContact(ByVal id As Integer) As Contact

' Group methods
Function CreateGroup(ByVal groupToCreate As Group) As Group
Function ListGroups() As IEnumerable(Of Group)
Function GetGroup(ByVal groupId As Integer) As Group
Function GetFirstGroup() As Group
Sub DeleteGroup(ByVal groupToDelete As Group)

End Interface

W rzeczywistości nie zaimplementowaliśmy żadnej z metod związanych z pracą z grupami kontaktów w rzeczywistej klasie EntityContactManagerRepository. 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 Function ListGroups() As IEnumerable(Of Group) Implements IContactManagerRepository.ListGroups

    throw New NotImplementedException()

End Function

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.vb

Public Class EntityContactManagerRepository
Implements IContactManagerRepository

Private _entities As New ContactManagerDBEntities()

' Contact methods

Public Function GetContact(ByVal id As Integer) As Contact Implements IContactManagerRepository.GetContact
    Return (From c In _entities.ContactSet.Include("Group") _
            Where c.Id = id _
            Select c).FirstOrDefault()
End Function

Public Function CreateContact(ByVal groupId As Integer, ByVal contactToCreate As Contact) As Contact Implements IContactManagerRepository.CreateContact
    ' Associate group with contact
    contactToCreate.Group = GetGroup(groupId)

    ' Save new contact
    _entities.AddToContactSet(contactToCreate)
    _entities.SaveChanges()
    Return contactToCreate
End Function

Public Function EditContact(ByVal groupId As Integer, ByVal contactToEdit As Contact) As Contact Implements IContactManagerRepository.EditContact
    ' Get original contact
    Dim originalContact = GetContact(contactToEdit.Id)

    ' Update with new group
    originalContact.Group = GetGroup(groupId)

    ' Save changes
    _entities.ApplyPropertyChanges(originalContact.EntityKey.EntitySetName, contactToEdit)
    _entities.SaveChanges()
    Return contactToEdit
End Function

Public Sub DeleteContact(ByVal contactToDelete As Contact) Implements IContactManagerRepository.DeleteContact 
    Dim originalContact = GetContact(contactToDelete.Id)
    _entities.DeleteObject(originalContact)
    _entities.SaveChanges()
End Sub

    ' Group methods

Public Function CreateGroup(ByVal groupToCreate As Group) As Group Implements IContactManagerRepository.CreateGroup 
    _entities.AddToGroupSet(groupToCreate)
    _entities.SaveChanges()
    Return groupToCreate
End Function

Public Function ListGroups() As IEnumerable(Of Group) Implements IContactManagerRepository.ListGroups
    Return _entities.GroupSet.ToList()
End Function

Public Function GetFirstGroup() As Group Implements IContactManagerRepository.GetFirstGroup
    Return _entities.GroupSet.Include("Contacts").FirstOrDefault()
End Function

Public Function GetGroup(ByVal id As Integer) As Group Implements IContactManagerRepository.GetGroup
    Return (From g In _entities.GroupSet.Include("Contacts") _
            Where g.Id = id _
            Select g).FirstOrDefault()
End Function

Public Sub DeleteGroup(ByVal groupToDelete As Group) Implements IContactManagerRepository.DeleteGroup
    Dim originalGroup = GetGroup(groupToDelete.Id)
    _entities.DeleteObject(originalGroup)
    _entities.SaveChanges()
End Sub

End Class

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.