Test Driving Your Code with OCUnit
OSXDEV
개발에 있어서 일반적인 시나리오는 다음과 같다. 빌드가 성공적이면 어플리케이션이 동작할 것이고, 여러분은 모든 기능을 테스트해보고 그 결과를 확인할 것이다. 적어도 이 상황까지는 여러분은 이것이 안전하다고 말할 수 있을 것이다. 하지만 추가할 다음 기능이 뒤에서 돌게되는 공포스런 낮은 우선권의 쓰레드라면 "만약 코드가 변경되면, 무엇이 잘못될 것인가?" 를 생각하지 않을 수가 없다. 여러분은 분명히 일일이 각각의 변화에 따른 모든 기능에 대해 테스트해 볼테지만, 얼마후에 지루하고 잘못될 수도 있다고 깨닫게 될 것이다. 아이러니하게도 여러분은 테스트하기에 충분한 시간이 없다고 느낄 것이다.
이런 시나리오가 여러분에게 친숙하게 들린다면, 여기 좋은 소식이 있다. 여러분의 컴퓨터는 낮은 우선권의 공포스런 쓰레드를 체크하는 사이클을 갖고 있다. 만약 자동화된 테스트를 통한 시각화된 분석이 제공된다면, 컴퓨터는 행복하게도 여러분이 원할땐 언제든지 그 결과를 확인해줄 것이다. 자동화된 테스트를 작성하는건 항상 쉬운일은 아니지만, 한번 투자해놓으면 필요한 모든 상황에 테스트를 할 수 있다. 그리고 이러한 테스트를 작성하는 것은 더 나은 디자인을 할 수 있는 기회를 제공해 주기도 한다.
프로젝트를 완성해야만 유닛 테스트를 할 수 있는것은 아니다. 오히려 시작단계부터 이것을 여러분의 개발 사이클에 통합하는 것이 가장 좋다.
이 문서는 OCUnit 을 통한 유닛 테스팅에 대해 소개한다. OCUnit 은 Xcode 와 통합되는 Objective-C 를 위한 유닛 테스팅 프레임워크이다. Xcode 프로젝트를 위한 자동화된 OCUnit 테스트 작성에 대해 차례차례 살펴보기로 하자.
목차 |
[편집] 왜 유닛 테스트가 필요한가?
- 더 튼튼한 소프트웨어. 지금 여러분이 작성한 코드가 잘 동작한다고 말할 수 있겠으나, 앞으로의 변화가 잘못된 상황을 초래하지 않는다는것을 어떻게 보장할 수 있는가? 자동화된 유닛 테스트는 새로운 기능을 추가하고, 버그를 고치고, 코드를 리팩터링하는 상황에서 그로인한 변화를 쉽게 파악함으로써 프로그램의 무결성을 입증하는데 도움을 준다.
- 디자인 향상. 여러분이 작성했던 코드에 유닛 테스트를 추가할 경우, 여러분은 작성한 코드를 다시한번 살펴볼 기회를 갖게 된다. 실제로 작성된 테스트가 그 코드의 첫번째 클라이언트가 되게 된다. 어떤 API의 테스트를 작성하는 것이 어렵다면, 그 API를 사용하는 것 역시 똑같이 어렵다는 말이 된다. 뜻하지 않은 설계상의 문제를 즉시 발견하여 이것이 어려분의 설계 전체를 오염시키는 것을 방지해 줄 것이다.
- 튼튼한 기반. 여러분의 코드는 외부 프로젝트에서 개발된 컴포넌트에 의존하고 있을 수도 있다. 만약 이러한 컴포넌트 중 하나가 잘못되거나 호환되지 않는 방법으로 변경되었다면, 여러분의 코드의 기반을 산산조각 내버릴 것이다. 유닛 테스트는 하부의 무언가가 예상하지 못하게 변경되버린 경우 빠르게 그것을 찾아낼 수 있게 한다. 더구나 실패한 테스트의 경우 코드의 존재하는 문제들을 정량화할 수 있는 수단이 된다. 그것들을 고침으로써 프로그램이 유효성이 증명된다.
- 실행가능한 문서화. 유닛 테스트는 실제로 작동하는 코드의 예를 제공함으로서 어떤 의도로 코드가 씌여질 것인지 문서화하여 준다. 간단히 말해 테스트는 거짓말을 하지 않는다. 어떻게 하나 혹은 일련의 메쏘드를 실행하는지, 정상적인 그리고 예외적인 사용 패턴에 대해서 보여준다. 예를들면 메모리 누수 현상 없이 다양한 사용 패턴을 보여주는 유닛 테스트는 "이 코드가 동작한다" 는 것 이상의 확신을 준다.
- 개발의 가속화. 많은 테스트를 작성하는 것이 더 좋은 소프트웨어를 더 빠르게 작성할 수 있게 해준다는 것은 비직관적인 이야기처럼 들릴지 모르나, 이것은 사실이다. 테스트를 실행함으로써 재작업이 줄어들고 코드가 결합되기전에 문제를 발견할 수 있게 됨으로써 코딩-테스트-활용(deploy) 의 사이클이 더욱 굳건해진다.
[편집] 테스팅 프레임워크
여러분은 xUnit 부류의 테스팅 프레임워크에 대해 적어도 하나는 들어 봤을 것이다. Java를 위한 JUnit, Ruby를 위한 Test::Unit, Python을 위한 PyUnit, C++를 위한 CppUnit, 그 밖에도 많은 것들이 있다. 이들 중 대부분은 Smalltalk 를 위한 SUnit 에서 비롯되었다. 그래서 이 테스팅 프레임웍들은 서로 어느정도 공통점을 가지고 있다. 테스트 상에서 코드에 대한 단언(assertion)을 포함하는 테스트 케이스의 작성이 가능하다. 테스트 집합(suite)이란 테스트 실행이 일률적으로 구동되는 테스트 케이스들을 모아놓은 것이다. 테스트가 실행이 되면, 결과를 확인하고 명확하게 테스트가 통과인지 실패인지 알려줄 것이다.
이 문서는 Objective-C 코드를 위한 OCUnit 의 사용법에 중점을 두고 있다. Objective-C의 테스트 프레임워크가 이것만 있는 것은 아니다(UniKit 같은 다른 훌륭한 프로그램도 있다). 그것 둘다 사용하기 쉽고, Xcode 와 투명하게 결합된다. 그러나 이 문서에서는 OCUnit 을 사용하겠다. OCUnit은 완성도 있고, 많은 기능과 안전성을 보장하며, Apple 커뮤니티로부터 많은 지원을 받고 있다.
OCUnit 은 그냥 툴일 뿐이다. 어떤 유닛 테스팅 툴을 쓸것인가는 실제로 돌아가는 유닛 테스트를 작성하는 일에 비하면 별로 중요하지 않다. 그럼 이제 시작해보자.
[편집] OCUnit 설치하기
이 글을 쓰고있는 지금 Xcode 를 위한 OCUnit 의 최신 버젼은 부트 디스크의 최상위 레벨 디렉토리에 자동으로 설치되는 인스톨러 패키지와 OCUnit 을 빌드해서 홈 디렉토리의 하위 디렉토리에 복사해주는 스크립트의 2가지 형태로 제공된다.
빠르고 쉬운 시작을 위해, 인스톨러 패키지를 사용하자. 간단히 http://www.Sente.ch 에서 최신 버젼의 OCUnitRoot를 다운로드 하자. 일단 DMG 파일을 다운받았으면, 인스톨러 패키지를 실행한다. 설치전에 OCUnit 파일들이 설치될 디렉토리를 미리 확인할 수 있다. 예를 들면 문서파일들은 /Developer/Source/OCUnit/Documentation에 설치될 것이다.
[편집] OCUnit을 Xcode 프로젝트에 추가하기
OCUnit의 설치가 되었으면, OCUnit 테스트를 Xcode 프로젝트에 추가할 수 있다. 어떻게 하는지 보여주기 위해 Xcode와 함께 배포되는 예제 프로젝트에 테스트를 추가해보도록 하겠다.
/Developer/Examples/AppKit/TemperatureConverter 디렉토리에 있는 Temperature Converter 프로젝트를 적당한 디렉토리에 복사하자. 이 복사본을 가지고 연습을 해볼 것이다. 첨부터 다시 시작하고 싶다면 원본 파일을 다시 복사해 오면 된다.
프로젝트 복사본을 열고, Build and Go 버튼을 눌러서 실행해보자. 다음의 그림 1과 같은 화면을 볼 수 있다.

