Xcode4 단위 테스트 프레임웍(OCUnit Test Framework) 사용하기

Apple 개발자 도구인 Xcode는단위 테스트를 위한 OCUnit Framework를 포함한다. 테스트 주도 개발(Test-Driven Development)로 iOS 앱 개발시 필요한 OCUnit 사용법에 대해 알아보자. (본인의 Xcode 버전:  4.4.1)

OCUnit 사용 세팅

OCUnit 설정 방법은 기본적으로 두 가지다.

  1. 프로젝트 생성과 함께 단위 테스트 사용 옵션 선택
  2. 기존 프로젝트에 단위 테스트 타겟 추가

1. 프로젝트 생성과 함께 단위 테스트 사용 옵션 선택

< 프로젝트 생성시 단위 테스트 사용 옵션 설정 >

2. 기존 프로젝트에 단위 테스트 타겟 추가

Project Navigator 의 첫번째 항목인 프로젝트 이름을 클릭하면 아래와 같이 Project Editor 가 나타난다. Add Target 을 클릭하자.

Cocoa Touch Unit Testing Bundle 을 선택하고 Next로 이동한다.

테스트 타겟 설정 화면이다. Product Name은 보통 테스트 대상 앱 프로젝트 이름 끝에 “Tests” 를 붙여 설정한다. Project 설정 콤보박스에는 테스트 대상 프로젝트를 설정한다.

1번과 2번 방법으로 OCUnit 설정을 완료하면 테스트 타겟 파일들이 프로젝트에 추가된다. 설정 후 곧바로 테스트를 실행하면 (단축키 : Command + U ) 자동 생성된 테스트 코드가 발생시키는 테스트 실패 메시지가 나타난다. 아래 그림과 같다.

2번 방법으로 설정한 경우, 테스트를 실행해도 실패 메시지가 나타나지 않을 수 있다. 아래의 단계를 따라가며 설정 상태를 확인하자. 설정이 되어 있지 않다면 설정을 추가한다.

Xcode 메뉴바 -> Product -> Edit Scheme 을 선택 (아래 화면)하여 Build 섹션의 (+) 버튼으로 테스트 타겟을 추가하고 “Test” 체크박스를 체크한다.

Test 섹션의 (+) 버튼을 이용하여 테스트 타겟을 추가한다. 아래 그림과 같다.

(1)번 방법을 이용하면 추가로 설명한 단계들이 자동으로 설정된다. 다시 테스트를 실행하면 테스트 실패 메시지를 확인할 수 있다.

여기서 잠깐 한가지 확인하자.

단위 테스트 도구를 이용하여 SW 개발을 하면, 테스트 메소드 개수가 증가하기 마련이다. 매번 모든 테스트 케이스를 확인하는 것이 TDD의 핵심이지만, 상황에 따라 특정 메소드만 테스트 할 경우도 있다. 메소드 선택 체크박스 (위 그림 참조)를 통해 테스트 대상 메소드 선택이 가능하다.

위 그림은 “testExample” 메소드 하나만 선택되어 있음을 보여준다. “testExample”은 OCUnit 설정시에 자동 생성된 메소드로 무조건 테스트가 실패하는 조건을 갖는다. 설정 후 테스트의 결과가 실패인 이유다.

OCUnit 테스트 모드

OCUnit 테스트에는 두 가지 모드가 있다.

  • Logic Unit Tests : 로직 전용 테스트 모드. iOS Simulator 모드에서만 가능.
  • Application Unit Tests : 어플리케이션 테스트 모드. 실제 디바이스에서도 테스트 가능.

(1) 방법으로 OCUnit 설정을 하면 디폴트로 Application Unit Tests 모드다. (2)방법으로 설정하면 Logic Unit Tests 모드가 기본으로 선택된다.

Project Editor 화면에서, 테스트 타겟의 Build Settings 속성(“Bundle Loader”, “Test Host”) 설정을 통해 테스트 모드 변경이 가능하다. Build Settings의 검색을 이용하자.

Bundle Loader 와 Test Host의 설정값을 지우면 Logic Unit Tests 모드다. Logic Unit Tests는 반드시 iOS Simulator 만 지원함을 기억하자.

