IBM®
메인 컨텐츠로 가기
    Korea [국가변경]    이용약관
 
 
   
        제품    서비스 & 솔루션    고객지원 & 다운로드    회원 서비스    
한국 developerWorks   >   Open developerWorks  > developerworks

GUI를 단위 테스트하는 방법



류준호
류준호 junhoryu@gmail.com

윈도우 애플리케이션과 유닉스 서버 개발 경험이 있으며, 세상을 널리 이롭게 하는 소프트웨어를 만드는 꿈을 이루기 위해 노력하고 있다. 현재 구글의 서울 사무실에서 소프트웨어 개발자로 일하고 있다.


난이도 : 초급
2007년 10월 30일


[오픈 디벨로퍼웍스]는 여러분이 직접 필자로 참가하는 코너입니다. 이번 연재에서는 리치 클라이언트(rich client)를 배경으로 GUI를 테스트하는 이유와 효과적인 테스트 방법을 알아보고자 합니다.

자주 바뀌는 GUI일수록 단위 테스트가 필요하다

한 달이 멀다 하고 바뀌는 GUI에 테스트라니 시간이 아깝다고 말하는 개발자라면 오히려 단위 테스트의 진정한 가치를 아는 순간 열광하게 될 가능성이 높다. 물론 기능을 수정할 때 테스트 코드도 함께 변경해야 하고, 그로 인한 추가 작업에 시간이 든다. 하지만 대부분의 경우에는 그 시간을 감안해도 작업 전체 생산성은 향상된다. 이 말이 이상하게 들린다면 아래 이야기가 자신에게도 해당되는지 살펴보자.

   소셜 북마크

   mar.gar.in mar.gar.in
    digg Digg
    del.icio.us del.icio.us
    Slashdot Slashdot

문제없이 돌아가는 코드는 손대지 말라는 격언을 가슴속 깊이 새기고 있지는 않은가? 기능을 수정하기도 전에, 그로 인해 영향을 받는 다른 코드들을 파악하느라 긴 시간을 보내본 경험이 있지 않은가? 단위 테스트가 갖춰진 프로젝트라면 각각의 모듈을 독립적으로 테스트할 수 있다. 그렇기 때문에 특정 코드를 수정할 때 그 코드가 속한 테스트들만 통과하면, 외부 모듈과의 복잡한 상호작용에 신경 쓰느라 보내야 하는 긴 시간을 절약하게 된다. “GUI 작업하다가 생긴 버그는 디버거 한번 돌려보면 금새 찾을 수 있던데? 굳이 해야만 하나?”라고 말하는 사람은 타고난 실력의 개발자이거나 아니면 아직 뜨거운 맛을 보지 못한 건지도 모른다.


GUI 단위 테스트의 어려움

GUI에 단위 테스트를 적용하기 까다로운 이유는, 테스트 환경을 초기화하기 위해 사용자의 입력이 필요하기 때문이다. 그래서 사용자의 입력을 자동화하는 도구도 존재한다. 하지만 이는 서로 다른 프로세스간의 상호작용이기 때문에 테스트를 실행하기 전에 어느 정도 시간을 두고 기다려야 한다. 결과를 검증할 때에도 마찬가지로 일정 시간을 기다렸다가 확인해야 하므로 시간이 오래 걸려 단위 테스트라고 하기에는 적합하지 않다.

마땅한 방법이 떠오르지 않아 망설여진다면 이렇게 생각해 보자. 네트워크상의 다른 서버와 통신하는 코드를 테스트하고자 한다면 어떻게 할 것인가? 어쩌면 주저 없이 네트워크 라이브러리를 대신하는 테스트용 함수나 클래스를 구현해 네트워크 환경으로부터 코드를 독립시킨다고 할 것이다. 이 일반적인 방법을 GUI에 적용하려는 생각을 쉽게 하지 못한 건 GUI에 대한 고정관념 때문이다. 아래 그림 1은 흔히 볼 수 있는 도식이다. GUI가 애플리케이션의 레이어 중 가장 상단에서 사용자와의 상호작용을 담당하고 있음을 의미한다. 하지만 더 정확히 표현하면 GUI는 GUI 환경을 제공하는 시스템의 API를 개발자 대신 호출해 주는 모듈이기도 하다. 이 사실을 반영하면 그림 2와 같은 모습이 된다.

GUI

 

GUI

비즈니스 로직

 

비즈니스 로직

 

데이터 접근

 

데디터 접근

시스템

 

시스템

그림 1. 오해하기 쉬운 GUI의 역할                                      그림 2 시스템과 상호작용하는 GUI의 모습



위로



