티스토리 툴바

KSUG 1회 세미나이기도 한 이프릴 세미나 첫 발표는 "다중 레이어 환경에서 Spring을 활용한 통합 테스트 및 단위 테스트 방안"이다. 당시 국내 스프링 사용자에게 거의 알려지지 않은 보석 같은 내용을 일주일 동안 연습해서 시연하던 기억이 생생하다. 월간 마소에 일민 형이 기고한 글을 줄거리로 한 라이브 코딩이었다.

사용자 삽입 이미지

사진 출처: 이프릴(Epril) 스프링 세미나 풍경

그간 스프링의 테스트 지원 기능은 Java 5 프로그래밍 요소에 맞춰 우아하게 변모하여 Spring TestContext Framework로 발전했다.

이와 관련한 책 읽기 모임 의견을 들어보자. 찬욱군은 TestContext를 학습하면서 JUnit 3.8과 JUnit 4.x 호환성을 지키는 부분에 놀랐다고 한다. 나는 주입을 위해 굳이 생성자나 수정자가 필요하지도 않게 배려한 부분에서 감동했다. :)

테스트 코드에 의한 DI
책을 따라 실습해보면 무리 없이 스프링 테스트 컨택스트를 배울 수 있다. 다만, @DirtiesContext 내용이 정확히 와 닿지 않았다. 이런 경우라면 다음 절에 소개하는 학습 테스트를 만들어 명확히 할 수 있다. 여러 개의 테스트 클래스를 만들고 한 쪽에 정적 변수로 ApplicationContext 개체를 참조할 수 있게 한다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/testcontext/applicationContext.xml")
public class AaaTest {
   
    static ApplicationContext firstContext;

그리고 테스트 클래스에서 정적 변수를 한 번은 초기화하게 한다.

    @Before
    public void setUp(){
        if(AaaTest.firstContext == null) AaaTest.firstContext = applicationContext;
    }

마지막으로 정적 변수와 멤버 변수인 applicationContext가 가리키는 개체가 같은지 비교하는 테스트를 여러 개 만든다. @DirtiesContext를 클래스에 붙였다가 메소드에 붙여본다.

    @Test
    public void test1(){
        assertEquals(AaaTest.firstContext, applicationContext);
    }
   
    @DirtiesContext
    @Test
    public void test2(){
        assertEquals(AaaTest.firstContext, applicationContext);
    }

@DirtiesContext를 클래스에 붙이면 해당 클래스가 끝날 때 애플리케이션 컨텍스트가 새로 만들어진다. 메소드에 붙이면 해당 메소드가 끝나면 새로 만들어진다.

저자의 꼼꼼함을 들여다볼 수 있긴 했으나, 찬욱군은 현장에서 @DirtiesContext가 필요한 경우에 대해 아직 감이 없다고 한다.

컨테이너 없는 DI 테스트

UserDaoTest는 사실 UserDao 코드가 DAO로서 DB에 정보를 잘 등록하고 잘 가져오는지만 확인하면 된다. 스프링 컨테이너에서 UserDao가 동작함을 확인하는 일은 UserDaoTest의기본적인 관심사가 아니다. DaoFactory를 만들어 의존관계 설정 책임을 분리하기 직전에 테스트 코드에서 DI 작업을 직접 했던 것을 기억할지 모르겠다. 바로 그 방법을 사용해서 UserDao가 동작하게 만들고 테스트할 수 있다.

2장까지 내용을 충분히 이해했다면 쉽게 읽고 넘어갈 내용이지만, 사실 관심사 분리(SoC) 훈련이 없다면 이해하지 못할 내용이 아닐까 싶다. 혹시 발췌한 내용이 이해가 가지 않거나 갑자기 왜 이런 설명을 하는지 이해가 안 간다면 안타깝지만 1장을 다시 읽어야 한다. :)

DI를 이용한 테스트 방법 선택
다양한 방법을 소개하고 마지막에 선택 기준을 제시한다. 1번은 역시 스프링 컨테이너조차 없는 테스트다. 앞서 설명한 고립성과 시간 절약은 스프링 컨테이너에도 그대로 해당한다. 스프링 컨테이너를 이용하는 기준은 무엇인가? 여러 개체와 복잡한 의존관계를 갖는 개체를 테스트할 경우다. 이 경우는 테스트 설정을 따로 만드는 방법이 좋다. 세 번째는 예외적인 의존관계를 강제로 구성할 경우다. 이때는 앞서 학습한 @DirtiesContext가 유용하다.

§ 2.5 학습 테스트로 배우는 스프링
작년인가 월간 마소에 JUnit을 이용한 코드 개선 과정 녹화라는 글을 기고한 일이 있다. 개발 후 운영자도 내막을 잘 모르는 레거시 코드를 학습하는 과정과 이를 공유하는 수단으로 테스트를 사용한 경험담이다. 저자가 열거한 학습 테스트 다섯 가지 장점을 그 경험에 대입해보니 그대로 들어맞는다. 학습 테스트는 과정에서 배우는 바도 매우 크지만, 결과를 보존하면 문서로는 전달하기 어려운 생생한 재현을 보장한다. 특히나 프레임워크나 제품의 주요 기능 혹은 API 쓰임새를 포괄하는 테스트를 만들어 둔다면 마치 TCK(Technology Compatibility Kit)처럼 훌륭한 호환성 검증 도구 역할을 할 수 있다.

설정

트랙백

댓글

* 책 읽기 토론 내용을 반영해 수정 (2010.10.30) <- 최초 작성(2010.9.30)

157쪽에서 수정 후에는 성공 메시지가 "조회 테스트 성공"으로 바꾸어야 하는데 출판사 정오표에는 빠진 오류인 듯하다. 159쪽 하단에 JUnit 필수 조건에도 void 가 빠져 있다. 170쪽에 내용이 나오는 내용인지라 저자가 실수로 빠뜨린 듯하다. 저자는 스프링 3.0에서 처음 등장한 설정 방법부터 소개한 것처럼 assertEquals() 메소드보다 assertThat() 메소드부터 소개한다. assertThat은 예전에 만든 교육 자료가 있어 일부를 공개한다.


Joe Walnes의 글은 Flexible JUnit assertions with assertThat()이다. assertThat 자체는 JUnit에 들어갔지만, 책에서 사용하는 Matcher는 hamrest 라이브러리에 있다. 책 읽기 모임에서 나온 의견을 덧붙여 보자. JUnit을 충분히 써본 찬욱군은 굳이 assertThat을 사용하는 이유를 모르겠다고 한다. 이에 대해 용권씨는 Scala 학습 전에는 자신도 몰랐으나 이제는 DSL로 대변하는 가독성 있는 스타일에 대해 이해가 가며, assertThat도 같은 맥락으로 이해한다고 했다. 내 생각엔 assertThat 자체는 영어문화권이 아닌 우리에게 큰 의미는 없다. 다만, Matcher는 (클래스 수준 이하) 단위 테스트를 돕는 훌륭한 개념이기 때문에 은근 슬쩍 스프링 학습과정에서 배웠으면 하는 저자의 노파심이 아닐까 싶다. 찬욱군 의견처럼 161쪽 리스트 2-5는 assertEqual이 차라리 낫다. :)


JUnit에 hamcrest가 포함되어 있긴 하지만, 테스트 주도 개발 TDD 실천법과 도구에 따르면 최신 버전(1.2)은 직접 내려받아야 한다. hamcrest에 대한 자세한 내용은 테스트 주도 개발 TDD 실천법과 도구에서 배울 수 있다. 134쪽부터 약 15쪽 정도 분량으로 hamcrest 기초부터 확장 방법까지 설명하고 있다.

테스트 주도 개발 - 10점
채수원 지음/한빛미디어


205~206쪽을 보면 종전 사용법까지 포함한 여러 가지 확인 코드 작성 방식을 보여준다. 저자는 더 나아가 자신의 블로그를 통해서 JUnit assert 매쉬업이라는 최신 기법까지 소개하고 있다.

개발자를 위한 테스팅 프레임워크 JUnit
저자는 다시 한번 테스트를 강조한다.

다시 말하지만 테스트 없이는 스프링도 의미 없다.

역시 지나치다 싶을 정도로 초강수를 둔다. 하지만, 단위 테스트가 주는 이점(품질 향상, 빠른 개선 주기 지원)을 공감하지 못하는 개발자에게 이 정도 위협(?)이 먹힌다면 의미는 있다. 필자는 다른 회사가 개발을 맡는 2번의 프로젝트에서 DAO 단위 테스트만 의무화했던 경험이 있는데 모두 예상치를 웃도는 효과를 얻었다. '우리도 한번 해보자!'라고 하시는 분은 저렴한 방법으로는 테스트 주도 개발 TDD 실천법과 도구 학습이 있고, 비용이 들어가는 방법으로는 필자에게 컨설팅 요구를 하실 수 있다. :)

동일한 결과를 보장하는 테스트

따라서 테스트 후에 USER 테이블을 지워주는 것도 좋지만, 그보다는 테스트하기 전에 테스트 실행에 문제가 되지 않는 상태를 만들어주는 편이 더 나을 것이다.

혼동하기 쉬운 내용을 잘 지적하는 내용이다. 외부 요인이나 실행 순서가 테스트에 영향을 미치지 않도록 실험실 꾸미듯 단위 테스트를 만들어야 한다.

주의해야 할 점은 두 개의 테스트가 어떤 순서로 실행될지는 알 수 없다는 것이다. JUnit은 특정한 테스트 메소드의 실행 순서를 보장해주지 않는다. 테스트의 결과가 테스트 실행 순서에 영향을 받는다면 테스트를 잘못 만든 것이다. 예를 들어 addAndGet() 메소드에서 등록한 사용자 정보를 count() 테스트에서 활용하는 식으로 테스트를 만들면 안 된다. 모든 테스트는 실행 순서에 상관없이 독립적으로 항상 동일한 결과를 낼 수 있도록 해야 한다.