Bundle Loader 와 Test Host 에 아래 값을 설정하면  Application Unit Tests 모드다.

  • iOS : $(BUILT_PRODUCTS_DIR)/<app_name>.app/<app_name>
  • Mac : $(BUILT_PRODUCTS_DIR)/<app_name>.app/Contents/MacOS/<app_name>

아래 그림은 설정 예시다. 위의 값에서 <app_name>을 테스트 대상 프로젝트 이름으로 바꾸어 입력한다. Test Host 설정값은 Bundle Loader 과 동일하다.

OCUnit 사용

테스트 코드와 어플리케이션 코드는 분리되어야 한다. 테스트 목적으로 새로운 클래스를 추가할 경우 설정 방법을 알아보자.

테스트 타겟에 클래스 – 보통 테스트 메소드를 포함하는 클래스 – 를 추가할 경우, 아래 그림처럼 Targets 옵션에 테스트 타겟만 설정한다.

그러나 어플리케이션 코드에 클래스 – 테스트 대상 클래스 – 를 추가할 경우, 타겟 옵션에 테스트 타겟과 어플리케이션 타겟을 같이 선택해야 테스트 코드와 어플리케이션 코드 양 쪽에서 사용 가능하다.

테스트 케이스 메소드 사용법은 Writing Test Case Methods 을, 사용에 필요한 OCUnit  API 정보는 Unit-Test Result Macro Reference 를 참고하자. (나중에 별도 포스팅으로 다룰 생각이다.)

테스트 케이스 메소드 작성 방법은 어렵지 않다. 중요한 것은 제품 개발에 의미있는 테스트 케이스를 발굴하는 것이다.

* Reference

Advertisements

Test-Driven Development

린(Lean) 소프트웨어 개발론의 핵심 철학 중 하나는 “결함은 발견 즉시 해결”이다. 린 개발은 이것의 실천법으로 테스트 주도 개발(Test-Driven Development, TDD)을 제시한다. TDD 의 개념과 방법론 그리고 한계에 대해 알아보자.

TDD (Test-Driven Development) 란?

TDD는 반복 테스트을 이용한  소프트웨어 개발법이다. 작은 단위의 테스트 케이스를 작성하고 이를 통과하는 코드를 추가하는 단계를 반복하여 소프트웨어를 구현한다.

TDD의 목표는 작동하는 깔끔한 코드 “Clean code that works” 이다. 이를 위해 두 가지 원칙을 제시한다 .

  • 오직 자동화된 테스트가 실패할 경우에만 새로운 코드를 작성한다.
  • 중복을 제거한다.

린에서는 불필요한 기능 구현을 가장 큰 낭비로 여긴다. 이는 코드베이스의 복잡성을 높이고, 유지 보수를 어렵게 하여 높은 개발 비용을 초래한다. TDD도 테스트를 통과시키는 코드만을 작성함으로써 맥락을 같이한다.

요구사항 분석 -> 설계 -> 개발 -> 테스트 -> 배포 개발법엔 소프트웨어 개발을 느리게 하는 잠재적 위험이 존재한다.

  • 소비자의 요구사항이 처음부터 명확하지 않을 수 있다.
  • 따라서 처음부터 완벽한 설계는 어렵다.
  • 설계 -> 개발을 진행하면, 실제 코드와 설계간 갈등이 생길 수 있다.
  • 한 곳의 수정이 다른 곳에 미치는 영향을 확인 및 다른 기능의 정상 동작을 보장하기 어렵다.

TDD는 테스트 케이스를 생성한다. 테스트 케이스는 자동화된 테스트 도구로 이용되어, 코드 변경시 기존 기능이 제대로 동작하는지 쉽게 확인할 수 있고 정상 동작을 보장한다. 또한 TDD는 리펙토링을 개발 프로세스에 포함시켜 ‘변경’이라는 소프트웨어의 특성을 반영한다.

애자일 개발론의 Robert C. Martin은 다음의 TDD 원칙을 제시한다.

  • 실패하는 테스트를 작성하기 전에는 절대로 제품 코드를 작성하지 않는다.
  • 실패하는 테스트 코드를 한 번에 하나 이상 작성하지 않는다.
  • 현재 실패하고 있는 테스트를 통과하기에 충분한 정도를 넘어서는 제품 코드를 작성하지 않는다.