컨트롤러와 뷰의 분리

하지만 막상 GUI 프레임워크가 제공하는 클래스를 테스트용 클래스로 대체하기가 쉬운 일은 아니다. 이 작업을 진행하다 보면 곤란한 상황이 생기는데, 설명을 돕기 위해 간단한 프로그램을 하나 가정하자.



후보자 중 한 사람을 뽑는 프로그램이다. 후보를 추가하는 인터페이스가 화면 좌측에 있고, 중앙에는 참가자들을 선택하는 리스트가 있다. 그리고 오른쪽에는 후보 중 한 명을 선택하는 인터페이스가 있다. 단위 테스트를 고려하지 않은 일반적인 애플리케이션의 클래스 구조는 아래와 비슷할 것이다.



GUI 프레임워크가 제공하는 DialogView라는 클래스를 상속받은 LotteryView라는 클래스가 있고, 이 클래스에서 사용자의 입력을 처리한다. 추가 버튼을 누르면 AddCandidateEvent라는 이벤트 핸들러에서 후보자를 추가하고, 뽑기 버튼을 누르면 PickEvent가 실행되어 후보자 중 한 명을 선택해 그 이름을 보여준다.

다시 원래 문제로 돌아와서 DialogView를 테스트용 클래스로 대체할 때 생기는 걸림돌이 무엇인지 알아보자. 문제는 LotteryView가 DialogView에서 제공하는 풍부한 API들을 사용하고 있다는 점이다. 예를 들어 뷰를 초기화하는 InitializedEvent에서는 창의 속성이나 크기를 설정하는 DialogView의 다양한 메서드들(ConfigurationMethods)을 호출한다. 그리고 더 섬세한 GUI를 제공하다 보면 DrawEvent라는 이벤트 핸들러에서 창에 그림을 그리는 메서드들(DrawMethods)을 사용하게 된다. 이 모든 메서드들을 DialogView를 대신할 테스트용 클래스에 추가하지 않으면 컴파일조차 되지 않을 테니 답답한 일이다.

게다가 이들 중 대부분은 순수하게 화면에 보여주기만을 위한 코드일 뿐 테스트할 만큼 의미 있는 로직이 아닌 경우가 흔하다. MVC(Model View Controller)나 MVP(Model View Presenter)에서 말하는 뷰(view)는 단위 테스트의 대상이 아니다. 위에서 언급한 창의 속성을 설정하고 화면에 그림을 그리는 코드가 여기에 속한다. 반면에 “추가”나 “뽑기”버튼을 눌렀을 때 실행되는, 컨트롤러(controller)나 프레젠테이션(presentation)의 역할을 하는 코드들은 테스트하는 것이 바람직하다.

일반적인 라이브러리들은 인터페이스를 기준으로 테스트의 경계가 명료하다. 하지만 GUI 라이브러리는 뷰를 정의하는 코드와 뷰를 컨트롤하는 로직을 같은 클래스에 추가하는 것이 자연스럽도록 설계되어 있어서 이런 상황이 자주 발생한다. 이를 해결하기 위해 하나의 클래스에 공존하는 테스트하기 곤란한 코드와 테스트해야 하는 코드를 분리해야 한다. 아래와 같이 뷰를 컨트롤하는 로직들을 Lottery라는 새로운 클래스에 위임하고 LotteryView 클래스에서는 뷰를 정의하기 위한 최소한의 코드만 남겨두는 것이 좋은 방법이다. 그리고 LotteryView가 Lottery 클래스의 메서드들을 사용할 수 있도록 LotteryView에 Lottery의 인스턴스를 추가했다.



LotteryView는 AddCandidateEvent가 호출되면 Lottery의 AddCandidate를 호출하고, PickEvent가 호출되었을 때에는 Lottery의 Pick을 호출하기만 할 뿐 테스트가 필요한 코드가 남아 있지 않다. 이제 Lottery 클래스는 GUI 프레임워크와 상관이 없는 클래스이기 때문에 독립적으로 단위 테스트가 가능하다.



위로



뷰의 동기화

하지만 이것이 끝이 아니고, LotteryView가 Lottery에 위임한 코드의 실행 결과를 동기화해야만 비로소 화면에 그 결과가 반영된다. 이를 구현하는 두 가지 방법이 있다.

  1. LotteryView가 Lottery로부터 데이터를 가져온다.
  2. Lottery가 LotteryView의 데이터를 설정한다.

1)의 경우는 Lottery 클래스의 AddCandidate와 Pick 함수가 결과값을 반환하도록 하기만 하면 되기 때문에 구조에 변화가 없다.