포괄적인 테스트
토스 177쪽을 보면, 스프링의 창시자인 로드 존슨이 '항상 네거티브 테스트를 먼저 만들라'라고 조언했다고 하는데 정말 그럴까?  Expert One-on-One J2EE Development without EJB 430쪽에 Negative Tests라는 내용이 나온다. '항상 먼저 만들라'라고 한 내용은 찾을 수 없지만, 바람직한 작동을 위한 테스트만큼이나 비정상적일 때에 대한 테스트도 잊지 말 것을 강조하고 있다. 영어라 접근에 제약은 있지만, Expert One-on-One J2EE Development without EJB 14장은 J2EE/Java EE 영역에 있어서만큼은 가장 얇으면서 풍부한 내용을 담은 주옥같은 글이니 꼭 읽어보길 권한다. 사실 많은 내용이 토스에 녹아 있긴 하다.

책 읽기 모임에서 용권씨가 막장(?) 사례를 말해줬다. 이를 테면 assertTrue(true)와 같은 코드로 단위 테스트를 한 경우도 있단다. ㅡㅡ;

반면에 찬욱군은 외부 솔루션이나 고객 사이트 환경을 모르고 대답해주는 이가 없어서 대충 테스트 했던 과거를 회계했다.

테스트 코드 개선
테스트 순서나 외부 요인에 영향을 받지 않는 고립 테스트의 중요성은 여러 차례 이야기했다. JUnit은 이를 위해 테스트마다 별도 개체를 생성한다. 토스 2장에서는 실습과 함께 체득하기 때문에 효과적으로 익힐수 있다.

픽스처
테스트 픽스처(Test fixture)에 대한 위키피디아 정의를 옮겨본다.

Test fixture refers to the fixed state used as a baseline for running tests in software testing. The purpose of a test fixture is to ensure that there is a well known and fixed environment in which tests are run so that results are repeatable. Some people call this the test context.


설정

트랙백

댓글

* 책 읽기 토론 내용을 반영해 수정 (2010.10.30) <- 최초 작성(2010.9.29)

아래 발췌한 도입부 내용은 테스트에 대한 강조가 지나쳐 마치 스프링을 볼모로 독자를 위협하는 듯한 인상을 받는다.

스프링으로 개발을 하면서 테스트를 만들지 않는다면 이는 스프링이 지닌 가치의 절반을 포기하는 셈이다. 스프링 개발자라면 테스트 작성 방법과 이를 효과적으로 개발에 활용하는 전략을 알아야 하며, 이를 실전에 적용할 수 있어야 한다.

스프링 프레임워크 자체는 테스트를 강제하지 않는다. 그럼에도 저자가 2장 전반에 걸쳐 무리다 싶을 정도로 강조하는 이유는 무엇일까? 추측건대 테스트를 거의 하지 않는 개발 풍토 탓이 아닐까? 2005년부터 현재까지 참여한 9개 프로젝트 가운데 단위 테스트 작성에 성공(?)한 경우는 5회이고, 실패한 경우는 4회다. 다른 이유를 배제하면 단위 테스트에 성공한 다섯 번은 모두 납기 내에 시스템을 오픈했다. 단위 테스트 작성을 도입하지 못한 4번 중 3개의 프로젝트는 필자가 조기 철수하는 프로젝트여서 결말은 모르지만 무리한 야근이 있었다는 점은 확인할 수 있었다. 그리고 필자의 참여 비중(주 1회)이 낮아 어찌할 수 없이 실패를 지켜봤던 1번은 6개월가량 오픈이 늦어졌다. 자동화 테스트와 회귀 테스트를 충족하는 단위 테스트의 효과는 분명한데 실전에 도입하기란 만만치 않다. 필자가 성공한 5번의 프로젝트 중에 3번은 우리 팀만으로 개발한 경우다. 우리 팀에서도 단위 테스트를 한 번도 작성하지 않았던 개발자가 있었지만, 짝 프로그래밍 형태로 일주일만 가르쳐주면 무리 없이 테스트를 작성했다. 문제는 소속이 다른 여러 조직에서 개발하는 경우다. 필자는 항상 단위 테스트를 주장하지만, 개발이 끝날 때까지 참여하는 경우만 힘을 쓸 수 있었다. 필자를 대신하여 테스트를 유도하고, 개발자가 막힐 때 방법을 알려줄 사람이 없기 때문이다. 성공한 두 차례중 한 번은 외국에서 훈련 받은 최고수준의 프로젝트 관리 진이 포진한 경우였다. 그렇지 않은 한 차례는 필자가 속한 팀이 프로젝트 관리 조직을 장기간 계몽(?)했다. 사실 투쟁의 역사이기도 했다. 그리고 실무적으로도 1,000회 정도의 코드 인스펙션을 수행했다. 우리 팀 개발자는 '차라리 내가 짜는 편이 빠르겠다'는 불평을 수없이 내뱉었다. 이런 사회적 배경을 고려하면 테스트는 아무리 강조해도 지나치지 않다.

토론 모임에서는 '스프링을 통해 처음으로 자동화 테스트를 경험했다'는 사람이 많았다. 소수지만 모인 사람만 놓고 보면 압도적인 비중이었다. 한편으로는 스프링이 현실에서 테스트를 적합하게 해낸 사례기도 하다.

작은 단위의 테스트

직접 관계는 없지만, JSP 모델1 구조에서 화면 단위로 테스트하던 모델2 MVC 구현일텐데. 그 부분이 아니라 아이들이 학교와 와야 의미가 있겠죠.

지금까지 UserDaoTest를 수행할 때 매번 USER 테이블의 내용을 비우고 테스트를 진행했다. 이렇게 사용할 DB의 상태를 테스트가 관장하고 있다면 이는 단위 테스트라고 해도 된다. 다만, DB의 상태가 매번 달라지고, 테스트를 위해 DB를 특정 상태로 만들어줄 수 없다면 그때는 UserDaoTest가 단위 테스트로서 가치가 없어진다. 그런 차원에서 통제할 수 없는 외부의 리소스에 의존하는 테스트는 단위 테스트가 아니라고 보기도 하는 것이다.

토스 151쪽 내용은 단위 테스트 경험자라면 누구나 고민해봤거나 고민했어야 할 내용을 잘 짚어낸 좋은 예다. 단위 테스트의 고립(isolated) 테스트 특성을 잘 설명한 내용이다. 필자는 2006년 단위 테스트의 "단위"라는 글을 쓸 즈음에 단위 테스트 개념을 이해하려고 여러 가지 시도를 한 바 있다. Mock을 이용한 테스트가 단위 테스트를 이해하는 데 큰 도움이 되었다. 국내 블로그 등에서 "단위"에 대한 고민을 찾을 수 없다는 점은 얼마나 단위 테스트를 안 하는지를 반증한다. 예전에 단위 테스트에 대해 고민했던 흔적을 남겨둔다.


저자는 작은 단위 테스트의 효과를 문제가 발생했을 때 찾을 수 있다고 설명한다. 프로젝트 전체를 놓고 생각하니 V 모델이 입자 크기와 겹쳐져서 떠올랐다. 프로젝트 초반에는 시스템을 규정하는 입자가 크고, V 모델 하부 구현단계에 들어서면 입자가 가장 작아졌다가 점차 테스트 입자가 커지는 그림이 머릿속에 그려졌다.

File:Systems Engineering Process II.svg
출처: 위키피디아

검증과 확인을 제공하는 V 모델의 효과를 극대화하는 방법은 무엇이 있을까? 수학적으로 증명할 수는 없지만, 빠른 피드백을 받고자 한다면 가장 작은 V 인스턴스(?)를 만드는 방법이다. 다시 말해서 작은 단위 테스트를 활용하는 방법이다. 필자가 켄트벡의 TDDBE를 읽고 가장 인상 깊었던 부분이 바로 작은 테스트를 통해 찾아가는 리듬인데, 앞서 사용한 표현에 따라 가장 적절한 V 인스턴스 크기라고 말할 수 있다. 린 소프트웨어 개발의 적용에서도 작은 테스트를 지지하는 경구를 제공한다.


하지만, 적절한 V 인스턴스 크기를 찾는 일은 매우 높은 밀도로 상당 기간 경험을 쌓았을 때 주어지는 선물이다. 볼링 게임으로 처음 TDD를 배우던 때를 회상하면 밥 삼촌이 카타에서 보여준 놀라운 칼질은 내공 차이를 명확히 보여준다.

저자는 단위 테스트에 대해서 개발자 테스트라는 다른 표현을 제시한다. 책에서 다루는 단위 테스트 작성자가 개발자임을 분명히 한다. 단위 테스트 수행자가 누구냐에 따라 테스트 목적이 달라지고, 프로젝트 갈등 구도도 달라진다. 하지만, 설사 제삼자 테스트 형태로 단위 테스트를 수행하는 이상적인 조직이 있다고 해도 개발자 스스로 단위 테스트를 해야 한다. 그렇지 않은 코드는 부실 코드(anemic code)라 불러 마땅하다.

자동수행 테스트 코드

이렇게 테스트는 자동으로 수행되도록 코드르 만들어지는 것이 중요하다. 어떤 개발자는 모든 클래는 스스로 자신을 테스트하는 main() 메소드를 갖고 있어야 한다고 주장하기도 한다. 굳이 모든 클래스의 main() 메소드에 테스트 코드가 들어가 있을 필요까지는 없겠지만, 어쨌든 테스트 자체가 사람의 수작업을 거치는 방법을 사용하기보다는 코드로 만들어져서 자동으로 수행될 수 있어야 한다는 건 매우 중요하다.

저자는 수치까지 예를 들며 자동화 테스트의 중요성을 설명한다. 사실 자동화 테스트 작성의 생소함 탓에 놓치기 쉬운 그러나 실로 엄청난 자동화 테스트의 위력이다. 프로젝트 전체를 놓고 보면 잘 만든 자동화 테스트가 모여 만들어낸 회귀 테스트 자산의 힘은 말로 설명하기 어렵다. 앞에 언급한 필자의 경험을 놓고 보면 프로젝트 성공의 핵심 열쇠 중 하나다.

작은 시간이라도 아끼라는 자동수행 테스트 저번에 깔린 교훈은 필자가 학교에서 C++을 배울 때 가장 먼저 마음에 새긴 원칙인 Principle of least privilege을 떠오르게 한다. 이른바 POLA 원칙은 변수의 가시성(scope) 맥락에서 배웠지만, 자원 사용에 대한 정책이나 보안에도 응용된다. 그런데 지금 생각해보니 프로젝트에 주어진 시간 자원에 적용해도 의미가 살아난다.