TDD 개발법

TDD는 아래 단계의 반복으로 진행된다.

  1. 빨강 : 실패하는 작은 테스트 케이스를 작성한다. 처음에는 컴파일조차 안될 수 있다.
  2. 초록 : 테스트를 통과하는 코드를 작성한다.
  3. 리펙터링 : 테스트를 통과하기 위해 만든 코드의 모든 중복을 제거하고, 불명확한 것을 명확히 한다.

이러한 단계로 인해 TDD는  “업무 코드 작성 전에 테스트 코드를 먼저 만드는 것”으로 정의되기도 한다. 이해를 위해 간단한 sum 함수를 예제로 살펴보자.

  • 작성 메소드 이름 : sum
  • 기능 구현에 필요한 재료 (argument) : int a , int b
  • 반환 값의 타입 : int
  • 정상 동작 만족 조건 (작업 종료 조건) : a와 b 를 더한 값을 결과로 돌려줌

간단히 작성된 sum 함수의 테스트 조건이다. 꼭 위와 같은 형식이 아니라 하나의 문장이어도 된다. 단 테스트는 너무 크지 않은 규모여야 하며 테스트 통과 조건(작업 종료 조건)이 반드시 있어야 한다.

아래 예시 코드를 보자. (제품 코드 관리를 위해 테스트 클래스는 제품 클래스와 구분되어어 쌍을 이루는 것이 일반적이지만, 간단한 예시를 위해 하나의 클래스 안에 테스트 메소드와 제품 메소드를 같이 두었다.)


-(void)testMain{ // 테스트 메소드

    NSLog(@"True or False : %d",[self sumWithIntegers:10 :20] == 30);
    NSLog(@"True or False : %d",[self sumWithIntegers:1 :2] == 3);
    NSLog(@"True or False : %d",[self sumWithIntegers:-10 :20] == 10);
    NSLog(@"True or False : %d",[self sumWithIntegers:0 :0] == 0);

}
-(int)sumWithInteger:(int)a :(int)b{ // 제품 메소드
    return 0;
}

먼저 테스트 메소드를 작성한다. 메소드 안에는 테스트 성공 여부를 확인할 수 있는 코드가 있다. 위 예제는 로그에 모두 True 가 나오면 테스트 케이스가 통과되었다고 가정한다.

제품 메소드보다 테스트 메소드를 먼저 작성하므로 sumWithIntegers: 메소드를 찾을 수 없다는 컴파일 에러가 발생한다. 의아해 할 필요없다. 의도한 바다. 테스트 코드를 작성하면서 자연스럽게 제품 코드의 클래스 이름 및 메소드 이름 등의 설계 요소를 고민하게 된다.

다음 단계로 테스트 케이스의 결과를 ‘통과’로 만든다. 컴파일 오류 해결을 위해 sumWithIntegers: 메소드를 정의한다. 그리고 메소드 안에 테스트를 만족시키는 코드를 작성한다. (예시에서는 단순히 0을 리턴하였다. 제품 코드를 먼저 작성하지 않음을 보여주기 위함이다.)

테스트가 통과되면 테스트 수행에 사용된 테스트 클래스와 제품 클래스 모두 리펙토링한다. 필드 이름, 중복 제거, 가독성 향상 등 간결한 코드를 유지하여 변경에 유연한 코드를 만들도록 노력한다. 리펙터링은 이전의 테스트 케이스를 모두 통과시켜야만 한다. 개발 단계에서  테스트 케이스가 누적되어 유지되므로 이전 기능들에 대한 정상 동작 여부가 보장된다. (위 예시는 지극히 간단하여 리펙토링 단계 없음)

개인적으로 다음의 역량이 TDD 개발에 있어 개발자에게 중요할 것 같다.

  • 테스트 케이스 작성 능력 : 테스트 케이스 자체가 의미있지 않으면 개발된 코드를 신뢰할 수 없다.
  • API 숙련도 : 테스트 케이스를 먼저 작성하면 컴파일 조차 되지 않아 개발 도구의 자동 완성 기능의 도움을 받지 못할 수 있다.
  • 설계 및 리펙토링 능력 : 매 단계마다 상황에 맞는 설계적 결정을 해야 한다. 전체 SW 구조에 대한 직관을 가질 필요가 있다.