한편 2)는 LotteryView에 set 함수들을 추가해야 하고, Lottery 클래스도 LotteryView의 객체를 참조해야 하므로 아래와 같이 구조가 변한다.



1)의 경우 클래스 구조뿐만 아니라 이를 구현한 코드도 간단하다. 그럼에도 불구하고 객체지향형 디자인적인 측면에서 봤을 때 2)번이 더 바람직한데, 그 원인을 예제를 통해 알아보자.

1)의 경우의 아래와 같이 단 한 줄에 구현할 수 있다.

  

LotteryView::PickEvent()
{
selectedCandidate = lottery.PickOne();
}
                                        

그런데 나중에 애플리케이션의 요구사항이 한 명이 아닌 두 명을 뽑도록 바뀌었다고 가정해보자.



이 경우 급한 일정에 쫓거나, 여러 사람이 함께 작업을 하다 보면 누군가는 아래와 같은 코드를 작성하기가 쉽다.

  

LotteryView::PickEvent()
{
selectedCandidate = lottery.Pick();
do {
selectedCandidate2 = lottery.Pick();
} while(selectedCandidate == selectedCandidate2);
}
                                        

코드는 여전히 간결한데, 이 코드가 컨트롤러가 아닌 뷰에 추가되어 테스트가 힘들어졌다.

이번에는 2)번과 같이 뷰 클래스가 set 함수들을 통해 수동으로 데이터를 업데이트 받도록 설계했다고 가정해보자. 이 경우에는 뷰에 코드를 추가하려면 컨트롤러에서 데이터를 가져올 수 있도록 인터페이스를 수정해야 한다. 코드를 읽어 본 개발자라면 일관성을 깨는 이 작업을 어색하게 느끼고 뷰 대신 컨트롤러에 코드를 추가할 것이다. 이는 능동적으로 데이터를 처리하는 컨트롤러의 인터페이스를 개방하는 대신, 수동으로 데이터를 업데이트 받는 클래스의 인터페이스를 개방함으로써 바람직한 캡슐화(encapsulation)를 이룬 덕분이다.



위로



정리

  • GUI 라이브러리를 상속받은 클래스에는 최소한의 코드만 남기고 로직은 단위 테스트가 가능한 다른 클래스로 옮기자.
  • 뷰와 컨트롤러간의 데이터를 동기화할 때에는 컨트롤이 능동적으로 뷰를 변경하도록 하고, 컨트롤러의 데이터는 캡슐화를 통해 감추자.

이번에 우리가 작성한 코드는 실제로 50줄 정도다. 이 정도의 코드로 테이블 뷰에 데이터를 입력하고, 한 행의 총합을 출력하고, 저장 기능까지 추가하였다. 입력한 코드 내용은 얼마 되지 않지만 실제로 다룬 내용은 정말 많다. 사실 코코아 바인딩에 관한 내용과 아카이브에 관한 내용은 여기에선 코드 설명만을 위해 간략하게 하였지만 이 두 가지 주제만으로도 책 반 권 정도의 분량을 차지할 내용이다. 이미 코코아에 익숙한 독자들은 무리 없이 이해하고 넘어갔겠지만 코코아엔 익숙하지 않은 독자들은 다음 두 문서를 읽어보면 도움이 될 것 같다.


참조

  • The Humble Dialog Box, Michael Feathers – 테스트 주도 개발 방식의 순서에 맞춰 GUI의 단위 테스트를 구현하는 방법에 대해 설명한다.
  • Passive View, Martin Fowler – 뷰를 동기화는 다른 방법들도 소개한다.


이제 전문가의 글을 단순히 ‘보는 것’에서, 직접 여러분이 developerWorks의 필자가 될 수 있습니다. IBM developerWorks를 통해 공유하고 싶은 지식이 있으신 분들은 원고 기획안을 접수해주세요. 채택되신 분께는 소정의 원고료를 드립니다.
Open developerWorks 신청하기   MS워드 아이콘   아래아한글 아이콘



[지난 Open dW 보기]

사이트 여행

dW 커뮤니티
포럼 | 블로그 | Spaces
dW Student Community

로컬 컨텐츠

행사 및 세미나

기획 기사

개발자 입문

튜토리얼 및 교육

TOP 10 인기자료

SW 다운로드

RSS 피드

뉴스레터
  
자바스크립트가 작동이 중지되었습니다. 이 기능을 수행하시려면 브라우저에서 자바스크립스트를 작동시켜 주시거나 이곳을 클릭해주세요.
Special offers
IBM SOA Sandbox 시험판
dW Student Community
로보코드
코드 트레이닝


    IBM 소개 개인정보 보호정책 문의