지속적인 개선과 점진적인 개발을 위한 테스트
다시 한번 V 모델을 떠올린다. 작은 단위 테스트가 쌓여 회귀 테스트 기반을 이루어 시스템 전반을 검증할 수 있는 토대를 만드는 양상은 실용적인 V 모델 적용의 전형적인 예다. 아쉽게도 많은 프로젝트에서는 V 모델의 맥락을 충분히 이해하지 못하고 높은 성숙도를 요하는 이론을 그대로 적용하려고 옥신각신하는 모습을 볼 수 있다. 진정 V 모델을 실현하고자 한다면 긴 호흡으로 계획하여 단장은 필수적인 단위 테스트부터 팀/조직에 보급하고 성숙도를 높아지면 더 큰 그림을 그리는 방법이 어떨까?

UserDaoTest의 문제점
옥에 티 수준이지만, 두 번째 문제점 제목은 실행 작업의 번거로움보다는 체계적인 테스트 실행의 어려움이 나을 듯하다. UserDaoTest를 수정한다고 실행 작업을 벗어나는 것은 아니다. 155쪽 내용에도 체계적인 테스트 실행과 결과 확인에 대해 이야기하고 있다.

설정

트랙백

댓글

테스트 주도 개발 - 10점
채수원 지음/한빛미디어

채수원님으로부터 책을 증정받은 지가 한참인데 바쁘다고 잊고 살다가 일민형이 백년만에 쓴 서평을 보고 몇 자 덧붙인다. 일민형 서평에 완전 공감하는지라 "me, too." 라고 달아도 충분하지만 짧게 추가.

한마디로 정말 좋은 책이다. 이미 TDD에 익숙한 사람이라도 한번은 훑어볼만한 책이고, TDD에 익숙치 않다면 MUST HAVE 아이템이다. 이 책은 TDD나 자동화 테스트 실천에 필요한 꼭지를 두루 다루고 있다. 일민형이 책 분량이 적다고 아쉬워했는데 난 아이폰이나 플래쉬 책만큼 많이 팔리지는 않는다는 점이 아쉽다. 사실 그래서 더욱 값진 책이다. 열악한 시장성(?)에도 불구하고 집필한 저자의 노력과 의지의 결과물이니까.

책 Q&A를 위한 그룹스가 있다. '애프터'를 만들어가는 독자를 기대하며 찾은 김에 가입을 했다. 책 공식 사이트책에 실어준 인터뷰가 있길래 링크한다.


설정

트랙백

댓글

Update 주소 메모할 겸 기록함

* Test Coverage 측정: Alt+Shift+X, T 대신 Alt+Shift+E, T만 써주면 OK
http://update.eclemma.org/

* TestCase와 테스트 대상 사이에서 Jump to 지원:
http://moreunit.sourceforge.net/org.moreunit.updatesite/

관련 글:
TDD를 위한 기본 코딩 습관 3종 세트
TDD를 위한 이클립스 메소드 생성 템플릿

설정

트랙백

댓글

SI 현장에서 자동화 테스트를 적용하면서 테스트 작성 여부에 따라 개발자 성향이 바뀌는 모습을 경험할 수 있다. 같은 기반 기술을 사용하지만, 개발 일정이 달라 A 시스템과 B 시스템 개발자 투입 사이에 한 달 정도 공백이 있었다. 국내 SI에선 JUnit 테스트 작성을 필수 활동으로 삼고, 테스트 내용이 적절한가 까지 동료 검토 혹은 인스펙션하는 일은 흔치 않아 반발이 심하다. 우여곡절 끝에 개발이 한 달쯤 지나자 90% 가까이 테스트를 작성한다.

A 시스템 개발이 약 두 달쯤 진척했을 즈음의 일이다. 공통으로 사용하는 기반 코드(혹은 프레임워크)에 API 변경이 필요했다. 그래서, 거의 모든 DAO 클래스에 몇 줄이지만 변경이 필요했다. 공교롭게 개발 착수가 늦은 B 시스템 개발자에게 먼저 공지를 했다. 예상대로 상당한 반발을 겪을 수 있었다. 심지어 아직 완성한 DAO가 하나도 없는 개발자마저 변경 발생에 대해 불평을 토로했다. B 시스템 개발자는 한 달 남짓 새로운 개발환경(Spring과 X-internet 사용 기반을 공통화한 기반 코드 사용 및 화면 입력 데이터 및 DB 접근 로직에 대한 JUnit 작성)에 익숙해지는 중이었다.

긴 설득 과정에서 얻은 피로감을 안고 이번에는 A 시스템 개발자에게 공지했다. A 시스템 개발자는 두 달 전과 달리 수정사항에 대해 거부감이 확연하게 적었다. 단지, '바꿔야 하는 이유'와 '현재 방법이 가장 나은가?' 등을 확인해왔다. 안도감을  넘어서 고마움을 느낄 수 있었다. 태도 변화는 어디서 왔을까? 답은 확실했다. 우리는 TDD를 채용하지는 않았다. 무슨 말이냐면, 테스트를 먼저 만들지는 않는다. 사용자 입력 값과 DB 상태를 고려하여 서버측 로직이 정상작동 하는가를 확인하는 기능 테스트를 JUnit 기반으로 작성한다. 어차피 화면을 통해 제삼자가 테스트를 하기 때문에 JUnit이 제공하는 기능 검증 효과는 크지 않았다.[각주:1] 가장 두드러진 효과는 회귀 테스트가 주는 이점이다.

이와 달리 눈에는 잘 띄지 않았지만, 훨씬 강력한 이점을 발견했다. 아직 대다수 개발자가 JUnit 테스트 작성에 서툴다 보니 동료 검토를 강화했다. 개발자가 코드를 작성하고 나면, 이슈 트래커(툴은 Redmine)를 사용하여 동료 검토를 요청한다. 코드 전체를 몇 사람이 나누어서 검토해주고 지적사항은 '결함'이나 '권고'로 다시 이슈 트래커에 올린다. 처음에는 개발자가 수정에 반감을 갖지만, 반복하다 보면 일상으로 변한다. 일상으로 변하면, 이유가 어떻든 더 나은 코드를 만들기 위해 수정을 수용한다. 회귀 테스트를 갖췄으니 리팩터링은 한결 수월하다. 자동화 테스트를 몇 달 이상 수행한 개발자는 분명히 그렇지 않은 개발자 혹은 테스트 수행하기 전보다 변화에 대해 관대했다. 물론, 테스트를 작성하지 않아도 동료 검토 과정을 통해 성향을 바꿀 수 있다. 하지만, 기준 부합 여부를 명확히 성공이나 실패로 판별해주는 방법이 있는 경우는 그렇지 않은 경우에 비해 확연히 유리하다.
  1. 화면을 띄워서 확인하는 기능 테스트를 JUnit 기반으로 대체하려면 테스트 케이스 작성에 더 큰 공수를 투입해야 하는데 현실적으로 어려웠다. [본문으로]

설정

트랙백

댓글

Toby(이하 굵은 글씨): 근데 dao테스트의 데이터는 어떻게 만들어 써?

엑셀로 만들어서 DBUnit 써서 올리지. 형한테 배운 방법이야. 스프링이 제공하는 트랜젝셔널 테스트[각주:1] 안에서 테이블 레코드를 지우고, 초기 데이터를 부어 넣는 방식으로 현재 개발 DB에 있는 데이터 상태에 영향을 받지 않게 했지. 그런데, 업무 자체가 이력을 포함한 다량의 실 데이터로 검증해야 의미를 가지기 때문에 테이블 전체를 지우는 일이 큰 부하를 발생시키지. 개발 표준을 전담한 후배가 유틸 함수인 deleteFromTables() 를 쓰도록 가이드하니까 개발자 모두가 일괄적으로 쓰다가, 기준 정보 2천만건을 지우느라 문제를 일으켰지. CI 돌리니까 DB 풀나는거야. DBA가 세션 모니터링 해서 그렇게 지울꺼면 Truncate를 하라고 권고를 했지.

테스트 코드에서 부작용 막으려는 선행 작업이니까 튜닝할 문제는 아니잖아. 그래서, jdbcTemplate.execute() 함수를 써서 insert 할 대상만 조건(Where) 걸어서 지우거나 get 인 경우에 주석으로 전제를 달고 이미 데이터가 존재한다는 전제하에 지우지 말고 테스트 하라고 해줬지. 그랬더니 그 문제는 사라졌어. [각주:2]

답이 좀 핀트가 어긋났네. 데이터 만드는 방식은 데이터 허브격인 EDW나 기간계 데이터를 기초로 처리하는 업무인터라 생성하기가 좀 수월하지. 질문의 요지가 단위 테스트를 위해 잘 선별한 Sampling 방식을 물은건지 모르겠는데... 그점까지 고려할 정도로 테스트에 투자할 여력은 없어. 오히려 고민은 내일부터 배치 테스트 자동화를 위한 구체적인 실천 방안을 협의하는데... 다량의 데이터를 전수 비교하긴 힘드니까이 부분을 해결하기 위한 묘수를 찾는 일에 생각을 많이 쏟고 있지.

내가 하려고 했던 얘기는 order by 를 포함한 sql 전체 테스트는 간단하지만 검증가능한 작은 데이터를 만들어 두고, 그 결과를 골든 마스터랑 비교하는게 낫지 않냐 하는 얘기 할라고 한거지

맞아. 그렇게 하고 있어. add 유형의 함수(insert를 encapsulation)는 주로 화면 입력 값에서 유추할 수 있는 전형적인 DTO 객체를 생성해 add() 실행을 권고 하고 있어. insert 의 경우는 입력한 모든 필드에 대해 검증(assertion) 코드를 넣게 하고, update 류 함수는 수정한 필드에 대해서 비교하게 기준을 잡았지.

목록형 CRUD를 진행할 때는 엑셀에 저장한 레코드 상태를 골든 마스터로 써. 일단 온라인 화면 단위 테스트(실제로는 DAO 구현체 테스트로 대체)는 개발자가 테스트 데이터를 만드는 책임을 지는 형태로 테스트 자동화를 수행하는데, 배치는 컨설턴트로 구성된 업무 전문가가 엑셀로 값을 만들기로 했어. 배치 테스트 자동화는 아직 실제 적용하기 전이고.




  1. 자동 롤백 효과를 제공해 테스트 메소드 안에서는 고립 테스트를 보장하는 방식 [본문으로]
  2. 다른 이야기지만, SVN 싱크를 시키지 않는 어딘가에서 DELETE FROM '문제의 기준 정보 테이블' 쿼리를 자꾸 날려서 지금은 모듈 확인 Aspect를 걸어서 모니터링 하고 있다. [본문으로]

설정

트랙백

댓글

얼마 전 개발 일정을 줄이는 방법-테스팅의 오류를 거론한 글을 쓴 바 있다. 간과할 수 없는 오류가 있어 짚고 넘어갔으나, 후속으로 블로고스피어에 등장한 글을 보면 내 글 역시 균형잡힌 내용은 아니다. 칼럼 필자의 이전 칼럼을 읽어 보면 개발 일정을 줄이는 방법-테스팅을 통해 진짜 하고 싶은 말이 무언인지 짐작할 수 있다. 정작 하려던 말을 골자는 우리는 개발자가 테스트해요. 에서 주장하는 바가 아닐까? 개발자에게 테스트 책임을 전적으로 위임하는 일은 위험하다.

자급자족(?)이 아닌 이상 최종 사용자의 요구에 맞는 결과인지를 판단 주체가 개발자일 수는 없다. 내용이 아닌 절차 측면에서 보면, 작업 주체가 합격 여부를 결정한다면 상대적으로 객관성이 떨어질 수밖에 없다. 개발과 검증은 분명히 다른 성격의 일이다.

검증 방법의 하나가 테스트인데, 테스트는 수행 주체, 목적 및 대상에 따라 구분할 수 있다. 혼자 개발하는 경우가 아니라면 특정 개발자가 만든 코드와 다른 개발자가 만든 코드를 합쳐서 하나의 시스템으로 수행하는 경우가 흔하다. 보통, 통합(Integration)이라고 부르는데 통합이 수월하게 하고, 이전에 만든 코드를 수정할 때 영향도 파악이 수월하게 하려면 개발자 테스트를 만들어야 한다. 이러한 개발자 테스트는 앞서 언급한 검증 목적으로 수행하는 테스트와는 구분할 수 있으며, HelloWorld 규모가 아닌 이상 자동화 테스트가 필수다.

프레임워크나 라이브러리와 같이 개발팀이 만든 코드를 사용하는 애플리케이션이 많아지면 유기적으로 구축한 자동화 테스트[각주:1]가 얼마나 중요한지 알 수 있다. 비슷한 수준의 버그 수정이나 기능 추가 요청이 있을 때, 자동화 테스트를 잘 갖춘 코드는 쉽게 반영할 수 있다. 그러나 자동화 테스트가 없는 코드에 대해서는 수정을 망설일 수밖에 없다. 유지보수 담당자가 간단한 기능 추가에 대해 신경전을 벌이는 이유는 천성이 게으르기 때문은 아니다. :)

직전에 언급한 자동화 개발자 테스트의 중요성을 간과할 수 있어 역시 IBM DeveloperWorks 칼럼의 주요 필자토비님김창준님이 글을 썼다고 이해할 수 있다.

한편, 케빈 님은 한발 더 나아가서 칼럼 저자의 의도를 유추하여 정정을 시도하고 있다.
  1. 회귀 테스트(Regression Test)라고 하기도 한다. [본문으로]

설정

트랙백

댓글

IBM DW 칼럼으로 올라온 글에 대해 일민형이 의견 개진을 요청했다. 글을 읽어보니 주장하는 바가 무언지 잘 모르겠다. 표면에 드러난 글만 보면, 기초적인 개념에서부터 문제가 있어 보인다. 글쓴이가 QA를 전문적으로 하는 분이라니 실수한 모양이다. 일민형의 글김창준 씨의 부연이 오류를 잘 설명하고 있지만, 좀 다른 각도로 몇 가지 덧붙여본다.

이 글을 보면 학부 소프트웨어 공학 서적에 흔히 나오는 V 모델에 대한 이해가 부족한 듯하다. 그림[각주:1]에서 보듯 단위 테스트는 특정 프로그램 단위의 설계 검증이 목적이고, 시스템 테스팅은 요구사항 명세에 대한 검증이 목적이다. 그런 둘을 같은 선상에 놓고 결함 검출률을 이야기하는 일이 무슨 의미가 있을까? 볼트와 너트가 서로 맞물리는지 확인하는 일과 자동차가 정상적으로 주행하는가? 중에 어떤 검사가 더 효과적인가?

개발자들이 일반적으로 생각하는 것과는 달리 소스 기반 단위 테스팅보다 소스를 고려하지 않는 블랙박스 개념의 시스템 테스팅의 결함 검출률이 높다


Flowchart of the V (U) Model for SDLC - Requirements, Specifications, Design High-Level, Unit Design Low-Level, Build Code, Unit Testing, Integration Testing, System Testing, Acceptance Testing

결함 검출에 대한 이상한 비교가 주장하고 싶은 바는 다음 내용으로 짐작할 수 있다. 이 사실을 알리려고 현재시점에서 도표까지 인용할 필요가 있을지 의문이다.

상기 표는 요구사항 분석시 발견되는 결함 수정에 비해 테스트 단계에서 결함 수정이 50배 많은 비용(시간)이 든다는 것을 보여준다. ... 중략 ... 코드를 개발하던 중에 한 시간을 소비하여 결함을 하나 찾아 수정했다면, 그것은 향후 유지보수를 위해 소비해야 했던 다섯 시간(다섯 배의 비용)을 절약했다고 할 수 있겠다.

인용문을 한 줄로 줄여도 큰 무리는 없다.

'결함을 빨리 발견할수록 고치는데 비용이 적게 든다.'

과연 필요성을 몰라서 그동안 요구 사항 분석 단계에서 결함을 좌시했을까? 인용문에서 의미하는 리뷰나 인스펙션 정도로 90%까지 찾을 수 있는 결함은 과연 무엇일까?

요구분석서와 설계 문서 리뷰를 통해 결함을 찾을 수 있다. ... 중략 ... Gilb와 Graham에 의하면 코드 인스펙션으로 무려 60%에서 90%까지 결함을 발견할 수 있으며...

외주개발이 보편적인 정보시스템 개발 분야에서 수년 전이 아닌 요즘에 리뷰/인스펙션의 중요성을 주장하는 경우는 거의 드물다. 리뷰/인스펙션이 효과적이기 위해서는 요구분석과 설계 문서가 완벽하다는 전제가 필요하다. 그런데 완벽한 요구분석서나 설계서를 작성하는 일이 얼마나 가능할까? 주류 방법론 변화를 살펴보면 산업계의 보편적인 답을 들을 수 있다.

최근 애자일 방법론 및 프랙티스는 제법 인기를 끌고 있다. Rational을 인수한 IBM은 한동안 주류였던 RUP 방법론에 애자일을 접목해서 Open UP를 만들어 공개했다. RUP에서 OpenUP로 변화하는 과정에서 가장 눈에 띄는 점은 매 반복 말에 아래 그림에서처럼 Demo-able or Shippable Build를 내놓는다는 점이다. 이는 반복마다 시행했던 공식적인 리뷰를 고객 데모나 실제 운영환경에 적용에 따르는 활동으로 대치함을 의미한다.


글로 기술하거나 화면 정의를 한다고 해서 고객이 의도한 바를 달성할지 어떻게 검증할 수 있을까? 소프트웨어 공학은 아직 비즈니스 환경이 제시하는 문맥 안에서 기능 검증을 할 수 있는 메커니즘 따위는 발견한 적이 없다. 또한, 업무 시스템은 현존하는 무엇이 아닌 아직 사용해본 일이 없는 새로운 무엇을 추구한다. 그런데 존재하지도 않는 시스템을 검증할 수 있는 Reviewer가 과연 존재할까?

OpenUP에서 이제 인스펙션이나 리뷰란 말은 찾아보기 어려워졌다. 궁극적으로 고객이 원하는 제품 검증(Validation)을 위해선 빠르고 잦은 출시/피드백이 필요하다. 개발절차를 변경해서라도 이를 가능하게 하는 것이 엄격한 리뷰/인스펙션보다 효과적이란 인식공유를 방법론에 반영한 것이다.

개발자들이 일반적으로 생각하는 것과는 달리 소스 기반 단위 테스팅보다 소스를 고려하지 않는 블랙박스 개념의 시스템 테스팅의 결함 검출률이 높다

반면 결함 검출률에 대한 저자의 주장이 무색하게도 OpenUP는 단위 테스팅의 지위를 대폭 격상했다. RUP에서는 단위 테스트(unit test)란 용어를 사용했으나, OpenUP에서는 개발자 테스트(development test)로 이름을 변경하고, 구현 주요 활동 5개 중 2개를 개발자 테스트에 할애했다. 다시 말해 TDD를 채용했다.


그렇다고 리뷰와 인스펙션이 중요하지 않다는 뜻은 아니다. 비용 대비 효과를 중요시하는 산업계에서 그간 시행착오를 통해 리뷰/인스펙션보다는 더 빠르게 그리고 자주 최종 사용자에게 전달하는 방법을 택했다. 이를 실현하려고 개발자 테스트를 엄밀하게 하는 방법이 가장 효과적이기 때문에 애자일이 RUP의 반대편에 있다고 인식하게 했던 주역인 TDD를 채용하여 개발 과정의 심장부에 개발자 테스트를 추가했다.
  1. 출처: http://www.tfhrc.gov/safety/pubs/04080/02.htm [본문으로]

설정

트랙백

댓글

테스트 스위트를 어떻게 정리할까 고민하고 있는데 관련 글이 올라왔다.

My Unified Theory of Bugs

이 그에서는 테스트를 세 가지로 분류했다. 실용적인 분류다. 내가 즉흥적으로 메모한 분류와도 일맥한다.

- 클래스 수준의 단위 테스트
- 리소스나 DB 값을 확인하기 위한 테스트
- 설정 정보를 확인하기 위한 테스트
- 성능 테스트

일찌기 Scott Ambler는 시계열까지 동원해서 훨씬 더 복잡하게 테스트를 정리한 바 있다.

Miško Hevery는 테스트 하기 좋은 코드(Testable code)가 갖춰야 하는 요건으로 다음과 같은 사항을 제시하면서 다른 글을 소개한다:
말미에 눈에 띄는 얘기를 한다.

I find that a lot of people claim that they write unit-tests, but upon closer inspection it is a mix of functional (wiring) and unit (logic) test. This happens becuase people wirte tests after code, and therefore the code is not testable.

읽고 보니 그렇다. 테스트를 하지 않고 머리속으로 테스트 하기 좋은 코드를 작성하거나, 작성한 뒤에 테스트를 하면서 수정하기 보다는 당연히 먼저 테스트를 할 때 테스트 하기 좋은 코드를 작성하기가 수월할 것이다. :)

2008.11.21 점심 시간 추가:
간혹 그런 경우가 있습니다. 거의 완성이 되어가는데 결과가 조금 이상합니다. 이리 바꿔보고 저리 바꿔보고 하다보니 어느샌가 잘 동작합니다. 저는 이런 경우, 코드에 끌려왔다라고 표현합니다. 하지만 TDD로 개발할 때는 자신이 통제하지 못하는 상황에서 앞으로 나아가기가 어렵습니다.

출처: 테스트 수준은 개발 수준을 드러낸다



설정

트랙백

댓글

public class SomeNamespaceTest extends TestCase {
   
    /**
     * 로거 설정
     */
    private final Logger logger = LoggerFactory.getLogger(SomeNamespace.class);
   
    private ApplicationContext context;

    protected void setUp() throws Exception {
        super.setUp();
        this.context = new FileSystemXmlApplicationContext(new String[] {"./config/runtime/service/service-sample.xml"});
    }
   
    public void testNamespace() throws Exception {
        ServiceDefinition service = (ServiceDefinition)context.getBean("sample");
       
        assertEquals("sample", service.getId());
        assertEquals("샘플 명", service.getName());
        assertEquals("test_01", service.getRef());
        assertEquals("샘플 서비스 설정에 대한 설명 입니다.", service.getDescription());
        assertEquals(false, service.getAvailable());
        assertEquals("aaa002", service.getNotAvailableMessageId());
       
        ServiceDefinition service1 = (ServiceDefinition)context.getBean("sample1");
       
        assertEquals("sample1", service1.getId());
        assertEquals("sample1", service1.getName());
        assertEquals("test_02", service1.getRef());
        assertEquals("샘플 서비스 설정에 대한 설명 입니다.", service1.getDescription());
        assertEquals(true, service1.getAvailable());
        assertEquals("bbb001", service1.getNotAvailableMessageId());
       
        ServiceGroupDefinition sg1 = (ServiceGroupDefinition)context.getBean("sample-group1");
       
        assertEquals("sample-group1", sg1.getId());
        assertEquals("샘플 그룹 1", sg1.getName());
        assertEquals("샘플 서비스 그룹1에 대한 설명 입니다.", sg1.getDescription());
       
        List sg1List = sg1.getTempServiceList();
       
        assertEquals("sample", sg1List.get(0));
        assertEquals("sample1", sg1List.get(1));
       
       
        PreProcessorDefinition pre = (PreProcessorDefinition)context.getBean("pre_processor_01");
       
        assertEquals("pre_processor_01", pre.getId());
        assertEquals("테스트 프리 프로세서 01", pre.getName());
        assertEquals("test_pre_01", pre.getRef());
        assertEquals("샘플 pre_processor 설정에 대한 설명 입니다.", pre.getDescription());
       
       
        PostProcessorDefinition post = (PostProcessorDefinition)context.getBean("post_processor_01");
       
        assertEquals("post_processor_01", post.getId());
        assertEquals("테스트 포스트 프로세서 01", post.getName());
        assertEquals("test_post_01", post.getRef());
        assertEquals("샘플 post_processor 설정에 대한 설명 입니다.", post.getDescription());
        assertEquals(true, post.isFailOnError());
       
        ProcessTemplateDefinition pt1 = (ProcessTemplateDefinition)context.getBean("process_template_01");
       
        assertEquals("process_template_01", pt1.getId());
        assertEquals("테스트 프로세스 템플릿", pt1.getName());
        assertEquals("샘플 프로세스 템플릿 설정에 대한 설명 입니다.", pt1.getDescription());
       
        List preList = pt1.getPreProcessorList();
        List postList = pt1.getPostProcessorList();
       
        assertEquals("pre_processor_01", preList.get(0));
        assertEquals("post_processor_01", postList.get(0));
    }

}

첫 번째 개선할 사항은 test 메소드를 수행할 때마다 매번 Application context를 로딩하지 않도록 캐싱을 이용하는 것입니다. 이프릴 공개 세미나에서 다룬 내용이죠. 저는 사정이 있어서 JUnit 3.8 라이브러리를 이용하겠습니다. 일반적인 상황이라면 JUnit 4를 쓰는 것이 좋겠죠. JUnit 4로 실행하시려면 스프링 레퍼런스 매뉴얼을 참조하시면 됩니다.

3.8 이용시는 AbstractDependencyInjectionSpringContextTests를 활용하여 DI까지 활용한 테스트가 가능합니다. 우선 다음과 같이 상속하는 클래스를 변경해주시고

public class HoneNamespaceTest extends AbstractDependencyInjectionSpringContextTests {
   
멤버 변수로 설정한 context 대신에 테스트 대상으로 사용할 객체를 선언해주세요.

    private ServiceDefinition sample;

그리고, DI를 위한 getter를 추가합니다.

    public void setSample(ServiceDefinition sample) {
        this.sample = sample;
    }

AbstractDependencyInjectionSpringContextTests는 Autowiring을 지원해주는데 저는 By Name 방식을 쓸 예정입니다. 디폴트는 AUTOWIRE_BY_TYPE인지라, 다음과 같이  변경합니다. 얼핏 생각하면 setUp()을 대신하는 hook인 onSetUp()에서 설정해야 할 것처럼 보이지만, 생성자에서 해야 합니다.

    public SomeNamespaceTest() {
        setAutowireMode(AUTOWIRE_BY_NAME);
    }

그 이유는 onSetUp()이 인젝션이 수행되는 prepareTestInstance() 다음에 수행되기 때문이죠.

이렇게 하면 테스트 수행 속도를 개선할 수 있습니다. 한가지 빠뜨렸군요. context 파일 로딩은 템플릿 메소드를 쓰도록 바꾸셔야죠.

    protected String getConfigPath() {
        return "service-sample.xml";
    }

쾌적한 테스트를 위해서 수행 속도 개선은 중요한 문제이긴 하지만 테스트의 본질은 아닙니다. 위의 테스트는 테스트 자체가 너무 길어서 냄새(Toooo long 냄새)가 납니다. ^^;

테스트의 목적을 분명히 하고, 향후 운영 시점에서 Regression Test가 가능하게 하려면 테스트 메소드의 가독성을 높이는 작업이 필요합니다. 흠 이건... 작업자에게 숙제로 남겨줘야겠군요. 테스트 코드에서 한 단락 단위로 테스트 메소드를 만들어주면 대강은 작업자의 의도와 드러맞겠죠.

설정

트랙백

댓글

Stickyminds Column Posted: What Project Managers Need To Know About Testing을 읽어보면 외국도 우리와 상황이 비슷한 모양이다. 아래 그림은 Manage It!: Your Guide to Modern, Pragmatic Project Management 저자 Johanna Rothman이 자기 책에서 발췌한 그림이다.


우측으로 향할수록 프로그램 구성 단위(Unit)에서 시스템으로 진화한다. 선 위에 있는 리뷰는 자동화가 불가능한 테스트이고, 선 아래는 자동화를 권장하는 영역이다.
Johanna Rothman은 PM이 하는 가장 중요한 일이 선 아래 테스트에 대해서 지속적으로 확인하는 것이라 한다. 그리고, 120% 공감하는 내용은 사람들은 마지막 날이 되어야 자신이 만든 것을 제출하는 경향이 있기 때문에[각주:1] 가능한 조기에 리뷰/테스트 할 수 있게 하는 것이 중요하다.

Manage It!: Your Guide to Modern, Pragmatic Project Management
Manage It!: Your Guide to Modern, Pragmatic Project Management by Johanna Rothman
  1. 방학숙제를 떠올리면, 이런 현상은 비단 개발팀에 국한된 일은 아니라 보편적인 현상임을 알 수 있다 [본문으로]

설정

트랙백

댓글

아래와 같은 지져분한 테스트 코드가 있었다.
    @Test
    public void requireSomePropsToJoinup() {
        Member member = getInsufficientMember();
        try{
            membership.join(member);
            fail();
        }catch (InsufficientInfoException e) {
        }
       
        try{
            member.setEmail("ahnyounghoe@gmail.com");
            membership.join(member);
            fail();
        }catch (InsufficientInfoException e) {
        }

       
        try{
            member.setPassword("blahblah");
            membership.join(member);
            fail();
        }catch (InsufficientInfoException e) {
        }
       
        member.setName("안영회");
        membership.join(member);

    }

연속적인 try - catch가 보기 싫고, 중복이 많다.

    @Test(expected = InsufficientInfoException.class)
    public void nameIsReuired() {
        membership.join(memberWithout("name"));
    }

    @Test(expected = InsufficientInfoException.class)
    public void emailIsReuired() {
        membership.join(memberWithout("email"));
    }

    @Test(expected = InsufficientInfoException.class)
    public void passwordIsReuired() {
        membership.join(memberWithout("password"));
    }

    @Test
    public void blogIsNotRequired() {
        membership.join(memberWithout("blog"));
    }