테스트 케이스가 의미있으려면 테스트 케이스는 정상적으로 끝까지 수행되어야 한다. 테스트의 결과 중 개발자가 의도 하지 않은 문제 – 예상치 못한 Exception 등 – 는 실패가 아닌 오류로 기록된다. 테스트 케이스가 정상적으로 끝까지 진행되어 오류가 아닌 실패로 기록되도록 하자.

테스트 클래스를 생성/사용할 때는 제품코드와 ‘소스 코드는 다르게, 패키지는 동일, 컴파일된 클래스는 다른 곳으로’ 설정하는 방법이 가장 많이 쓰인다. 폴더만으로 제품 코드와 분리하면 테스트만을 위한 라이브러리를 구별하기가 어려워진다.

일반적으로 TDD에는 Unit Test Framework이 많이 사용된다. iOS 개발 도구 Xcode에는 OCUnit Test Framework이 내장되어 있다.

TDD 장점

  • 개발자의 방향을 잃지 않게 유지: 현재 자신의 개발 내용 및 진척 상황을 항상 살펴볼 수 있다.
  • 품질 높은 소프트웨어 모듈 보유 : 간결한 코드 유지 가능
  • 자동화된 단위 테스트 케이스 소유: 개발자가 필요한 시점에 언제든 수행할 수 있으며 시스템의 이상 유무를 바로 확인할 수 있다.
  • 사용설명서 & 의사 소통의 수단:  작성된 테스트 케이스는 제품 코드 사용 설명서이자 동시에 다른 개발자와 소통하는 커뮤니케이션 통로가 된다. (제품 코드 API 사용 예시가 된다.)
  • 설계 개선 : 테스트 케이스 작성 시 개발에 포함된 다양한 설계요소들에 대해 고민하게 된다. 흔히 테스트하기 어렵다고 생각되는 코드들은 객체 설계 원리 중 기본에 해당하는 원칙들이 잘못 적용됐거나 충분히 고려되지 않았을 가능성이 높다. TDD 진행하면서 테스트가 가능하도록 설계 구조를 고민하다 보면 자연스럽게 디자인을 개선하게 된다.
  • 보다 자주 성공한다 : TDD 는 주기를 짧게 설정하도록 권한다. 개발자는 성취감을 자주 느낄 수 있다.

TDD 의 한계

TDD 선구자인 Kent Beck 은 다음 두 가지를  TDD가 현재 접근하기 어려운 분야라고 말한다.

  • 동시성(Concurrency)
  • 보안 등의 비기능적 요소

이 외에 국내 TDD 도서 저자인 채수원씨는 본인의 도서에서 아래와 같이 추가적인 어려움들을 덧붙였다. 불가능하진 않지만 쉽지 않다는 의미로 받아들이면 될 것 같다.

  • 접근 제한자 메소드 : 현재는 public 메소드만 테스트해도 무방하다는 경향이 대세로 보이지만, 필요에 따라 public 으로 테스트 후 접근 제한자를 수정하는 방법도 있다.
  • GUI : 뷰 레이어와 로직 레이어를 확실히 분리시켜 설계할 필요가 있다.
  • 의존성 모듈 테스트 (target = A but A -> B) : 타겟 A 테스트를 위해 B 도 필요하다. 그러나  B가 아직 준비가 되어 있지 않다면 테스트가 이루어질 수 없다. 보통 이런 경우 B의 Mock객체를 생성하여 B는 문제없음을 가정하고 테스트 한다.

* Updated 9/14 , 접근제한자 메소드 한계 : TDD 단계를 준수하면  Private 메소드는 빨강 – 초록 단계를 지나 리펙토링 단계에서 생성된다. Private 메소드는 새로운 테스트 케이스가 필요한 것이 아니라 이미 통과한 테스트의 리펙토링 결과다. 별도로 접근제한자를 변경해서 테스트할 필요가 없다. 그렇게 되도록 TDD 개발 단계를 준수하자.

* Reference

%d bloggers like this: