Knockout과 Mocha를 사용한 TDD기반 웹 개발

개요

UI를 포함하는 프로그램의 경우 Coded UI Test 등의 기술이 발전되고 있지만 MVVM 디자인 패턴을 적용할 경우 UI의 많은 부분을 전통적 테스트 기술을 사용하여 검증할 수 있습니다. 오랫동안 WPF의 전유물로만 여겨지던 MVVM 패턴은 최근 KnockoutJS와 AngularJS 등의 프레임워크의 도움을 받아 웹 응용프로그램 개발 영역까지 확대되고 있습니다.

배경

TDD(Test Driven Development)

테스트 주도 개발은 테스트를 중심으로 기능의 설계와 구현을 진행하는 개발 방법론입니다. TDD를 적용하면 필연적으로 code coverage가 높아지고 간단한 설계가 장려되며 코드 품질이 높아진다고 알려져 있습니다. TDD를 사용하여 기능을 개발하는 일반적 프로세스는 다음과 같습니다.

  1. 테스트 작성
  2. 테스트 실패
  3. 코드 작성
  4. 테스트 통과
  5. 코드 정리(refactoring)
  6. 반복

MVVM(Model View ViewModel)

MVVM 패턴은 MVC 패턴 변형의 일종입니다. WPF를 위해 만들어졌으며 현재는 웹 응용프로그램 개발에도 사용됩니다. 전통적 MVC 패턴과 비교했을 때 특징은 다음과 같습니다.

  • 뷰모델은 이름처럼 뷰의 모형이며 뷰의 구성과 행위를 정의합니다. MVVM 패턴에서 뷰는 뷰모델의 투영(projection)입니다.
  • 뷰는 뷰의 요소에 뷰모델의 속성과 명령을 바인딩하여 모델에 접근하고 뷰모델의 명령을 실행합니다.
  • 뷰모델은 뷰에 독립적입니다. 이것은 뷰와 뷰모델 사이의 약한 결합도(loose coupling)를 의미하며 뷰의 구성과 동작을 뷰를 구현하는 기술과 독립적으로 테스트할 수 있게 해줍니다.

가능성

AngularJS와 KnockoutJS 등의 MVVM 패턴을 지원하는 웹 개발 프레임워크를 사용하면 동적 사용자 환경을 제공하는 웹 페이지를 plain-old HTML와 DOM 독립적 Javascript를 사용하여 개발할 수 있습니다. DOM 독립적 Javascript 코드는 디자이너와 개발자의 작업이 병렬적으로 진행될 수 있도록 도와주며 testability가 높아 TDD 적용에 매우 유리합니다.

실습

웹 front-end 담당 개발자가 MVVM 패턴과 TDD를 적용하여 프로그램을 작성하는 과정을 살펴봅니다.

기획

개발팀은 연락처 관리 서비스를 개발하려 합니다. 기획자는 웹 클라이언트 화면 스케치를 작성했고 back-end 개발자는 연락처를 등록하고 연락처 목록을 가져오는 API를 제공해주기로 했습니다. 기획자에 의하면 새 연락처가 추가되면 비동기로 연락처 목록이 갱신되어야 한다고 합니다. 디자이너는 스케치를 기반으로 HTML 작업을 시작합니다. Front-end 개발자는 back-end 작업과 디자인 작업이 진행되는 동안 기획 안을 바탕으로 front-end 뷰모델을 개발하기로 합니다. 기획된 뷰와 back-end 개발자와 합의한 API 프로토콜의 연락처 엔터티 계약(contract)은 아래와 같습니다.

ContactsViewModels - Contract

개발 환경

MVVM 프레임워크는 KnockoutJS 2.3.0을 선택하고 뷰모델 테스트 플랫폼은 Node.js를 이용합니다. 단위 테스트 프레임워크는 Mocha를 사용하고 assertion 코드를 보다 아름답게 하기 위해 Should.js를 추가합니다. 테스트 더블 작성은 Sinon.js의 도움을 받습니다. 다음은 주요 파일 구조와 각 컴포넌트 설치방법을 보여줍니다.