그림1: Temperature Converter 어플리케이션
켈빈, 섭씨, 화씨, 랭킨 네가지의 단위 중 하나로 온도 값을 입력하면, 나머지 세가지의 값이 계산되어 화면에 나타난다. 이것은 유닛 테스트 작성을 시작하기에 좋은 간단한 프로그램이다. 사실 몇번 온도값을 입력하다 보면, 여러분은 변환이 제대로 되었는지 계속해서 확인해줄 테스트 코드를 작성해야 겠다는 생각을 할지도 모른다.
테스트 코드를 작성하기 전에, OCUnit 을 Xcode 프로젝트에 추가해야 한다. 그렇게함으로써 코드와 그것의 테스트가 같은 프로젝트 안에서 밀접하게 위치하게 된다. 프로젝트의 각각의 타켓에 대해 테스트의 수행이 가능하다.
다음 과정을 따라 OCUnit 을 Temperature Converter 프로젝트에 추가해 보자.
- 테스트가 실행될 새로운 타겟을 만든다. "Project > New Target" 을 고른 후 "Cocoa > Test Framework" 타겟 형식을 선택한다. "Next" 버튼을 클릭한 후, 새로운 타겟의 이름을 "Test" 로 하고 "Finish" 버튼을 클릭하여 새로운 타켓을 만들면 Xcode 의 "Groups & Files" 브라우져의 "Targets" 그룹에 들어가게 된다.
- OCUnit 프레임워크를 추가한다. Xcode의 "Groups & Files" 브라우져에서, "Temperature Converter" 폴더 안에 있는 "Frameworks" 폴더를 연다. 그리고 "Linked Frameworks" 폴더에서 마우스의 오른쪽 버튼을 클릭하고 "Add > Existing Frameworks" 를 선택한뒤, /Library/Frameworks/SenTestingKit.framework 를 선택하고 "Add" 버튼을 누른다. 다이알로그 창이 뜨면 "Test" 타켓에 체크하고, "Temperature Converter" 은 체크를 해제한다. 마지막으로 "Add" 버튼을 누른다.
- 테스트 케이스를 위한 새로운 그룹을 만든다. Xcode의 "Groups & Files" 브라우져 최상단의 "Temperature Converter" 에서 오른쪽 버튼을 클릭하고 "Add > New Group" 을 선택한다. 새로 생성된 그룹의 이름을 "Test Cases" 로 변경한다.
다되었다면, "Groups & Files" 브라우져가 그림 2와 같이 보일것이다. 이제 테스트를 작성할 준비가 끝났다.
[편집] 테스트 작성하기
그림 1에 나온 몇몇 온도 값을 검사하는 테스트를 작성하면서 시작해보도록 하자. 여러분은 유저 인터페이스를 통해 그 값들을 테스트 할 수 있다, 그러나 이것은 기반 코드 보다 더 변화가 심하다. 그리고 일반적으로 GUI 컴포넌트를 테스트하는 것은 어렵다. 이러한 것이 좋은 자동화된 테스트를 작성을 시작하는데 걸림돌이 되서는 안된다.
코코아 애플리케이션의 유저 인터페이스와 비지니스 로직이 깨끗하게 구별되는 특징은 이러한 경우에 큰 도움이 된다. 디스플레이되는 각각의 영역은 그에 맞는 적절한 온도를 변환하는 코드에 바인딩되어있다. 온도변환 루틴이 주어진 온도에 대해 올바른 온도값을 반환한다면, 그 루틴에 바인딩 되있는 디스플레이 영역도 올바른 값을 출력한다. 그래서 각 영역의 바인딩이 제대로 되있는지 확인하는 대신, 하부의 온도변환 루틴이 화면에 출력될 값을 제대로 계산해내는지를 테스트하는데 중점을 두자.
테스트 케이스 생성하기
OCUnit 테스트 케이스를 생성하기 위해, 다음과 같이 SenTestCase의 서브클래스를 생성하자.
- Xcode 의 "Groups & Files" 브라우져의 "Test Cases" 에서 오른쪽 클릭을 하고, "Add > New File" 을 선택한다.
- 다이알로그 창에서 "Cocoa > Objective-C SenTestCase subclass" 를 선택하고 "Next" 버튼을 클릭한다.
- 다이알로그 창에서 파일 이름을 TemperatureTest.m 으로 한다. automatically creates the TemperatureTest.h file 항목이 체크되어 있어야 한다.
- "Temperature Converter" 타켓의 체크를 없애고 "Test" 타켓을 체크한다. "Finish" 버튼을 누르면 TemperatureTest.h 와 TemperatureTest.m 파일이 "Test Cases" 그룹에 추가된다.
여기까지 되었다면, Xcode의 "Groups & Files" 브라우져가 그림 3과 같이 보일것이다.
테스트 메쏘드의 구현
다음으로 할일은 테스트 케이스에 테스트 메쏘드를 추가하는 것이다. 테스트 메쏘드는 코드가 테스트를 거치면서 여러분의 기대대로 동작하는지를 검사하는 단언(assertion)을 포함한다. 예를 들면, 켈빈온도로 273.15도는 물의 어는점으로써 반드시 섭씨온도로 0도가 되어야 한다. 이것을 위한 테스트를 작성해보도록 하자.
TemperatureTest.m 파일을 다음과 같이 수정한다.
#import "TemperatureTest.h"
#import "CentigradeValueTransformer.h"
@implementation TemperatureTest
- (void) testCentigradeFreezingPoint
{
CentigradeValueTransformer *transformer =
[[CentigradeValueTransformer alloc] init];
NSString *kelvinFreezingPoint = @"273";
NSNumber *centigradeFreezingPoint =
[transformer transformedValue:kelvinFreezingPoint];
STAssertEquals(32, [centigradeFreezingPoint intValue],
@"Centigrade freezing point should be 32, but was %d instead!",
[centigradeFreezingPoint intValue]);
[transformer release];
}
@end
Temperature Converter 프로젝트는 켈빈온도와 섭씨온도간의 변환을 담당하는 CentigradeValueTransformer 클래스를 포함하고 있다. 이 테스트 코드는 STAssertEquals 매크로를 사용하여 CentigradeValueTransformer 클래스의 transformedValue: 메쏘드가 주어진 켈빈 온도 273 에서 32의 결과값을 리턴하는지를 단언(assert)한다.
관례적으로, STAssertEquals 매크로의 첫번째 매개변수로 기대값이 들어가고 두번째 매개변수로 실제 처리 값이 들어간다. 만약 기대값과 실제값이 == 연산자로 정의된대로 같은 값을 갖게되면 그 단언은 통과된다. 세번째 매개변수는 단언이 실패할 경우 출력되는 부차적인 메세지가 들어간다. 여기에 nil 값을 넣으면 단언이 실패할 경우 기본 메세지가 출력된다. 불행이도, 이 기본 메세지가 모든 상황에 적절할 수는 없다. 테스트에서 최대한 유익한 정보를 얻기위해 세번째 매개변수에 형식 문자열(format string) 을 넣고 네번째 매개변수로 그 값을 넣을 수 있다. (모든 STAssert*() 매크로는 NSString의 stringWithFormat: 메쏘드에 넘겨지는 형태와 같은 형식 문자열과 그 값들을 매개변수로 취할 수 있다.)
[편집] 테스트 실행하기
테스트를 실행해보기 전에, "Test" 타겟이 참조하는 몇가지 클래스들을 추가해야한다. 그러기 위해 "Classes" 폴더에서 CentigradeValueTransformer.m 파일을 클릭하여 "Test" 타겟의 "Sources" 폴더에 드래그 한다.
다음으로, Xcode 윈도우의 좌측 상단에 있는 Active Target 메뉴에 "Test" 타켓이 선택되어 있는지 확인한다. 그리고 "Build" 버튼을 눌러 "Test" 타켓을 빌드한다.
기대한대로, "Errors and Warnings" 스마트 그룹에 그림 4와 같은 에러가 출력되면서 테스트는 실패한다.
- [TemperatureTest testCentigradeFreezingPoint] : '< 00000020 >' should be equal to '< 00000000 >' Centigrade freezing point should be 32, but was 0 instead!
에러를 클릭하면, 코드 에디터에서 실패한 단언이 하이라이트된다. 테스트 결과는 "Build Results" 윈도우에도 기록이 남는다. ("Build > Detailed Build Results" 를 선택하면 그림 5와 같은 화면이 나온다.)

