다음을 통해 공유


반복 #6 - 테스트 중심 개발 사용(VB)

작성자: Microsoft

코드 다운로드

이 여섯 번째 반복에서는 단위 테스트를 먼저 작성하고 단위 테스트에 대한 코드를 작성하여 애플리케이션에 새로운 기능을 추가합니다. 이 반복에서는 연락처 그룹을 추가합니다.

연락처 관리 ASP.NET MVC 애플리케이션 빌드(VB)

이 자습서 시리즈에서는 처음부터 끝까지 전체 연락처 관리 애플리케이션을 빌드합니다. 연락처 관리자 애플리케이션을 사용하면 사용자 목록에 대한 연락처 정보(이름, 전화 번호 및 전자 메일 주소)를 저장할 수 있습니다.

여러 반복을 통해 애플리케이션을 빌드합니다. 반복할 때마다 애플리케이션을 점진적으로 개선합니다. 이 다중 반복 방법의 목표는 각 변경 이유를 이해할 수 있도록 하는 것입니다.

  • 반복 #1 - 애플리케이션을 만듭니다. 첫 번째 반복에서는 가능한 가장 간단한 방법으로 연락처 관리자를 만듭니다. 기본 데이터베이스 작업인 CRUD(만들기, 읽기, 업데이트 및 삭제)에 대한 지원을 추가합니다.

  • 반복 #2 - 애플리케이션을 멋지게 만듭니다. 이 반복에서는 기본 ASP.NET MVC 보기 master 페이지 및 계단식 스타일시트를 수정하여 애플리케이션의 모양을 개선합니다.

  • 반복 #3 - 양식 유효성 검사를 추가합니다. 세 번째 반복에서는 기본 양식 유효성 검사를 추가합니다. 사용자가 필요한 양식 필드를 완료하지 않고 양식을 제출하지 못하도록 합니다. 또한 이메일 주소 및 전화 번호의 유효성을 검사합니다.

  • 반복 #4 - 애플리케이션을 느슨하게 결합합니다. 이 네 번째 반복에서는 여러 소프트웨어 디자인 패턴을 활용하여 Contact Manager 애플리케이션을 더 쉽게 유지 관리하고 수정할 수 있습니다. 예를 들어 리포지토리 패턴 및 종속성 주입 패턴을 사용하도록 애플리케이션을 리팩터링합니다.

  • 반복 #5 - 단위 테스트 만들기 다섯 번째 반복에서는 단위 테스트를 추가하여 애플리케이션을 더 쉽게 유지 관리하고 수정할 수 있습니다. 데이터 모델 클래스를 모의하고 컨트롤러 및 유효성 검사 논리에 대한 단위 테스트를 빌드합니다.

  • 반복 #6 - 테스트 기반 개발을 사용합니다. 이 여섯 번째 반복에서는 단위 테스트를 먼저 작성하고 단위 테스트에 대한 코드를 작성하여 애플리케이션에 새로운 기능을 추가합니다. 이 반복에서는 연락처 그룹을 추가합니다.

  • 반복 #7 - Ajax 기능 추가 일곱 번째 반복에서는 Ajax에 대한 지원을 추가하여 애플리케이션의 응답성과 성능을 향상시킵니다.

이 반복

Contact Manager 애플리케이션의 이전 반복에서는 코드에 대한 안전망을 제공하는 단위 테스트를 만들었습니다. 단위 테스트를 만드는 동기는 코드를 변경하기 위한 복원력을 높이는 것이었습니다. 단위 테스트가 적용되면 코드를 변경하고 기존 기능이 손상되었는지 즉시 알 수 있습니다.

이 반복에서는 완전히 다른 용도로 단위 테스트를 사용합니다. 이 반복에서는 테스트 기반 개발이라는 애플리케이션 디자인 철학의 일부로 단위 테스트를 사용합니다. 테스트 기반 개발을 연습할 때 먼저 테스트를 작성한 다음 테스트에 대한 코드를 작성합니다.

보다 정확하게 말하면, 테스트 기반 개발을 연습할 때 코드를 만들 때 완료하는 세 단계(빨간색/녹색/리팩터링)가 있습니다.

  1. 실패한 단위 테스트 작성(빨간색)
  2. 단위 테스트를 통과하는 코드 작성(녹색)
  3. 코드 리팩터링(리팩터링)

먼저 단위 테스트를 작성합니다. 단위 테스트는 코드가 작동할 것으로 예상되는 방식에 대한 의도를 표현해야 합니다. 단위 테스트를 처음 만들면 단위 테스트가 실패합니다. 테스트를 충족하는 애플리케이션 코드를 아직 작성하지 않았기 때문에 테스트가 실패해야 합니다.

다음으로 단위 테스트를 통과하기 위해 충분한 코드를 작성합니다. 목표는 가능한 가장 지연되고, 가장 빠르며, 가장 빠른 방법으로 코드를 작성하는 것입니다. 애플리케이션의 아키텍처를 생각하는 데 시간을 낭비해서는 안 됩니다. 대신 단위 테스트에서 표현한 의도를 충족하는 데 필요한 최소한의 코드를 작성하는 데 집중해야 합니다.

마지막으로, 충분한 코드를 작성한 후 한 걸음 물러서서 애플리케이션의 전체 아키텍처를 고려할 수 있습니다. 이 단계에서는 리포지토리 패턴과 같은 소프트웨어 디자인 패턴을 활용하여 코드를 다시 작성(리팩터링)하여 코드를 더 쉽게 유지 관리할 수 있습니다. 코드가 단위 테스트로 처리되므로 이 단계에서 코드를 두려움 없이 다시 작성할 수 있습니다.

테스트 기반 개발을 연습하면 많은 이점이 있습니다. 첫째, 테스트 기반 개발에서는 실제로 작성해야 하는 코드에 집중해야 합니다. 특정 테스트를 통과하기에 충분한 코드를 작성하는 데 지속적으로 초점을 맞추고 있기 때문에 잡초로 방황하고 사용하지 않을 엄청난 양의 코드를 작성할 수 없습니다.

둘째, "첫 번째 테스트" 디자인 방법론을 사용하면 코드를 사용하는 방법의 관점에서 코드를 작성해야 합니다. 즉, 테스트 기반 개발을 연습할 때 사용자 관점에서 테스트를 지속적으로 작성합니다. 따라서 테스트 기반 개발로 인해 더 깨끗하고 이해하기 쉬운 API가 발생할 수 있습니다.

마지막으로 테스트 기반 개발에서는 애플리케이션을 작성하는 일반적인 프로세스의 일부로 단위 테스트를 작성해야 합니다. 프로젝트 최종 기한이 다가오면 테스트는 일반적으로 창 밖으로 나가는 첫 번째 작업입니다. 반면에 테스트 기반 개발을 연습할 때는 테스트 기반 개발로 인해 단위 테스트가 애플리케이션 빌드 프로세스의 중심이 되기 때문에 단위 테스트 작성에 대해 유덕할 가능성이 더 높습니다.

참고

테스트 기반 개발에 대해 자세히 알아보려면 레거시 코드로 효과적으로 작업하는 Michael Feathers 책을 읽어보는 것이 좋습니다.

이 반복에서는 연락처 관리자 애플리케이션에 새 기능을 추가합니다. 연락처 그룹에 대한 지원을 추가합니다. 연락처 그룹을 사용하여 연락처를 비즈니스 및 친구 그룹과 같은 범주로 구성할 수 있습니다.

테스트 기반 개발 프로세스를 수행하여 이 새로운 기능을 애플리케이션에 추가하겠습니다. 먼저 단위 테스트를 작성하고 이러한 테스트에 대해 모든 코드를 작성합니다.

테스트되는 항목