+ Scripts
  + ViewModels
    + node_modules
      + should
      + sinon
      test.js
      Contacts.js
      Knockout-2.3.0.js
컴포넌트 설치방법
Node.js http://nodejs.org/에서 각 플랫폼에 맞는 버전 설치
Mocha Node.js 콘솔에서 $ npm install mocha 입력
should Node.js 콘솔에서 ViewModel 폴더로 이동한 뒤 $ npm install should 입력
Sinon.js Node.js 콘솔에서 ViewModel 폴더로 이동한 뒤 $ npm install sinon 입력
KnockoutJS http://knockoutjs.com/에서 다운로드 하거나 Visual Studio 환경이면 Package Manager Console에서 PM> Install-Package knockoutjs 입력

뷰모델 구성

뷰는 뷰모델의 투영입니다. 뷰모델 구성을 설계하는 것은 기획된 뷰 개요를 추상화 하는 작업입니다. 다시 한 번 뷰 기획 안을 살펴봅니다.

Front-end 개발자는 먼저 뷰 전체를 추상화하는 MainViewModel을 고안한 뒤(이는 MVVM 패턴에서 매우 일반적인 작업입니다.) 뷰를 크게 두 부분으로 나누었습니다. 왼쪽의 새 연락처 입력화면과 오른쪽의 연락처 목록이 그것입니다. 이에 각각 대응되는 NewContractViewModel과 Contract 엔터티 시퀀스를 MainViewModel에 배치합니다. NewContactViewModel은 뷰의 3개의 텍스트 상자와 버튼을 firstName, lastName, email 속성과 add 함수로 추상화합니다. 마지막으로 MainViewModel에 연락처 목록을 비동기로 갱신하는 updateContacts 함수를 추가합니다.

테스트 작성

기획자가 작성한 화면 스케치와 설명을 바탕으로 기능 목록을 열거합니다.

  1. API를 사용하여 연락처 목록을 가져와 화면에 보여줍니다.
  2. First Name, Last Name, Email을 입력하고 Add 버튼을 누르면 연락처를 등록하는 API를 호출합니다.
  3. Add 버튼을 누르면 First Name, Last Name, Email 필드를 초기화합니다.
  4. 연락처 추가가 완료되면 연락처 목록을 갱신합니다.

각 기능에 대응되는 테스트를 작성합니다. 각 테스트는 정의된 내용을 충분히, 그리고 정의된 내용만을 검증하도록 작성하는 것이 좋습니다. 다음 코드는 첫 번째 기능에 대한 테스트입니다.

describe('MainViewModel', function () {
  it('test should call getContacts and update contacts', function () {
    // Setup
    var ironman = {}, hulk = {};
    var service = { getContacts: function () { } };
    var mock = sinon.mock(service);
    mock.expects('getContacts').callsArgWith(0, [ironman, hulk]).once();

    // Exercise
    var viewModel = new MainViewModel(knockout, service);
    viewModel.updateContacts();

    // Verify
    mock.verify();
    viewModel.contacts().should.be.ok;
    viewModel.contacts().length.should.equal(2);
    viewModel.contacts()[0].should.equal(ironman);
    viewModel.contacts()[1].should.equal(hulk);
  });
});

보통의 테스트는 setup, exercise, verify 단계로 이루어집니다. Setup 단계에서 Sinon.js를 사용하여 API를 대리하는 서비스의 모의(mock) 개체를 만듭니다. MainViewModel 개체는 getContacts 서비스를 한 번 호출할 것이라(once) 기대되며(expects) 모의 서비스는 콜백 함수를 통해 일련의 개체 목록을 MainViewModel에 전달(callsArgWith)합니다. Exercise 단계에서는 연락처 목록을 갱신하도록 updateContacts 함수를 호출합니다. 마지막 verify 단계에서 mock 개체의 verify 함수를 호출해 MainViewModel이 getContacts 서비스를 호출했는지 검증하고(행위 검증) 연락처 목록에 대응되는 contacts 속성이 기대하는 대로 갱신되었는지 확인하여(상태 검증) 테스트를 마무리합니다. 일련의 과정을 통해 테스트는 API를 사용하여 연락처 목록을 가져와 화면에 보여주는 뷰의 기능을 시뮬레이션하고 검증하게 됩니다. 전체 4개의 테스트 코드는 여기에서 확인할 수 있습니다.