그림 5: Xcode의 Build Results 윈도우에 기록된 실패한 테스트 결과
무엇이 잘못된것인가?
우리가 온도 값을 바꿔버렸기 때문에 테스트가 실패하는 것은 당연하다. 저 단언문에서 화씨 온도 값인 32도가 기대되는 값으로 되어 있지만, CentigradeValueTransformer는 섭씨 온도 값을 반환한다.
이 테스트를 통과하기 위해서, 테스트 메쏘드를 다음 단언문과 같이 고치도록 하자.
STAssertEquals(0, [centigradeFreezingPoint intValue],
@"Centigrade freezing point should be 0, but was %d instead!",
[centigradeFreezingPoint intValue]);
이제 "Build" 버튼을 눌러서 테스트를 다시 실행시켜보자, 빌드가 잘되는 것을 확인할 수 있다.
[편집] 테스트 고정틀(Fixtures) 만들기
CentigradeValueTransformer의 reverseTransformedValue: 메쏘드는 섭씨온도를 켈빈온도로 변환해준다. 이러한 사실은 우리가 반드시 테스트를 해야 할 것처럼 들린다.
다음의 테스트 메쏘드를 TemperatureTest.m 파일에 추가하자.
- (void) testKelvinFreezingPoint
{
CentigradeValueTransformer *transformer =
[[CentigradeValueTransformer alloc] init];
NSString *centrigradeFreezingPoint = @"0";
NSNumber *kelvinFreezingPoint =
[transformer reverseTransformedValue:centrigradeFreezingPoint];
STAssertEqualObjects([NSNumber numberWithInt:273],
[NSNumber numberWithInt:[kelvinFreezingPoint intValue]],
@"Kelvin freezing point should be 273, but was %d instead!",
[kelvinFreezingPoint intValue]);
[transformer release];
}
이 테스트 메쏘드는 STAssertEqualObjects 매크로를 사용하여 CentigradeValueTransformer 클래스의 reverseTransformedValue: 메쏘드가 섭씨 온도 0으로 부터 켈빈 온도 273을 리턴하는 것을 단언한다. STAssertEqualObjects 매크로는 기대되는 객체와 실제 객체(이경우 두개의 NSNumber 객체)가 기대되는 객체 쪽의 isEqual: 메쏘드에 정의된대로 서로 같을경우 통과하게 된다.
이 테스트가 통과하는지 실행해서 확인해보자. 빌드는 분명 성공한다, 그러나 이걸로 끝난 것이 아니다. 두번째 테스트 코드를 작성할때 똑같은 코드를 또 다시 반복해서 쓰는 최악의 코딩을 하게 될 것이다. 각각의 테스트 메쏘드가 시작할때 새로운 transformer 객체를 생성하고, 끝날때 이것을 해제한다는 점을 주목하자. 우리가 이 과정대로 계속 나간다면, 각각의 테스트 메쏘드를 작성할때 이러한 절차를 기억하고 있어야 한다. 만약 우리가 어느 시점에서 이러한 사실을 잊어버린다면, 작성한 테스트 메쏘드에 메모리 누수가 발생할 것이다. 테스트가 안전하게 통과된다는 가정하에, 우리는 반복된 코드의 사용을 제거하기 위해 여러개의 테스트에서 실행되는 테스트 고정틀(fixture)를 작성할 수 있다.
첫번째로, 다음과 같이 TemperatureTest.h 파일에 transformer 인스턴스 변수를 정의한다.
#import <SenTestingKit/SenTestingKit.h>
#import "CentigradeValueTransformer.h"
@interface TemperatureTest : SenTestCase
{
CentigradeValueTransformer *transformer;
}
@end
다음으로, TemperatureTest.m 파일에서 각각의 테스트 메쏘드의 객체를 생성하는 코드를 빼서 setUp: 메쏘드에 넣고, 객체를 해제하는 코드는 tearDown: 메쏘드에 넣는다. 코드는 다음과 같다.
#import "TemperatureTest.h"
@implementation TemperatureTest
- (void) setUp
{
transformer = [[CentigradeValueTransformer alloc] init];
}
- (void) tearDown
{
[transformer release];
}
- (void) testCentigradeFreezingPoint
{
NSString *kelvinFreezingPoint = @"273";
NSNumber *centigradeFreezingPoint =
[transformer transformedValue:kelvinFreezingPoint];
STAssertEquals(0, [centigradeFreezingPoint intValue],
@"Centigrade freezing point should be 0, but was %d instead!",
[centigradeFreezingPoint intValue]);
}
- (void) testKelvinFreezingPoint
{
NSString *centrigradeFreezingPoint = @"0";
NSNumber *kelvinFreezingPoint =
[transformer reverseTransformedValue:centrigradeFreezingPoint];
STAssertEqualObjects([NSNumber numberWithInt:273],
[NSNumber numberWithInt:[kelvinFreezingPoint intValue]],
@"Kelvin freezing point should be 273, but was %d instead!",
[kelvinFreezingPoint intValue]);
}
@end
마지막으로, 테스트 고정틀이 제대로 동작하는지 실행해서 확인한다.
각각의 테스트 메쏘드들이 각자의 TemperatureTest 클래스의 인스턴스에서 실행된다는 사실은 중요하다. 이것은 여러분이 테스트 케이스를 실행할때, 두개의 TemperatureTest 클래스의 인스턴스가 생성된다는 것을 말해준다. 테스트 메쏘드의 호출시 모든 인스턴스 변수들이 초기화되기 때문에, 테스트 메쏘드에는 다른 테스트가 의존하게되는 어떠한 선조건이나 후조건도 없어야 한다.
[편집] 좋은 개발 방식으로 위험에서 벗어나기
우리는 온도값이 제대로 변환되었는지 테스트 했다. 슬프게도, 모든게 항상 계획대로 되는 것만은 아니므로, 최소한 하나의 경계가 되는 조건에서 테스트를 함으로써 좋은 프로그래머가 되도록 하자.
CentigradeValueTransformer의 transformedValue: 메쏘드는 매개변수로 넘어온 객체가 doubleValue: 메쏘드에 응답하지 않을 경우 예외(exception) 을 던진다. (이전의 테스트에 사용된 NSString 은 doubleValue: 에 응답한다.) doubleValue: 응답하지 않는 NSObject 메쏘드를 제공함으로써 transformedValue: 메쏘드가 잘못된 값을 막아내는 것을 테스트 할 수 있다.
TemperatureTest.m 파일에 다음의 테스트 메쏘드를 추가한다.
- (void) testBadValueThrowsException
{
NSObject *badValue = [[NSObject alloc] init];
STAssertThrows([transformer transformedValue:badValue],
@"Should raise exception!");
}
이 테스트는 STAssertThrows를 사용하여 transformedValue: 가 doubleValue: 에 응답하는 않는 객체와 함께 실행된 경우 발생한 예외를 단언한다. (만약 Objective-C의 새로운 @throw 스타일의 예외처리를 사용하여 매우 구체적인 예외 클래스를 발생시킨다면, STAssertThrowsSpecific 매크로를 사용하여 구체적인 예외를 발생시키는 코드를 단언할 수 있다.)
지금까지 다음과 같은 단언 매크로를 썼다: STAssertEquals, STAssertEqualObjects, STAssertThrows. OCUnit은 몇가지 단언을 더 제공한다. SenTestCase.h에 정의되어있고, 다음과 같다.
- STAssertNotNil(object, message, ...)
- STAssertTrue(expression, message, ...)
- STAssertFalse(expression, message, ...)
- STAssertThrowsSpecific(expression, exception, message, ...)
- STAssertNoThrow(expression, message, ...)
- STFail(message, ...)
여러분은 테스트 메쏘드에 하나 이상의 이 단언들을 사용할 수 있다. 테스트 메쏘드의 통과여부를 결정하기 위해 이러한 단언들의 통과여부를 검사할 수 있다. 만약 다양한 테스트 메쏘드에서 일련의 단언문들이 자주 쓰이는 경우, 재사용을 위해 OCUnit 에서 제공되는 몇몇 단언들을 캡슐화하는 사용자 매크로를 만드는 것도 괜찮다.
[편집] 테스트 집합(Suite) 작성하기
테스트 집합(suite)은 간단히 함께 실행되는 테스트 케이스들을 모아놓은 것이다. OCUnit 테스트를 포함하는 Xcode 타켓을 실행할 경우, 런타임 환경에서 발견되는 모든 테스트 케이스를 포함하는 테스트 집합이 기본으로 생성된다. 그것은 OCUnit 프레임워크가 자동으로 test로 시작하는 이름의 모든 메쏘드를 찾아 실행하는 것인데, SenTestCase의 서브클래스로 정의되어 있고, 매개변수도 없고, 리턴 값도 없다.
만약 더 많은 제어를 하고 싶다면, 프로그래밍하여 테스트를 고쳐서 사용자 테스트 집합을 만들 수 있다. 다음의 예제는, 조금 꾸며낸 감이 있지만 OCUnit의 테스트가 어떻게 임의로 중첩될 수 있는지 보여준다. 이 테스트 집합은 하나의 테스트 메쏘드와 다른 테스트 케이스의 모든 테스트 메쏘드를 포함하는 테스트 집합을 포함하고 있다.
SenTestSuite *suite = [SenTestSuite testSuiteWithName: @"My Tests"];
[suite addTest:
[TemperatureTest testCaseWithSelector:@selector(testCentigradeFreezingPoint)]];
SenTestSuite *anotherSuite =
[SenTestSuite testSuiteForTestCaseClass:[TemperatureTest class]];
[suite addTest: anotherSuite];
[편집] 빌드 과정과 함께 테스트를 수행하기
하나의 테스트를 만드는것은 여러분의 어플리케이션의 미래를 위한 투자이다. 그러나 여러분은 테스트의 반환값을 확인하기 위해 꾸준히 테스트를 수행시키기를 원할 수도 있다. 테스트가 수행될때마다, 그것은 여러분과 다른이들이 작성한 코드가 기대한 조건에 부합하며 제대로 되었다는 것을 증명해준다.
버젼 컨트롤 시스템에서 코드를 변경하기 전에, 이러한 변화를 포함하는 지역적인 테스트의 빠른 집합(suite) 을 실행한다. 만약 테스트가 통과하지 못한다면, 이 코드는 아직 버젼 컨트롤 시스템에 올려질 상태가 아니다. 지역적인 테스트에 성공한다 하더라도, 코드의 변경이 지역적인 테스트가 처리하지 못하는 영역의 코드에 좋지 않은 영향을 끼칠 가능성은 항상 있다. 프로젝트의 사이즈가 커질수록, 코드를 적용하기전에 모든 테스트를 수행하는 것은 실용적이지 못하다.
자동화된 빌드 과정은 테스팅에 대한 투자를 이용하는데 도움을 준다. 규칙적인 간격으로 컴퓨터가 비동기적으로 테스트를 수행하도록 하자, 하루종일 직접 빌드를 수행하는 것보다 더 나은 방법이다. 예를 들어 다음의 스크립트는 xcodebuild 커맨드를 사용하여 "Test" 타켓을 실행하고, 빌드가 실패하면 notify.sh 스크립를 호출한다.
#!/bin/sh xcodebuild -target Test if [ $? != 0 ] then sh notify.sh exit 1 fi
예로 cron 으로 스크립트를 실행하여 지속적으로 프로젝트에 통합시킬수도 있다. 만약 빌드가 실패하면, notify.sh 스크립트가 팀원에게 이메일을 보내거나, 핸드폰에 문자메세지를 보내준다거나, 램프에 빨간불을 켜준다거나 하는 방식으로 그들이 지금 잘못된 물건을 가지고 일을하고 있다고 경고해 줄 수 있다.
[편집] 테스트 주도적인 개발
지금까지 우리는 이미 작성된 코드에 테스트를 작성했다. 테스트 주도적인 개발방식은 아주 가치있는 디자인 기술이다. 먼저 테스트를 먼저 작성하고, 그 테스트가 통과할 수 있도록 코드를 작성한다. 다시 말하면, 실제로 구현을 하기 전에 테스트를 여러분이 빌드하고자 하는 것, 그리고 작업이 완료되었다는 것을 판단할 수 있는 기준으로 생각하고 작성하라는 것이다. 이러한 사고의 과정은 여러분의 디자인에 여러가지 놀라운 방법으로 영향을 줄 것이다.
테스트를 작성한다.
우리의 간단한 Temperature Converter 애플리케이션에 도시의 이름을 입력하면 그 도시에서의 물의 끓는 점을 개산하는 기능을 추가하고 싶다고 하자. 물의 끊는 점의 온도는 그 도시의 기압을 가지고 다음과 같은 식을 통해 계산할 수 있다.
물의 끓는 점 = 49.161 * Ln(기압) + 44.932
만약 Denver, Colorado의 기압이 현재 수은 기압계로 24.896인치라면, 물의 끓는점은 대략 화씨 202도 이다. 이 계산을 하는 코드를 어떻게 작성할 것인가 생각하지 전에, 테스트를 작성함으로써 대략 가닥을 잡아보자.
- (void) testBoilingPointOfWaterInDenver
{
TemperatureCalculator *calculator =
[[TemperatureCalculator alloc] init];
NSNumber *boilingPoint =
[calculator boilingPointOfWaterInCity: @"Denver"];
STAssertEquals(202, [boilingPoint intValue],
@"Boiling point should be 202, but was %d instead!",
[boilingPoint intValue]);
[calculator release];
}
이 테스트는 TemperatureCalculator 클래스의 boilingPointOfWaterInCity: 메쏘드를 실행한다. 이 테스트는 이 클래스와 메쏘드가 작성되기 전에는 컴파일 되지 않을 것이다. 그러나 첫째로, 여러분은 "테스트가 실행되기 위해서 어떻게 해야 할까?" 하고 생각하게 될것이다. 이것은 굉장히 중요한 질문이다. 보통 코드가 테스트하기 어렵다면 그 코드를 사용하는 것 역시 어렵다.
디자인 피드백에 귀를 기울인다
여기서의 문제는 결과가 결정되어 있지 않다는 것이다. 이 테스트는 Denver의 기압이 24.896일때만 통과하게 된다. 우리가 생각하고 있는 구현은 the TemperatureCalculator 클래스가 네트웍으로 The Weather Channel에 접속하여 현재 기압을 받아오도록 하는 것이기때문에, 이것은 문제가 있다. 아마도 우리는 결론을 다시 생각해야한 할 것 같다.
고맙게도, 이 테스트로부터 TemperatureConverter 클래스가 날씨를 받아오는 특정한 구현으로 부터 분리되면 더 편리할 것이라는 설계상의 통찰력을 얻을 수 있다. 다음과 같이 WeatherLookupService 라는 약식(informal) 프로토콜을 정의한다.
@interface NSObject (WeatherLookupService)
- (double)currentBarometricPressure:(NSString*)city;
@end
그런후 TemperatureCalculator 클래스에 setWeatherLookupService: 메쏘드를 추가한다.
@interface TemperatureCalculator : NSObject
{
id weatherLookupService;
}
- (void)setWeatherLookupService:(id)delegate;
- (NSNumber*)boilingPointOfWaterIn:(NSString*)city;
@end
boilingPointOfWaterInCity: 메쏘드는 현재 원하는 도시의 기압을 얻기 위해 특정한 조회 서비스를 사용할 수 있다.
테스트가 통과하게 만들기
네트웍에 접속하게 하는 API를 사용하는 대신에 우회적인 방법을 사용하는 디자인을 선택하도록 하겠다. 우리의 테스트는 간단히 currentBarometricPressure: 메쏘드만 정의할 수 있다, 그러기 위해 WeatherLookupService라는 약식의(informal) 프로토콜을 따른다. 달리 말하면, 이 테스트는 날씨 데이터를 제공할 수 있는 것처럼 행동한다. 그러나 데이터를 얻기위해 네트웍에 접속하는 대신에 우리는 간단히 처리된 데이터를 반환하기로 한다.
- (double)currentBarometricPressure:(NSString*)city {
return 24.896;
}
이 테스트는 TemperatureCalculator가 기압을 요청할때 위의 currentBarometricPressure: 메쏘드를 실행하기 위해 스스로를 TemperatureCalculator의 WeatherLookupService로 설정한다.
- (void) testBoilingPointOfWaterInDenver
{
TemperatureCalculator *calculator =
[[TemperatureCalculator alloc] init];
[calculator setWeatherLookupService: self];
NSNumber *boilingPoint =
[calculator boilingPointOfWaterIn: @"Denver"];
STAssertEquals(202, [boilingPoint intValue],
@"Boiling point should be 202, but was %d instead!",
[boilingPoint intValue]);
[calculator release];
}
우리가 WeatherServiceLookup 프로토콜을 채택하고 있는 객체와 상호작용하는 TemperatureCalculator 클래스만 테스트 하고 있는 사실에 주목하자. 우리는 효과적으로 TemperatureConverter 클래스를 메세지를 보내 네트워크로부터 온도를 조회하는 것처럼 속였다. 어떤 시점에 가서는 진짜 날씨를 조회하는 테스트를 필요로 하게 될 것이다. 하지만 그러는 동안에 유닛 테스트는 이 시점에서 코드가 잘 실행되고 있는지 집중할 수 있게 해준다. 그리고 이러는 동안에 우리는 더 나은 설계를 마무리 지을 수 있었다. 그림 6과 같다.
[편집] 결론
유닛 테스트의 작성은 여러분의 어플리케이션의 질을 높여줄 뿐만 아니라, 나중에 좀 더 경제적으로 코드를 수정할 수 있게 해준다. 또한 직접 코드를 만져보게 됨으로써 얻는 피드백은 설계를 향상시킨다. 이러한 모든 것들은 여러분이 좋은 코드를 쓸 수 있는 시간적 여유를 벌어 줄 것이다. 실제로 테스팅은 여러분이 더 좋고, 빠른 소프트웨어를 만드는데 도움을 준다.
원문 http://developer.apple.com/tools/unittest.html
번역 sunil