이전 반복에서 설명한 것처럼 일반적으로 데이터 액세스 논리 또는 뷰 논리에 대한 단위 테스트를 작성하지 않습니다. 데이터베이스 액세스는 비교적 느린 작업이므로 데이터 액세스 논리에 대한 단위 테스트를 작성하지 않습니다. 보기에 액세스하려면 상대적으로 느린 작업인 웹 서버를 회전해야 하므로 보기 논리에 대한 단위 테스트를 작성하지 않습니다. 테스트를 매우 빠르게 반복해서 실행할 수 없다면 단위 테스트를 작성해서는 안 됩니다.

테스트 기반 개발은 단위 테스트에 의해 구동되므로 처음에는 컨트롤러 및 비즈니스 논리 작성에 집중합니다. 데이터베이스 또는 뷰를 건드리지 않습니다. 이 자습서가 끝날 때까지 데이터베이스를 수정하거나 뷰를 만들지 않습니다. 테스트할 수 있는 것부터 시작합니다.

사용자 스토리 만들기

테스트 기반 개발을 연습할 때는 항상 테스트를 작성하는 것으로 시작합니다. 이렇게 하면 즉시 질문을 제기합니다. 어떤 테스트를 먼저 작성할지 어떻게 결정해야 할까요? 이 질문에 대답하려면 일련의 사용자 스토리를 작성해야 합니다.

사용자 스토리는 소프트웨어 요구 사항에 대한 매우 간단한(일반적으로 한 문장) 설명입니다. 사용자 관점에서 작성된 요구 사항에 대한 비기술적 설명이어야 합니다.

다음은 새 연락처 그룹 기능에 필요한 기능을 설명하는 사용자 스토리 집합입니다.

  1. 사용자는 연락처 그룹 목록을 볼 수 있습니다.
  2. 사용자는 새 연락처 그룹을 만들 수 있습니다.
  3. 사용자는 기존 연락처 그룹을 삭제할 수 있습니다.
  4. 사용자는 새 연락처를 만들 때 연락처 그룹을 선택할 수 있습니다.
  5. 사용자는 기존 연락처를 편집할 때 연락처 그룹을 선택할 수 있습니다.
  6. 연락처 그룹 목록이 인덱스 보기에 표시됩니다.
  7. 사용자가 연락처 그룹을 클릭하면 일치하는 연락처 목록이 표시됩니다.

이 사용자 스토리 목록은 고객이 완전히 이해할 수 있습니다. 기술 구현 세부 정보에 대한 멘션 없습니다.

애플리케이션을 빌드하는 동안 사용자 스토리 집합이 더 구체화될 수 있습니다. 사용자 스토리를 여러 스토리(요구 사항)로 끊을 수 있습니다. 예를 들어 새 연락처 그룹을 만드는 데 유효성 검사가 포함되도록 결정할 수 있습니다. 이름 없이 연락처 그룹을 제출하면 유효성 검사 오류가 반환됩니다.

사용자 스토리 목록을 만든 후에는 첫 번째 단위 테스트를 작성할 준비가 된 것입니다. 먼저 연락처 그룹 목록을 보기 위한 단위 테스트를 만듭니다.

연락처 그룹 나열

첫 번째 사용자 스토리는 사용자가 연락처 그룹 목록을 볼 수 있어야 한다는 것입니다. 우리는 테스트로이 이야기를 표현해야합니다.

ContactManager.Tests 프로젝트에서 Controllers 폴더를 마우스 오른쪽 단추로 클릭하고 추가, 새 테스트를 선택하고 단위 테스트 템플릿을 선택하여 새 단위 테스트를 만듭니다(그림 1 참조). 새 단위 테스트의 이름을 GroupControllerTest.vb로 지정하고 확인 단추를 클릭합니다.

GroupControllerTest 단위 테스트 추가

그림 01: GroupControllerTest 단위 테스트 추가(전체 크기 이미지를 보려면 클릭)

첫 번째 단위 테스트는 목록 1에 포함되어 있습니다. 이 테스트는 그룹 컨트롤러의 Index() 메서드가 그룹 집합을 반환하는지 확인합니다. 테스트는 보기 데이터에서 그룹 컬렉션이 반환되는지 확인합니다.

목록 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

Visual Studio의 목록 1에 코드를 처음 입력하면 빨간색 물결선이 많이 표시됩니다. GroupController 또는 Group 클래스를 만들지 않았습니다.

이 시점에서 첫 번째 단위 테스트를 실행할 수 없도록 애플리케이션을 빌드할 수도 없습니다. 그건 좋은. 이는 실패한 테스트로 간주됩니다. 따라서 이제 애플리케이션 코드 작성을 시작할 수 있는 권한이 있습니다. 테스트를 실행하려면 충분한 코드를 작성해야 합니다.

목록 2의 그룹 컨트롤러 클래스에는 단위 테스트를 통과하는 데 필요한 최소한의 코드가 포함되어 있습니다. Index() 작업은 정적으로 코딩된 그룹 목록을 반환합니다(그룹 클래스는 목록 3에 정의됨).

목록 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

목록 3 - Models\Group.vb

Public Class Group

End Class

GroupController 및 Group 클래스를 프로젝트에 추가하면 첫 번째 단위 테스트가 성공적으로 완료됩니다(그림 2 참조). 테스트를 통과하는 데 필요한 최소 작업을 수행했습니다. 축하할 시간입니다.

성공!

그림 02: 성공! (전체 크기 이미지를 보려면 클릭)

연락처 그룹 만들기

이제 두 번째 사용자 스토리로 이동할 수 있습니다. 새 연락처 그룹을 만들 수 있어야 합니다. 우리는 테스트로이 의도를 표현해야합니다.

목록 4의 테스트는 새 그룹을 사용하여 Create() 메서드를 호출하면 Index() 메서드에서 반환된 그룹 목록에 그룹을 추가하는지 확인합니다. 즉, 새 그룹을 만들면 Index() 메서드에서 반환된 그룹 목록에서 새 그룹을 다시 가져올 수 있습니다.

목록 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

목록 4의 테스트는 새 연락처 그룹을 사용하여 그룹 컨트롤러 Create() 메서드를 호출합니다. 다음으로, 테스트는 그룹 컨트롤러 Index() 메서드를 호출하면 뷰 데이터에서 새 그룹을 반환하는지 확인합니다.

목록 5의 수정된 그룹 컨트롤러에는 새 테스트를 통과하는 데 필요한 최소한의 변경 내용이 포함되어 있습니다.

목록 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

목록 5의 그룹 컨트롤러에는 새 Create() 작업이 있습니다. 이 작업은 그룹 컬렉션에 그룹을 추가합니다. Groups 컬렉션의 내용을 반환하도록 Index() 작업이 수정되었습니다.

다시 한 번 단위 테스트를 통과하는 데 필요한 최소한의 작업을 수행했습니다. 그룹 컨트롤러를 변경한 후 모든 단위 테스트가 통과합니다.

유효성 검사 추가

이 요구 사항은 사용자 스토리에 명시적으로 명시되지 않았습니다. 그러나 그룹에 이름이 있어야 하는 것이 합리적입니다. 그렇지 않으면 연락처를 그룹으로 구성하는 것은 매우 유용하지 않습니다.

목록 6에는 이 의도를 표현하는 새 테스트가 포함되어 있습니다. 이 테스트는 이름을 제공하지 않고 그룹을 만들려고 하면 모델 상태의 유효성 검사 오류 메시지가 발생하는지 확인합니다.

목록 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

이 테스트를 충족하려면 Group 클래스에 Name 속성을 추가해야 합니다(목록 7 참조). 또한 그룹 컨트롤러의 만들기() 작업에 약간의 유효성 검사 논리를 추가해야 합니다(목록 8 참조).

목록 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

목록 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

이제 그룹 컨트롤러 만들기() 작업에 유효성 검사 및 데이터베이스 논리가 모두 포함됩니다. 현재 그룹 컨트롤러에서 사용하는 데이터베이스는 메모리 내 컬렉션에 지나지 않습니다.

리팩터링할 시간

Red/Green/Refactor의 세 번째 단계는 리팩터링 부분입니다. 이 시점에서 코드에서 한 걸음 물러나서 애플리케이션을 리팩터링하여 디자인을 개선하는 방법을 고려해야 합니다. 리팩터링 단계는 소프트웨어 디자인 원칙 및 패턴을 구현하는 가장 좋은 방법에 대해 열심히 생각하는 단계입니다.

코드 디자인을 개선하기 위해 어떤 방식으로든 코드를 자유롭게 수정할 수 있습니다. 기존 기능을 중단하지 못하도록 하는 단위 테스트의 안전망이 있습니다.

지금, 우리의 그룹 컨트롤러는 좋은 소프트웨어 디자인의 관점에서 엉망이다. 그룹 컨트롤러에는 얽힌 유효성 검사 및 데이터 액세스 코드가 포함되어 있습니다. 단일 책임 원칙을 위반하지 않도록 하려면 이러한 문제를 다른 클래스로 분리해야 합니다.

리팩터링된 그룹 컨트롤러 클래스는 목록 9에 포함되어 있습니다. 컨트롤러가 ContactManager 서비스 계층을 사용하도록 수정되었습니다. 연락처 컨트롤러에서 사용하는 것과 동일한 서비스 계층입니다.

목록 10에는 그룹 유효성 검사, 나열 및 만들기를 지원하기 위해 ContactManager 서비스 계층에 추가된 새 메서드가 포함되어 있습니다. IContactManagerService 인터페이스가 새 메서드를 포함하도록 업데이트되었습니다.

목록 11에는 IContactManagerRepository 인터페이스를 구현하는 새 FakeContactManagerRepository 클래스가 포함되어 있습니다. IContactManagerRepository 인터페이스도 구현하는 EntityContactManagerRepository 클래스와 달리 새 FakeContactManagerRepository 클래스는 데이터베이스와 통신하지 않습니다. FakeContactManagerRepository 클래스는 메모리 내 컬렉션을 데이터베이스의 프록시로 사용합니다. 단위 테스트에서 이 클래스를 가짜 리포지토리 계층으로 사용합니다.

목록 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

목록 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

목록 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

IContactManagerRepository 인터페이스를 수정하려면 를 사용하여 EntityContactManagerRepository 클래스에서 CreateGroup() 및 ListGroups() 메서드를 구현해야 합니다. 이 작업을 수행하는 가장 빠르고 빠른 방법은 다음과 같은 스텁 메서드를 추가하는 것입니다.

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

마지막으로, 애플리케이션 설계를 변경하려면 단위 테스트를 일부 수정해야 합니다. 이제 단위 테스트를 수행할 때 FakeContactManagerRepository를 사용해야 합니다. 업데이트된 GroupControllerTest 클래스는 목록 12에 포함되어 있습니다.

목록 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

이러한 모든 변경을 수행한 후 다시 한 번 모든 단위 테스트가 통과합니다. Red/Green/Refactor의 전체 주기를 완료했습니다. 처음 두 사용자 스토리를 구현했습니다. 이제 사용자 스토리에 표현된 요구 사항에 대한 단위 테스트를 지원합니다. 사용자 스토리의 나머지 부분을 구현하려면 동일한 빨간색/녹색/리팩터링 주기를 반복해야 합니다.

데이터베이스 수정

아쉽게도 단위 테스트로 표현된 모든 요구 사항을 충족했지만 작업이 완료되지 않았습니다. 데이터베이스를 수정해야 합니다.

새 그룹 데이터베이스 테이블을 만들어야 합니다. 다음 단계를 수행합니다.

  1. 서버 Explorer 창에서 테이블 폴더를 마우스 오른쪽 단추로 클릭하고 메뉴 옵션 새 테이블 추가를 선택합니다.
  2. 표 Designer 아래에 설명된 두 열을 입력합니다.
  3. ID 열을 기본 키 및 ID 열로 표시합니다.
  4. 플로피 아이콘을 클릭하여 새 테이블을 그룹 이름으로 저장합니다.

열 이름 데이터 형식 Null 허용
Id int 거짓
속성 nvarchar(50) 거짓

다음으로 연락처 테이블에서 모든 데이터를 삭제해야 합니다(그렇지 않으면 연락처 테이블과 그룹 테이블 간에 관계를 만들 수 없음). 다음 단계를 수행합니다.

  1. 연락처 테이블을 마우스 오른쪽 단추로 클릭하고 테이블 데이터 표시 메뉴 옵션을 선택합니다.
  2. 모든 행을 삭제합니다.

다음으로 그룹 데이터베이스 테이블과 기존 Contacts 데이터베이스 테이블 간의 관계를 정의해야 합니다. 다음 단계를 수행합니다.

  1. 서버 Explorer 창에서 연락처 테이블을 두 번 클릭하여 테이블 Designer 엽니다.
  2. GroupId라는 연락처 테이블에 새 정수 열을 추가합니다.
  3. 관계 단추를 클릭하여 외래 키 관계 대화 상자를 엽니다(그림 3 참조).
  4. 추가 단추를 클릭합니다.
  5. 테이블 및 열 사양 단추 옆에 표시되는 줄임표 단추를 클릭합니다.
  6. 테이블 및 열 대화 상자에서 기본 키 테이블로 그룹을 선택하고 기본 키 열로 ID를 선택합니다. 연락처를 외래 키 테이블로 선택하고 GroupId를 외래 키 열로 선택합니다(그림 4 참조). 확인 단추를 클릭합니다.
  7. INSERT 및 UPDATE 사양에서 삭제 규칙에 대한 Cascade 값을 선택합니다.
  8. 닫기 단추를 클릭하여 외래 키 관계 대화 상자를 닫습니다.
  9. 저장 단추를 클릭하여 연락처 테이블에 변경 내용을 저장합니다.

데이터베이스 테이블 관계 만들기

그림 03: 데이터베이스 테이블 관계 만들기(전체 크기 이미지를 보려면 클릭)

테이블 관계 지정

그림 04: 테이블 관계 지정(전체 크기 이미지를 보려면 클릭)

데이터 모델 업데이트

다음으로 새 데이터베이스 테이블을 나타내도록 데이터 모델을 업데이트해야 합니다. 다음 단계를 수행합니다.

  1. Models 폴더에서 ContactManagerModel.edmx 파일을 두 번 클릭하여 엔터티 Designer 엽니다.
  2. Designer 화면을 마우스 오른쪽 단추로 클릭하고 데이터베이스에서 모델 업데이트 메뉴 옵션을 선택합니다.
  3. 업데이트 마법사에서 그룹 테이블을 선택하고 마침 단추를 클릭합니다(그림 5 참조).
  4. 그룹 엔터티를 마우스 오른쪽 단추로 클릭하고 메뉴 옵션 이름 바꾸기를 선택합니다. 그룹 엔터티의 이름을 Group(단수)으로 변경합니다.
  5. Contact 엔터티 아래쪽에 표시되는 그룹 탐색 속성을 마우스 오른쪽 단추로 클릭합니다. 그룹 탐색 속성의 이름을 Group(단수)으로 변경합니다.

데이터베이스에서 Entity Framework 모델 업데이트

그림 05: 데이터베이스에서 Entity Framework 모델 업데이트(전체 크기 이미지를 보려면 클릭)