테스트 실패

테스트를 실행하면 모든 테스트가 실패합니다. 계획된 TDD 절차이니 안심해도 됩니다. 오히려 새로 작성한 테스트가 실패하지 않으면 설계가 잘 못 되지 않았는지, 테스트 작성이 잘못되지 않았는지 확인해봐야 합니다.

코드 작성

MainViewModel과 NewContactViewModel을 구현합니다. 예를 들어 아래 다음 코드는 NewContactViewModel 구현 코드입니다.

Contacts.NewContactViewModel = function (knockout, service, mainViewModel) {
  var $this = this;

  $this.firstName = knockout.observable();
  $this.lastName = knockout.observable();
  $this.email = knockout.observable();

  $this.add = function () {
    service.addContact($this.firstName(), $this.lastName(), $this.email(), function () {
      mainViewModel.updateContacts();
    });

    $this.firstName('');
    $this.lastName('');
    $this.email('');
  };
};

NewContactViewModel 인스턴스는 생성될 때 프레임워크, API 서비스, MainViewModel 인스턴스를 주입 받으며 기능 목록 2, 3, 4를 모두 구현합니다. 전체 코드는 여기에서 확인할 수 있습니다.

테스트 성공

모든 테스트가 성공했습니다.

디자인

디자이너가 결과물을 공유했습니다. 무려 Bootstrap을 사용하여 구현된 멋진 plain-old HTML 코드입니다. 여기에서 확인할 수 있습니다.

서비스

마침 back-end 개발자도 API 초안을 팀에 공개했습니다. API URL은 ‘/api/Contacts/’입니다. 연락처 목록 조회는 GET 메서드를 사용하고 연락처 추가는 POST 메서드를 사용합니다. API를 대리하는 서비스 개체(proxy)를 작성합니다. 서비스 proxy 구현 전체 코드는 여기에서 확인할 수 있습니다.

바인딩

뷰에 뷰모델 바인딩 코드를 추가합니다. 바인딩 코드는 HTML요소 특성으로 작성되므로 디자이너 작업에 영향을 줄 가능성은 매우 낮습니다. 예를 들어 ‘Add’ 버튼 요소와 NewContactViewModel의 add 함수는 ‘data-bind’ 특성에 ‘click: add’를 대입하여 바인딩입니다. 또 다른 예로 연락처 테이블은 ‘foreach’ 바인딩을 사용하여 서비스로부터 제공된 연락처 수만큼의 정보를 렌더링합니다.

<table class="table table-striped">
  <tbody>
    <tr>
      <td></td>
      <td></td>
      <td></td>
    </tr>
  </tbody>
</table>

여기에서

요소는 DOM 요소가 아닌 뷰 템플릿 역할을 하게 됩니다. 필요한 모든 뷰모델 바인딩이 적용된 뷰 코드는 여기에서 확인할 수 있습니다.

완료

이제 뷰 개발이 완료되었습니다. 구현된 서비스는 http://webstack.azurewebsites.net/Contacts/에 배포되었고 전체 코드는 여기에서 확인할 수 있습니다.

아키텍처

결론

MVVM 디자인 패턴을 사용하여 TDD를 따르는 웹 클라이언트를 개발하는 방법에 대해 살펴봤습니다. MVVM 디자인 패턴은 UI 코드 개발을 보다 유연하고 testable하게 도와줍니다. 이 글에서 전달하려는 것은 MVVM 디자인 패턴이지 특정 프레임워크가 아닙니다. 비록 여기서 사용된 프레임워크는 KnockoutJS이지만 AngularJS 등의 다른 프레임워크를 사용해서도 같은 철학과 프로세스를 유지하며 개발할 수 있습니다.

댓글 남기기