    private Member memberWithout(String property) {
        Member member = new Member();
        if (! "blog".equals(property))    member.setBlog("http://younghoe.info");
        if (! "email".equals(property)) member.setEmail("ahnyounghoe@gmail.com");
        if (! "name".equals(property)) member.setName("안영회");
        if (! "password".equals(property)) member.setPassword("123456");
        return member;
    }

try - catch를 모두 제거했다.
memberWithout 메서드를 인라인으로 쓸 수 있어서 간결하고 가독성도 뛰어나다.

설정

트랙백

댓글

테스트 예찬

2007 2007/08/07 00:58
테스트가 생활이 되었다면 굳이 예찬할 필요가 없다. 나는 공개적으로 테스트를 예찬해서라도 스스로를 테스트를 피할 수 없는 궁지(?)로 몰아넣고 싶다. ^^

테스트는 코드의 품질을 높여주는 것에만 국한하지는 않는다. Testing Will Challenge Your Conventions라는 제목에 드러나듯 테스트는 테스트를 수행하는 개발자에게 훌륭한 습관을 배양시키기도 한다.

지난 스프링 사용자 모임에서 인터페이스가 반드시 필요하냐는 질문이 있었다. Testing Will Challenge Your Conventions에서 인터페이스가 테스트 전용 객체(Test Double)를 적용하는데 매우 유익함을 설명하고 있다. 구체 클래스를 목킹하는 것을 도와주는 라이브러리가 있기는 하다. 그렇다고 하더라도 인터페이스와 구현체를 분할하여 관점을 나누는 훈련은 파일이 두 배가 되는 번거로움을 충분히 감수할 만큼 개발자에게 유익하다.

Private makes less sense than it used to. 라는 3번 항목이 가장 눈에 띄었다. 실제로 TDD를 연습할 때 로직을 담고 있는 내부의 private 메소드를 테스트하는 것은 매우 어려웠다. 자바 기초를 배울 때 지나치게 강조 되었던 private의 역할에 대해 요즘은 왠지 속았다는 느낌이 든다. 내부의 주요 상태를 포함하는 것이 아니고, 유틸리티 성격의 함수라면 public 메소드로 정의하는 것이 나을지 모른다. 인터페이스에는 포함되지 않으면서 구현 클래스에만 들어 있는 것들은 DI(Dependency Injection) 환경에서는 private 메소드와 유사하게 볼 수도 있다.

하지만, Jaime Metcher의 반론처럼 모든 메소드를 public으로 만들어서 외부(TestCase)에서 테스트하게 할 필요는 없다. public 메소드의 일환으로 테스트 된 코드가 리팩토링 되어 private가 되었다면 다시 테스트 할 필요는 없는 일이다.

Testing Will Challenge Your Conventions에 소개된 12가지 프랙티스는 3번을 제외하고는 널리 알려진 것이다. 생소한 내용이라면 찬찬히 읽어보기를 권한다.

설정

트랙백

댓글

테스터는 프로젝트의 전조등이다.
테스터는 프로젝트의 앞길을 환하게 비춰주어야 한다.
프로그래머와 관리자들이 지도를 놓고 어디로 가야하는지에 대해 언쟁을 벌이더라도 꿋꿋하게 앞길을 비춰주면서, 적어도 현재 위치가 어디인지, 어느 곳을 지나가고 있는지, ... 알려줘야 한다.
테스트는 분쟁의 소지를 줄여줄 수 있다. 정성적인 생각을 성공/실패로 정량화 해준다. 테스트 작성으로 문제가 해결되지는 않지만 신뢰할만한 지표 역할을 해줄 수 있다.

테스터가 실패에 초점을 맞추면 고객은 성공에 초점을 맞출 수 있다.
창의성과 기술력을 모두 동원하여 제품 내에 숨어있는 중요한 문제점을 찾아내야 한다.
제품에서 문제가 있을 곳을 찾아냄으로써 프로젝트팀이 기술과 제품의 위험요소들을 더 많이 알 수 있도록 도와준다.
성공과 실패를 바라보는 관점(vision)의 무게가 평형/조화를 이뤄야 하는 것이 아닌가 생각된다.

테스트를 통해서 품질을 보장할 수는 없다.
회사 내에서 테스트팀을 "품질 보장팀"으로 부르고 있는지도 모르겠다. ... 프로젝트의 품질을 용이하게 보장할 수 있도록 도와주는 정보를 제공할 뿐이며, 프로젝트의 품질은 전체 팀원들의 노력으로부터 나오는 것이기 때문이다.
분업의 최대 취약점은 분리에서 오는 분열이다.

문지기가 되지는 마라!
지금까지 보아온 프로젝트 중에서 가장 효율적이었던 경우는 만장일치로 제품 출시를 결정하는 것이었다. 테스터에게 출시에 대한 결정 권한이 주어졌다면 그 즉시 팀에서 다른 역할을 맡고 있는 사람이나 관계자와 함께 그 권한을 공유해야 한다는 입장을 밝히는 것이 좋다.
관리가 아닌 협업

프로세스 개선 그룹이 되는 것을 조심하라.
문제를 발견하는 것에 지쳐서 문제를 예방하기 위한 더 나은 방법은 없는지 생각해볼 수도 있다. ... 다른 팀원들이 한순간에 테스터의 노력을 헛수고로 만들어서 무능한 것처럼 보이게 되는 경우도 많이 있다. ... 테스트팀을 프로세스 평가 모임 같은 것으로 보이게 하지 않도록 주의해야 한다.

테스트를 잘 수행하기 위하여 필요한 항목들을 모든 사람들이 이해할 것이라고 기대하지 마라.
여러분이 이 책을 읽고 있다고 해서 다른 사람들도 이 책을 읽을 것이라고 기대하지는 마라.

블랙박스 테스트는 알지 못함을 기반으로 테스트를 실행하는 방법이다.

테스터는 여행객 이상이다./모든 테스트는 질문에 대한 대답을 찾는 과정이다.
어떤 문제가 있을 때 문제의 종류를 식별해내는 어떤 원칙이나 프로세스를 적용하지 않는다면 제품을 다뤄보는 행동을 테스트라고 말할 수 없다. 테스트를 진행할 때, 평가 전략을 이끌어가고 있는 의문이 무엇인지를 스스로에게 물어보라. 이러한 의문을 가지지 않는다면 테스터라기 보다는 관광객일 뿐이다.

모든 테스트는 모델을 기반으로 수행된다.
문제가 복잡해진 경우, 관점의 분화(Separation of Concerns)가 분열로 이어지는 것을 막기 위해서 모델이 필요하다. 모델은 각각의 관점에 대한 액기스만을 취합하여 이들 사이의 연계를 표현할 수 있어야 한다.

소프트웨어 테스팅 법칙 293가지
Cem Kaner 지음,
이주호 옮김/정보문화사

설정

트랙백

댓글

테스트 코드를 짜다 보면 많은 중복이 발생한다.
더구나 DB 레코드 값과 객체의 프로퍼티를 비교하려면
테스트가 테스트 데이터에 의존하게 된다.

Least privilege 원칙은 언제나 유효하다.
내가 C++을 배울 때 가장 중요하게 생각했던 프로그래밍 원칙이다.
근래에도 Refactoring to Patterns를 보면 Kent Back의 Singleton에 대한 견해를 다루면서
유사한 내용을 담고 있다.

이 원칙에 근거하면 개별 테스트는 고유한 테스트 데이터를 써야 한다.
그렇다면 모든 테스트마다 수십줄이 넘는 테스트 데이터 생성 코드를 작성해야 할까?
원칙적으로는 그렇다.
런타임에는 이 원칙을 지켜야 한다. 이것은 테스트의 기본 품질에 관한 문제다.
하지만, 런타임과 프로그래밍 시점 즉, 정의 시점은 다르다.

런타임에 서로 연관성이 전혀 없게 해준다면 굳이 개별적으로 테스트 데이터를 생성할 필요가 없다.
Spring의 AbstractTransactionalDataSourceSpringContextTests를 활용하면 구현이 쉽다.

code


insertTestNews() 메소드는 다른 곳에서도 사용이 가능하다.

code


이렇게 테스트 메소드의 중복도 제거해나가다 보면...

TDD에 익숙하지 않는 (나와 같은) 상당수의 개발자는 다음과 같은 의문을 갖게 될 수 있다.

'테스트 코드를 이렇게 까지 해야해?'
'테스트인데 중복이 좀 있으면 어때?'

나는 몇 시간 이후에 단위 테스트를 하지 않았던,
현재 6개월째 디버깅중인(아마 앞으로도 계속 될 듯) 모 사이트에 가야한다.

테스트는 요구사항을 분명하게 해준다
. 데이터 항목은 무엇이고, 일 처리 순서와 작업의 연계에 대한 것들을 꼼꼼하게 작성하게 된다. 요구사항을 테스트로 변환하다 보면 구현 레벨 언저리까지 맞추게 되는 것이다.

어떤 면에서는 테스트 코드를 실제 코드만큼이나 신경써서 작성해야 할런지도 모른다는 생각이 스쳐간다.

테스트 유틸리티 메소드를 작성하다 보면 아래와 같이 길어지고 여러번의 호출 관계로 복잡해진다.
하지만, 이렇게 하다가 Mock이 아닌 실제 데이터를 테스트할 필요가 있는 경우(SQL 이용시 등)에 유틸리티를 만들 수 있다. (하다 보면 특정 부분은 DbUnit의 모양새와 유사해질수도 있지 않을까 하는 우려도 들지만)

또 한가지 중요한 것은 테스트 데이터 생성이나 Assertion을 돕는 유틸리티성 코드 조차도 팀내 공유가 필요하다는 점이다. 단순히 '이거 만들었으니 써라'라는 식이 아니라, 동료들이 납득하도록 대화가 필요하고, 소화할 시간이 요구된다.

more..


설정

트랙백

댓글

아래의 내용은 2004/11/01 작성했던 글이다. 벌써 2년 가까운 시간이 흘렀다.
J2EE 애플리케이션 개발의 어려움

J2EE 시장이 커질 수 있었던 주요 이유중 하나는 인터페이스의 장점을 제품수준에서 구현한 J2EE 솔루션들의 Specification이 주는 이점이었다. 다시 말해서, 복잡한 하부 기능들을 특정 J2EE 제품들이 제공해주고, 애플리케이션 개발자는 단지 Specification의 핵심 요소라고 할 수 있는 API만 알면 된다. 이점은 장점이면서, 동시에 단점이기도 하다. 모든 것이 그렇듯이 빛이 있으면 그림자가 존재하기 마련이다.

일단, EJB를 쓰는 경우는 말할 것도 없고, 서블릿 기술만 사용하더라도 테스트가 곤란해진다. 테스트라는 것은 개발자가 어떤 코드를 작성했을때, 결과가 어떻게 나타났는지를 봄으로써 프로그램이 의도대로 이루어지는지를 확인하는 것이다. 그러나, J2EE 개발을 하게되면 개발자가 프로그램의 모든 부분을 작성하지 않기 때문에 정확한 결과를 장담하기는 힘들다.

서블릿이나 EJB는 컨테이너에서 구동해야만 그 결과를 확인할 수 있는데, 작은 부분 하나를 수정할 때마다 컨테이너에서 결과를 확인하는 일은 번거로운 일이다. 하지만, 그건 약과다. 정작 큰 문제는 Specification에 명시된대로 작업했는데, 의도하지 않을 일이 발생할 때, 소위 말하는 trouble-shooting에 있다. 우리말로는 다소 미진하긴 하지만 문제 해결이라고 번역할 수 있으려나

이러한 문제 해결을 위해서는 일단 스펙(Specification을 준말)을 꼼꼼히 봐야하는게 일반적인 상식이라고 할 수 있다. 그러나, 스펙의 분양은 상당히 방대하다. 또, 대부분의 기술문서가 그렇든 영어다.
그 다음으로 해결할 수 있는 방법은 무얼까? 사용 제품이라면 제품을 판매한 업체에 의뢰를 할 수 있다. 대개 기술지원을 나오는 사람들도 자신이 경험한 것들에 대해서는 쉽게 처리해주지만 처녀(?) 문제에 대해서는 머리를 싸매기 마련이다.

요즘 들어 컴퓨터 하드웨어 업체들은 사양을 복잡하게 만들어서 판로를 확보한다. 컨테이너 제조사도 비슷하다. 대개 많은 기능을 탑재하기 위해서 스펙에 명시되지 않은 것들은 넣거나, 스펙과 다른 방법을 이용하여 효율적인 처리를 도모한다고 하는데... 이런 것들로 인해 개발자는 스펙 말고도 봐야할 제조사 매뉴얼이 생길 수 있다. @@;

하여간 이런 일들을 해소하기 위해서 해외에서는 경량 컨테이너가 붐을 일으키고 있는 것 같기도 하다. 경량 컨테이너의 하나인 Spring을 사용하면서 의외의 문제를 만났는데, 돌연 JSP EL(Expression Lanaguage)을 파싱하지 못하는 것이었다.

나는 순서대로 스텝을 밟았다. 우선 J2EE 튜토리얼과 JSTL 관련 참고 서적을 이용하여 EL에 관련된 사항을 내가 어겼는지 확인하고, 전에 발생했던 서블릿 2.3에서의 isELIgnored 속성 문제를 점검했다. 나는 서블릿 2.4로 선언했기 때문에 해당되지 않았지만, 지푸라기 잡는 심정으로...ㅡㅡ;

그 다음에는 스프링의 문제인지 확인했다. 공식 문서인 API나 RM(참조 매뉴얼)에는 해당 내용이 없었고, 포럼을 뒤져서 비슷한 문제를 찾아보았지만, 전혀 문제가 없다고만...

다음으로... JSP 파싱 결과를 확인했다. 컨테이너의 임시 작업 디렉토리에 TLD 파일이 없는 것을 확인했다. JSTL을 사용한 다른 WAR의 임시 작업 디렉토리에는 TLD가 위치한다. 쩝... 문제는 이건데 이유는 알 수 없다. 이유를 알려면 컨테이너의 작동 방식을 알아야 한다.

컨테이너의 의존을 최소화하려고 경량 컨테이너를 쓴다지만, 경량 컨테이너마저도 종국에는 서블릿 컨테이너를 써야 한다. 게다가 경량 컨테이너의 작동 방식을 완전하게 이해하지 못한다면, 문제 발생시 경량 컨테이너 문제인지, 서블릿 컨테이너 문제인지 아니면 내가 작성한 코드의 문제인지 알 수 없다.

많은 경우에 그렇듯이, 나는 또 이유는 모르고 문제만 해결했다. 각종 문서 확인, 코드 확인, ANT 구문 확인, 컨테이너 작업 디렉토리 등을 모두 다 확인하고 별짓(?)을 다한 후에 몇일이 걸려서 알아낸 사실은 서블릿 2.3으로 선언해야 한다는 것이었다.

자바는 참 좋은 언어지만, J2EE로 이동하면 깝깝한 세상이 펼쳐진다. 이유를 모르고 무언가를 해낸다는 것은 꺼림직한 일이다.

프레임워크의 사용일 늘어갈수록 많은 부분은 블랙 박스(black-box) 상태로 이해하고 개발을 하게 된다. 더구나 점차 대규모 단위 시스템 보다는 다수의 시스템 연동이 강조되는 이른바 SOA가 강화될수록 이러한 경향을 더욱 강화될 것이다.

그렇기 때문에 단위 테스트부터 꼼꼼하게 해야 한다. 신뢰가 확보되지 않은 클래스와 컴포넌트를 Loosely Coupled 형태로 엮는 다는 것은 갈등의 소지만 키우는 일이 된다.

설정

트랙백

댓글

Expert Spring MVC and Web Flow (Expert)에 소개된 springmodules의 valang을 활용하면 Validator를 자바 코딩 없이 선언만으로 구현할 수 있다.(XML 설정만으로 가능하다)

복잡한 자체적인 문법을 갖고 있어서 테스트 필요성이 높다. Expert Spring MVC and Web Flow (Expert)의 p281에는 테스트 예제가 있다.
public class PersonValidatorTests extends TestCase {
public void testEmptyPersonValidation() {
Person person = new Person();
Validator validator = new PersonValidator();
BindException errors = new BindException(person, "target");
validator.validate(person, errors);
new ErrorsVerifier(errors) {
{
forProperty("firstName").hasErrorCode("person.firstName.required")
.forProperty("lastName").hasErrorCode("person.lastName.required")
.otherwise().noErrors();
}
}
}
}

ErrorsVerifier를 헬퍼(Helper class)로 사용하여 간결하게 작성했지만 문제가 있다. ErrorsVerifier 클래스는 spring은 물론 spring-modules에도 존재하지 않는다는 점이다. 구글을 통해 Spring 포럼에 올라온 글, 'Where is ErrorsVerifier class mentioned in Expert Spring MVC?'을 확인할 수 있었다.

하지만, spring-modules의 테스트 및 소스 코드를 조금 참조하면 어렵지 않게 테스트를 작성하 수 있다.
public class MemberValidationTest extends TestCase {
private Member member;
private ValangValidator validator;
BindException errors;
@Override
protected void setUp() throws Exception {
member = new Member();
errors = new BindException(member, "member");
validator = new ValangValidator();
}
public void testPasswordValidation() throws Exception{
validator.setValang(
   "{ password : ? is not blank : '' : 'member.password.empty' }"
);
validator.afterPropertiesSet(); // configure validation rules
validator.validate(member, errors);

assertEquals("member.password.empty", errors.getFieldError("password ").getCode());
}
}

spring-modules의 테스트는 spring의 컨텍스트 파일(xml 형식의) 설정을 통해 테스트를 한다. 그러나, 단위 테스트를 목적으로 한다면 외부 리소스에 의존하는 일은 가능한 제거하는 것이 좋다고 생각하여 위와 같이 직접 setValang()을 호출한 것이다. setValang()은 실제로는 다음과 같은 식으로 설정해야 한다.
<bean id="easyAddressValidator" class="org.springmodules.validation.ValangValidator ">
  <property name="valang">
  <value><![CDATA[{ password : ? is not blank : '' : 'member.password.empty' }]]></value>
  </property>
</bean>

spring-module의 의존성에 따라 테스트를 실행하려면, 다음 라이브러리가 필요하다.
  • commons-collections.jar

assertion을 위한 코드가 늘어서는 것도 가독성을 크게 저해하지는 않는다. 책에서의 의도와 비슷하게 Fluent Interface 스타일을 적용해서 조금 수정해봤다.

public void testPasswordValidation() throws Exception{
validator.setValang(
   "{ password : ? is not blank : '' : 'member.password.empty' }"
);
validator.afterPropertiesSet(); // configure validation rules
validator.validate(member, errors);
new ErrorsVerifier(errors)
  .forProperty("password").hasErrorCode("member.password.empty");
}
class ErrorsVerifier{
private Errors errors;
private String cursor; // one of successive properties

public ErrorsVerifier(BindException errors) {
  this.errors = errors;
}
public ErrorsVerifier forProperty(String property) {
  cursor = property;
  return this;
}
public ErrorsVerifier hasErrorCode(String errorCode) {
  assertEquals(errorCode, errors.getFieldError(cursor).getCode());
  return this;
}

}

아쉬운 것은 정작 감춰야 할 부분인 아래 구문이 그대로 노출된다는 점이다:
validator.afterPropertiesSet();

valang 활용팁 더 보기


valang 함수 만들기

설정

트랙백

댓글

Toby님 추천으로 보게 된 JUnit Recipes: Practical Methods for Programmer Testing 6페이지에는 객체를 테스트 할 때 드러나는 흐름을 이렇게 정리하고 있다:

1 Create an object.
2 Invoke a method.
3 Check the result.

단위 테스트를 해본 사람이라면 별로 새로운 이야기는 아니다. 아마도 다수는 머리 속에 이런 개념을 갖고 있을 것이고, 일부는 주석에 이러한 내용을 기입할 수도 있다.

책 내용을 조금 더 보면, Refactoring Workbook에서는 위의 흐름을 좀 더 운치있게 표현했음을 소개한다:
“arrange, act, assert.”

멋진 표현이다. 하지만, 컴퓨터 개론인가에서 배웠던 IPO(Input-Process-Output)도 아쉬운대로(?) 셋을 설명할 수 있을 것 같다.

표현이야 어떻든, 중요한 점은 공유다. 최소한 팀내 공유. 그보다 더 작게는 오늘의 나와 미래의 나 사이의 공유.(잊어버리지 않을 표현) 언어나 언어 표현은 널리 쓰일 때 유용성을 논할 수 있을 테니까.

이러한 틀을 팀내에서 자연스럽게 퍼지도록 혹은 쉽게 학습하도록 할 수 있는 방법은 뭐가 있을까?

먼저, 주석을 달 수 있다. 가장 분명한 방법이지만 엄청난 주석의 중복을 낳는다. 주석도 코드라는 관점에서 보면 좋지 않은 방법이다.

둘째로 단락화 할 수 있다. 마치 html의 <p>태그를 쓰듯이 테스트 메소드의 내용을 묶어 놓는 것이다. 단점이라면 이미 세 가지 유형의 구문에 익숙한 사람은 단락화 하는 것이 중요하지 않게 느껴지기에 소홀해 질 수 있다는 것이다.

셋째로는 테스트 메소드를 셋으로 분할하여 루틴화 하는 것이다. 단점은 이클립스와 같은 IDE를 이용하면 코드 사이의 이동이 어렵지는 않지만 코드 사이의 이동에 불편을 초래하고, 전반적으로 가독성에 악영향을 줄 수도 있다. 게다가 지나치게 문제를 어렵게 만드는 느낌이다.

일단 두번째 방법이 가장 마음에 든다. 다만, 단락을 보다 명확하게 나누기 위해서 공백으로 남겨두는 행의 수에 초점을 두기 보다는 작명을 잘 하는 것이 좋을 것같다. 앞서 소개한 어휘도 좋고, 팀내에서 공유할 만한 용어를 정해서 가능한 메소드나 변수 이름을 해당 용어가 연상되도록 작명할 수 있다.

설정

트랙백

댓글

오랫동안 Design by Contract (DbC) 지지자와 Test Driven Development (TDD) 지지자 사이에서 논쟁이 있어왔다. (역자 주:DbC 는 설계를 먼저해서 요건을 구체화한 Contract 혹은 Interface 중심으로 개발하는 것으로 이해할 수 있구요. TDD 는 테스트로 요건을 정의하는 방식이죠. 즉, 테스트를 성공하면 요건을 만족했다고 보는 방식이죠.) 여기서 그 이야기를 하려는 의도는 없고,  Daniel Jackson 과 얘기했던 착상 즉, 이들 둘을 합쳐보는 것에 대해 언급해보겠다.

Design by Contract 을 따르면(설계를 먼저 하게 되면), 개별 클래스에 대해서 변하지 않는 부분을 정하게 됩니다. 이렇게 정의된 것들은 클래스가 항상 지켜야 할 속성들을 의미합니다. 이들 클래스의 인스턴스인 객체들은 이들 Invariant(혹은 contract)를 항상 충족해야 합니다. (어떤 일을 하는 과정에서 내부적으로 잠시 지키지 않을 수야 있지만)  프로그래밍 언어, Eiffel 을 쓰면 클래스의 invariant 를 메소드 호출 이전(선조건 점검)이나 이후(후조건 점검)에 자동으로 점검할 수 있습니다. Invariant 를 충족하지 않으면 예외를 발생시키죠. (실제 프로그램 사용시점에서는 성능을 고려해서 이러한 기능을 중지시켜 둘 수 있습니다.)

이러한 착상을 TDD 에 적용해보면 클래스의 Invariant 를 테스트하는 공통 메소드를 정의할 수 있습니다. 간단한 예제 코드를 보자.
public class Bowler ...int overs, runs, wickets;

Bowler 클래스의 invariant 는 위 세 개의 변수값이 자연수가 되야 한다는 것이다. 따라서, 다음과 같이 invariant 를 정의할 수 있다.
public boolean passesInvariant() {
return (runs >= 0 && overs >= 0 && wickets >= 0);
}

그리고 나면 테스트 과정에서 셋업 이후와 객체의 활용 이후에 호출할 수 있게 된다.
public void testConcedingRunsAddsToRunsScore() {

  Bowler botham = new Bowler();       // setup - showing my age
  assert botham.passesInvariant();
  botham.concedeRuns(4);              //exercise
  assert botham.passesInvariant();
  assertEquals(4, botham.getRuns());  //verify

}

흥미로운 생각이긴 하지만, 직접 시도해보거나 시도하는 것을 본 것은 아닙니다.

설정

트랙백

댓글

Gerard Meszaros는 다양한 Xunit 프레임워크를 사용하는 패턴을 다룬 책을 쓰고 있다. 집필과정에서 그간 만난 고충 가운데 하나는 사람들이 테스트를 위해 시스템의 일부를 대신하는 것을 Stub, Mock, Dummy, Fake 등과 같이 다양한 이름으로 부른다는 것이다. 이런 문제를 그는 자신의 용어를 사용해서 해결하려고 하는데 다른 사람들에게도 알릴 만한 가치가 있는 것 같다.

그가 사용하는 범용적인 용어는 Test Double(우리가 흔히 스턴트맨이라 하는 대역을 stunt double 이라 함)이다. Test Double 은 테스트를 위해 실제 객체를 대신하는 모든 상황에서 쓰이는 용어다. Gerard 가 열거한 Test Double의 유형은 다음과 같다:


  • Dummy 객체는 객체의 전달에만 사용되고 실제로 이를 사용하지 않는 것이다. 대개 매개변수 목록을 채우는데 쓰인다.
  • Fake 객체는 실제로 동작하도록 구현되지만, 보통 빠른 구현을 위해 실제환경과는 조금 다르게 구현할 수 있다. 실제 사용할 RDBMS 대신 InMemoryDatabase 를 사용하는 것이 적절한 예다.
  • Stubs 은 테스트를 위해 미리 준비한 응답만을 제공하는 것이다. 그외의 상황에 대해서는 정상적으로 작동하지 못하는 것이 일반적이다. 또한, Stubs 은 호출에 대한 정보를 기록하는 경우도 있다. 예를 들어, 이메일 게이트웨이 스텁의 경우 전송 메시지나 전송된 메시지의 개수를 기록하는데 쓰이기도 한다.
  • Mocks 은 스펙을 통해 정의된 응답을 받게 되어 있고, 다른 응답을 받게 되면 예외를 발생하도록 구현되어서, 응답에 대한 확인 기능을 수행하는 역할을 한다.

원문: TestDouble

파울러의 글은 아니지만, 좋은 글에서 발췌한 좋은 요약이 있어 옮겨둡니다.

  • The real object has nondeterministic behavior
  • The real object is difficult to setup
  • The real object has behavior that is hard to trigger
  • The real object is slow
  • The real object is a user interface
  • The real object uses a call back
  • The real object does not yet exist

설정

트랙백

댓글

Expert Spring MVC and Web Flow (Expert) 10장에 소개된 내용을 보면, Mock Object 라이브러리 세 가지를 비교하고 있다.