이러한 단계를 완료하면 데이터 모델이 연락처 및 그룹 테이블을 모두 나타냅니다. 엔터티 Designer 두 엔터티를 모두 표시해야 합니다(그림 6 참조).

그룹 및 연락처를 표시하는 엔터티 Designer

그림 06: 그룹 및 연락처를 표시하는 엔터티 Designer(전체 크기 이미지를 보려면 클릭)

리포지토리 클래스 만들기

다음으로 리포지토리 클래스를 구현해야 합니다. 이 반복 과정에서 단위 테스트를 충족하는 코드를 작성하는 동안 IContactManagerRepository 인터페이스에 몇 가지 새로운 메서드를 추가했습니다. IContactManagerRepository 인터페이스의 최종 버전은 목록 14에 포함되어 있습니다.

목록 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

실제 EntityContactManagerRepository 클래스에서 연락처 그룹 작업과 관련된 메서드를 실제로 구현하지 않았습니다. 현재 EntityContactManagerRepository 클래스에는 IContactManagerRepository 인터페이스에 나열된 각 연락처 그룹 메서드에 대한 스텁 메서드가 있습니다. 예를 들어 ListGroups() 메서드는 현재 다음과 같습니다.

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

    throw New NotImplementedException()

End Function

스텁 메서드를 사용하면 애플리케이션을 컴파일하고 단위 테스트를 통과할 수 있습니다. 그러나 이제는 이러한 메서드를 실제로 구현해야 합니다. EntityContactManagerRepository 클래스의 최종 버전은 목록 13에 포함되어 있습니다.

목록 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

뷰 만들기

기본 ASP.NET 뷰 엔진을 사용하는 경우 MVC 애플리케이션을 ASP.NET. 따라서 특정 단위 테스트에 대한 응답으로 뷰를 만들지 않습니다. 그러나 보기가 없으면 애플리케이션을 사용할 수 없으므로 Contact Manager 애플리케이션에 포함된 보기를 만들고 수정하지 않고는 이 반복을 완료할 수 없습니다.

연락처 그룹을 관리하기 위해 다음과 같은 새 보기를 만들어야 합니다(그림 7 참조).

  • Views\Group\Index.aspx - 연락처 그룹 목록을 표시합니다.
  • Views\Group\Delete.aspx - 연락처 그룹을 삭제하기 위한 확인 양식을 표시합니다.

그룹 인덱스 보기

그림 07: 그룹 인덱스 보기(전체 크기 이미지를 보려면 클릭)

연락처 그룹을 포함하도록 다음과 같은 기존 보기를 수정해야 합니다.

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

이 자습서와 함께 제공되는 Visual Studio 애플리케이션을 보면 수정된 보기를 볼 수 있습니다. 예를 들어 그림 8은 연락처 인덱스 보기를 보여 줍니다.

연락처 인덱스 보기

그림 08: 연락처 인덱스 보기(전체 크기 이미지를 보려면 클릭)

요약

이 반복에서는 테스트 기반 개발 애플리케이션 디자인 방법론에 따라 Contact Manager 애플리케이션에 새로운 기능을 추가했습니다. 먼저 사용자 스토리 집합을 만들기 시작했습니다. 사용자 스토리에서 표현한 요구 사항에 해당하는 단위 테스트 집합을 만들었습니다. 마지막으로 단위 테스트로 표현된 요구 사항을 충족하기에 충분한 코드를 작성했습니다.

단위 테스트로 표현된 요구 사항을 충족하기에 충분한 코드 작성을 완료한 후 데이터베이스 및 뷰를 업데이트했습니다. 데이터베이스에 새 그룹 테이블을 추가하고 Entity Framework 데이터 모델을 업데이트했습니다. 또한 뷰 집합을 만들고 수정했습니다.

마지막 반복인 다음 반복에서는 Ajax를 활용하기 위해 애플리케이션을 다시 작성합니다. Ajax를 활용하여 Contact Manager 애플리케이션의 응답성과 성능을 향상할 것입니다.