  • MockObjects 원조격이지만 문서가 적은 것이 흠
  • jMock MockObjects 개선안으로 튜토리얼이 다수 존재한다고. 위의 책과 AppFuse에서 사용
  • EasyMock많은 예제가 있고, Spring에서 사용

MockObjects는 현재 홈페이지도 연결이 되지 않고, SourceForge의 최근 뉴스가 2003년 5월인 것을 보면 더 이상 개발이 되지 않는 것으로 보인다. 아마도 jMock이 이들의 차기 (자바) 버전으로 추정된다.

Spring에서 사용한다는 이유만으로도 EasyMock이 가장 끌리지만 합리적인 판단은 아니다. :)

인터넷을 조금 찾아보니 비교한 글이 있다. 섬나라님의 글을 보니 가장 큰 차이는 프로그래밍 인터페이스였다. 아래 코드가 EasyMock의 방식이고
mockFoo.bar(arg);
ctrl.setReturnValue(1);
ctrl.replay();

아래가 jMock의 방식이다.

mock.expects(once()).method("bar").with(same(arg)).will(returnValue(1));

가독성(readability) 관점에서 평가는 상반될 수 있는 극명한 차이를 보인다. 마치 초보자도 쉽게 사용할 수 있는 단순하고 직관적인 UI(User Interface)와 한번에 모든 것을 처리할 수 있는 단축키가 풍부한 UI 중에 무엇이 좋으냐를 고르는 것 같은 기분이다. :)

인터페이스를 척도로 놓고 보면 지금 시점에서 나와 우리 개발팀에겐 직관적인 EasyMock이 알맞은 듯 하다.

해외 개발자 사이트 중에선 구체적인 비교표를 올려놓은 곳도 있다. 여기선 기능적으로 판단할 수 있는 좋은 요소들을 요약해두어 매우 유용하다.

  1. 테스트 메소드의 식별: 실제 메소드인 경우만 IDE의 코드 자동완성(code completion)과 리팩토링의 도움을 받을 수 있다. jMock의 String으로 메소드를 지정하는 점은 불리해보인다.
  2. EasyMock은 특수한 테스트 케이스 클래스를 상속할 필요가 없다. jMock은 MockObjectTestCase라는 것을 상속해야 한다. 이것은 EasyMock이 별도의 MockClassControl 혹은 ClassControl을 필요로 한다는 것과 관계가 있어 보인다. 테스터의 이면에서 Transparency를 제공할 때 사용하는 메커니즘이 무엇이고, 테스터 입장에서 치루는 비용이 무엇이냐에 대한 선택의 문제로 보인다. 상속보단 MockClassControl/ClassControl를 사용하는 것이 테스트케이스의 확장성 측면에서 유리할 것이다.
  3. Mock 객체의 활성화를 위해서 EasyMock은 control.replay()를 호출해야 하는 번거로움을 요구한다. API 전체의 복잡성과 함께 고려해보면 결국 인터페이스 스타일의 문제로 보인다. 절차가 두드러져 보이는 API를 선호하느냐/테스트에 충실한 어휘를 사용하여 생산성있게 표현하느냐?

3번은 앞서의 스타일 문제로 보면, 기능적인 면에서도 EasyMock가 앞선 것 같다.


설정

트랙백

댓글