티스토리 툴바

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


설정

트랙백

댓글


TDD is like dual entry bookkeeping for accountants, ...

TDD를 복식 부기[각주:1]에 비유하다니 절묘한 은유다. 동시에 외과 의사의 어떤 절차(sterile procedure for surgeons)[각주:2]를 함께 예로 들면서, 전문가들이 이러한 규율이 주는 이점을 알고 활용하듯 TDD 역시 전문적인 규율(TDD is a professional discipline)라 설명한다.

소프트웨어 산업은 아직 초창기인 듯하다. 최소한의 검증 장치가 없는 코드를 인정해주는 문화가 증거다. 현장에서 아직 TDD를 적용해보지 못했지만, JUnit 기반 자동화 테스트 구축으로 변화에 대응하기 위한 회귀 테스트를 5년에 걸쳐 4개 프로젝트에 적용하고 있다. 변화를 강제 당하는 개발자가 테스트 작성을 반대하는 목소리는 이해할 수 있다. 그러나 관리자마저 정확한 내용도 모르고 그저 개발자에게 부담을 준다고 테스트 작성을 반대하는 일을 최근에도 경험할 수 있다. 조금 과장해서 이야기하면, 개발자가 자기 마음대로 코드를 짜도 화면으로 결함을 확인할 수 없으면 문제없다는 이야기다.(세상에~ 내가 일하는 곳이 이렇게 엉성하게 일할 수 있는 곳이었다니... 새삼스럽다.) Uncle Bob이 TDD가 개발을 더디게 한다는 사람을 석기 시대(the stone age) 사람이라 했는데, 소프트웨어 산업은 이제 막 석기 시대를 지난 듯하다. :)




  1. 트위터에 링크를 소개했더니 복식부기를 쉽게 알려주는 만화를 공유한 분이 있다: http://ow.ly/t4qk [본문으로]
  2. 무엇을 말하는지 모르겠다. [본문으로]
TDD

설정

트랙백

댓글

일민형이 대답을 요구하는 글을 썼다. 알면서 왜 안할까? TDD

대답은 명쾌하다. 의지가 부족할 뿐.

나는 의지가 투철한 사람이 아니다. 초중고는 물론이고 대학교는 물론 회사에서까지 끊임없이 지각을 하는 행태를 고쳐야지 마음먹어도 이렇게도 오랬동안 고치지 못하는 것이 이를 입증한다. @@

그런데도 왜 자꾸 포스팅을 하고 난리냐? 부족한 의지를 채우는 나름의 방식이다. 좋은 글을 통해 만날 수 있는 동시대인들의 동경하는 모습은 부족한 의지를 극복하는데 도움을 준다. 그리고, 그때 생겨나는 열정이 식어버리기 전에 흔적을 남기는 수법이 아닐까 싶다. [각주:1]

한 가지 확실한 것은 적어도 나에게 있어 TDD는 일단 아침에 일찍 일어나는 것보다 쉽다. 그래서, 그리 멀지 않아서 'TDD를 하자'고 말할 필요도 없는 상태가 오지 않을까 기대하고 있다. 그때 되면 또 딴거 하자고 (스스로를 그리고 나와 같은 생각을 하는 이들을) 선동(?)하고 나서겠지만...
  1. 왜 내가 이러는지 즉흥적으로 분석해 본 결과 [본문으로]
TDD

설정

트랙백

댓글

영어 안들려도 눈으로만으로도 쉽게 TDD 기초를 익힐 수 있는 동영상



출처: Video tutorial: Test Driven Development in practice
TDD

설정

트랙백

댓글

당신이 생각하는 TDD에서 나눈 대화 후에 내가 왜 TDD에 관심을 가졌던가 떠올랐다. 생각해보면 TDD라는 말을 사용하여 몇차례 강의까지 했음에도 불구하고, 아무도 TDD가 무어냐고 나에게 묻지 않았던 것 같다. 내가 TDD를 처음 찾은 것은 UP/RUP 혹은 CBD 방법론을 써서 UML 모델링을 할 때의 취약성 때문이었다. 설계 언어로써 UML은 한계가 많다. 결국, 10여년이 지났어도 UML은 설계서를 Unified 하지 못한 것 같다. 유용하게 쓰이는 것은 높은 추상화 수준에서 클래스 다이어그램이나 객체가 스무개를 넘지 않는 작은 시퀀스 다이그램정도다.

내가 과거에 답답했던 것은 가이드에 의해 만들어내는 UML설계모델의 수많은 다이어그램들이 별로 유기적으로 코드로 이어지지 못하고 있다는 점이다. 내가 만들어야 했던 것은 요구사항부터 구현으로 이어지는 전체 표준 프로세스였다. 결국 유스케이스에서 테스트케이스를 만들게 하고, 이 때 설계한 내용을 구현에 반영하게 했다. 그때, 해결하지 못했던 것은 요구사항에 대응하는 Acceptance test수준에서 컴포넌트 테스트, 그 하위의 클래스 테스트까지 체계화하고, 유기적으로 연계할 방법이었다.

그로부터 2~3년이 흘렀다. 토비형 질문을 통해 내 머리속에서 추출한 TDD의 정의는 내가 TDD를 어떻게 써먹었는가를 여실히 보여준다. 당시 내 입장에선, 지금 이곳에서 유용한 내용을 실천하는 것이지, 원론적인 TDD는 아니었으니까.(지금은 2~3년 전과 같은 상황이 아니고, 이젠 TDD를 Kent Beck이 정의한대로 이해할 필요가 있기 때문에 다시 TDDBE책을 열어봤다. Preface를 꼭 보시기 바란다. ^^)

테스트 주도 개발 - 10점
켄트 벡 지음, 김창준 외 옮김/인사이트

2~3년이 지났지만, 밑줄친 저 부분에 대한 대답은 여전히 요원하다. 나비효과로 토비형과의 대화에서 'TDD에 어떻게 관심을 가졌는지'를 떠올렸지만, 밑줄친 과제(?)는 쉽게 잊지 못하는 것 같다. 내가 DDD에 관심을 갖았던 것도 거의 같은 이유다. UML을 소모적인 다이어그래밍에 쓰지 않고, 어떻게 설계모델을 구현에 유기적으로 반영할 것인가?

마침 다시 한번 모델링을 돌아볼 기회가 왔다. 어제부터 일을 잠시 쉬면 머리 속에서 모델링에 대한 아이디어가 모락모락 피어난다.

설정

트랙백

댓글

Toby형과 온라인으로 채팅을 하다가 애자일 메소드의 다른 지침은 명확한데, TDD는 대체로 느슨하다는.. 켄트 벡의 얘기를 하다 Toby형이 돌연 내가 생각하는 TDD가 뭐냐고 묻는다. 결국 다음과 같이 대답했다.

안영회 님의 말:요구사항 구현하는 과정에서 테스트를 출발점이자 기준으로 삼는 개발방식
Toby Lee 님의 말: 결국 요구사항을 검증할 수 있게 코드로 먼저 만들어 놓고 개발하는 것이 tdd라는 얘기네.
안영회 님의 말: 검증과 명세 두 가지 의미가 있겠죠.

사실 이러한 정의는 TDD 핵심을 언급했다기 보다는 Usecase Driven Developement에서 유기적인 설계가 어렵다는데서 대안을 찾던... 내 이력을 떠오르게 했다. :)

토비형의 정의를 묻자 다음과 같이 대답했다.

결정사항과 그 피드백의 간격을 인식하고, 그 간격을 제어하는 기법이야.

Toby Lee 님의 말: 그리고 화장실에서 TDDBE를 다시 읽었다고 하면서.. 켄트백의 얘기를 정리해준다. 결정 사이에 피드백을 제공하는 자동화된 코드를 가진다 이거지. tdd의 핵심은 그 결정과 결정 사이에 피드백을 이용하는 기법이지. 테스트를 어떻게 작성할 것인가는 한참.. 밑에서 등장하는 전략일 뿐이지.

조금 발전한 간단한 설계적 결정을 하고.. 그 피드백을 빨리 받고 그리고 용기를 얻은 후 조금 더 나아가고, 필요하면 중복을 제거해서.. 클린한 코드로 만들고, 세분화된 결정단계의 모든 피드백을 자동화된 테스트로 가지고 있으니 더 용기를 가지고 유기적인 설계가 지속되게 할 수 있다는 거지. tdd는 design skill이라는 얘기켄트 백이 정의한 tdd가 바로 이거야.

다들.. test 코드 만드는데만 정신이 팔리지. 그래서 두가지로 가지. 하나는.. unit test라는데 집착해서 pure unit test주의자가 되서 거기에 목매거나, 아니면.. 요구사항을 테스트로.. acceptance test를 전통적인 qa기법의 대체로 하는 자동화된 테스트 기반의 QA 기법으로 빠지거나. 다들 방향을 잘못잡고 있는거지. tdd를 통해서 어떻게 설계가 발전하는가 이지. test 코드 만드는 기법이 아니거든. 그래서 그걸 봐도 막상 test를 만들려면 잘 안되는게 그 책은 test 만드는 법을 알려주는 게 아니니깐. tddbe의 서문을 잘 읽어바. 거기 다 나오는 얘기야.

Write new code only if an automated test has failed.
TDD is an awareness of the gap between decision and feedback during programming, and techniques to control that gap

젠장 나는 서문을 빼고 읽는 버릇이 있다. 집에 가서 다시 읽어봐야겠다.

(집에 와서)

이런 TDDBE에서 내가 보고싶어했던 내용이 Preface에 다 있었다. 쩝... Preface/서문/머릿말은 무조건 스킵하는 습관때문에 앙꼬를 다 빼먹었다. 토비형이 이 책을 다시 뒤척이는걸 보니 애자일 스프링 책을 쓰고 있긴 한가 보다.



TDD

설정

트랙백

댓글

TDD isn’t about testing

TDD가 테스트에 대한 것이 아니고 그럼 도대체 뭐냐?

TDD is all about speed.

테스트 만들면 더 느려지는데 무슨 소리냐? 라고 한다면 TDD를 전혀 모르는 것이다.

Fixing defects that could have been spotted during development is a waste of time and money. Companies that are developing software without early testing will undoubtedly be stuck with a code base that can’t change easily– and they’ll be beaten by a company who is more nimble and quick– because they build solid code from the start with TDD.

TDD를 채용하면
  • 조기에 결함을 발견할 수 있어 누적되는 디버깅 시간이 줄어들고
  • 변경이 필요할 때, 영향도 파악이 가능해 빠른 프로그램 수정이 가능하다(Regression Test를 떠올려보라)
우리 팀에서 플랫폼 호환성을 검토하는 테스트를 해야 하는데, 그간 만들어두었던 많은 테스트가 있어서 가능하다. 테스트가 없었다면 호환성 테스트는 그야말로 막막한 초대형 공사다.

물론, TDD를 배우는 시간이 필요하지만, 세상의 모든 기법, 기술, 프로세스에는 학습 비용이 든다.

설정

트랙백

댓글

Mike Hill의 TDD에 대한 실용적인 태도를 잘 보여주는 글이 InfoQ에 올라왔다.

We call XP's unit tests "microtests", at least in part to sidestep the tedious and error-prone business of constantly explaining how XP's unit tests are quite unlike the testing world's unit tests.

TDD를 해보지 않은 사람들과 TDD의 단위 테스트를 이야기 할 때 나타나는 혼선을 막기 위해 단위 테스트 대신 '마이크로테스트'라고 다른 말을 쓴다는 것이다. :)

또한, 고품질은 TDD의 부수입(side effect)일 뿐이라며 본래 목적은 생산성에 있음을 효과적인 어법으로 표현했다.

We take the position that the real benefit of extensive microtest-driven development isn't higher quality at all. Higher quality is a side effect of TDD. Rather, the benefit and real purpose of TDD as we teach it is sheer productivity: more function faster.

듣고 보니, 종래 개발 방식에서 QA가 관심을 두는 단위 테스트와 TDD의 microtests가 분명 구분할 만 하다. 앞으로 나도 '마이크로테스트'라는 표현을 써야겠다.


설정

트랙백

댓글

CI, TDD

설정

트랙백

댓글

이클립스를 쓸 때 헛슨의 커버리지를 보는 것은 불편하다. 클로버를 쓸 수가 없어서, 오픈소스를 찾던 중에 동국씨 블로그를 통해 EclEmma를 알게 되었다. 설치하고 나서 어떻께 쓸까 잠시 고민하다가 도움말을 보니 JUnit을 실행해야 볼 수 있었다.

  1. Run the program
  2. Analyze coverage data
실행하고 본다. 탐색기에서 Coverage As... 라는 메뉴가 뜬다. 빨간 줄이 뭔가 해서 역시 도움말 찾아보니 금새 나온다. 테스트 안된 코드.. 노란색은 일부만 테스트 한거... 녹색은 테스트 한거...

Annotations

커버리지 툴은 필수는 아니지만, 훌륭한 조력자다.

테스트와 테스트 대상 코드 사이에서 마우스로 왔다 갔다 하는 것이 번거로와서 다시 MoreUnit을 설치했다. 이젠 이클립스 JDT에서 JUnit TestCase 자동생성을 지원하니 MoreUnit의 효용이 줄어들엇지만, 내가 필요한 것은 Ctrl+J 단축키 하나 뿐이다. ^^

사용자 삽입 이미지

스프링 스타일로 테스트 케이스 접미사를 Tests로 변경했더니 MoreUnit 디폴트와 충돌했다. 그래서 변경해주니 잘 먹는다. 편하다. ^^

설정

트랙백

댓글

    public void testGetLogger() throws Exception {

        // normal case
        assertEquals("loggerName", ApplicationLogger.getLogger("loggerName").getName());

        // unusual case
        try {
            ApplicationLogger.getLogger(null);
            fail("filename으로 null이 올 수 없습니다.");
        }
        catch (Exception e) {
            assertEquals(IllegalArgumentException.class, e.getClass());
        }

        try {
            ApplicationLogger.getLogger("");
            fail("filename으로  공백문자가 올 수 없습니다.");
        }
        catch (Exception e) {
            assertEquals(IllegalArgumentException.class, e.getClass());
        }

    }

동료개발자가 짠 테스트가 없는 코드에 단위 테스트를 같이 작성해본다. 위에 보이는 정도면 대부분 흥미를 보일 수 있는 수준이다. 그러나, 난항 끝에 긴 메소드의 작성 의도를 물어가며 만든 아래 테스트를 보자.

    public void testLogLogMessage() {
        // 시작 환경 조사
        Logger applicationLogger = ApplicationLogger.getLogger(ApplicationLogger.APPLICATION_LOGGER);
        assertNotNull("applicationLogger 로거가 없습니다.", applicationLogger);
        assertEquals("로거 이름이 applicationLogger가 아닙니다.", ApplicationLogger.APPLICATION_LOGGER, applicationLogger.getName());

        Logger defaultLogger = ApplicationLogger.getLogger(ApplicationLogger.DEFAULT_LOGGER);
        assertNotNull("defaultLogger 로거가 없습니다.", defaultLogger);
        assertEquals("로거 이름이 defaultLogger 아닙니다.", ApplicationLogger.DEFAULT_LOGGER, defaultLogger.getName());

        assertNull("default Level이 null이 아닙니다.", applicationLogger.getLevel());   
       
        // log4j.xml 설정
        FileAppender fileAppender = new FileAppender();
        fileAppender.setName("FILE");
        defaultLogger.addAppender(fileAppender);
        ConsoleAppender consoleAppender = new ConsoleAppender();
        consoleAppender.setName("CONSOLE");
        defaultLogger.addAppender(consoleAppender);
        JDBCAppender jdbcAppender = new JDBCAppender();
        jdbcAppender.setName("DB");
        defaultLogger.addAppender(jdbcAppender);
       
        applicationLogger.setLevel(Level.DEBUG);
        AsyncAppender asyncAppender = new AsyncAppender();
        asyncAppender.setName("ASYNC");
        applicationLogger.addAppender(asyncAppender);
       
        // log message 설정
        LogMessage logMessage = new LogMessage();
        List noticeTypeList = new ArrayList();
        noticeTypeList.add("FILE");
        noticeTypeList.add("CONSOLE");
        logMessage.setNoticeTypeList(noticeTypeList);
       
        // applicationLogger가  DEBUG모드일 때 debug() 수행
        assertTrue("applicationLogger가 DEBUG 모드가 아닙니다.", applicationLogger.isDebugEnabled());
       
        // Async debug
        AsyncAppender asAppender = (AsyncAppender) applicationLogger.getAppender("ASYNC");
        assertNotNull(asAppender);
       
        ApplicationLogger.log(logMessage, new Throwable());
        assertTrue("Location Info가 false입니다.", asAppender.getLocationInfo());
        assertNotNull("FILE 어펜터가 부착되지 않았습니다.", asAppender.getAppender("FILE"));
        assertNotNull("CONSOLE 어펜터가 부착되지 않았습니다.", asAppender.getAppender("CONSOLE"));
        assertNull("DB 어펜터가 부착되어 있습니다.", asAppender.getAppender("DB"));

        // Sync debug
        applicationLogger.removeAllAppenders();
        ApplicationLogger.log(logMessage, new Throwable());
        assertNull("", applicationLogger.getAppender("ASYNC"));
        assertNotNull("FILE 어펜터가 부착되지 않았습니다.", asAppender.getAppender("FILE"));
        assertNotNull("CONSOLE 어펜터가 부착되지 않았습니다.", asAppender.getAppender("CONSOLE"));
        assertNull("DB 어펜터가 부착되어 있습니다.", asAppender.getAppender("DB"));
       
    }

로거 설정 사항을 읽어서 동적으로 특정 로거 인스턴스에 어펜더를 붙이는 코드를 테스트한 것이다. 옆에서 작성하는 모습과 설명을 듣던 동료는 '굳이 이렇게 해야 하나?'라는 반응이었었다. 엄격한 설계를 하지 않은 경우에 테스트마저 작성하지 않는다면, 암산(?)으로 설계를 할 것인가?  내가 동료의 코드를 파악하는 동안에 설명을 요구했지만, 그는 정확한 작동 원리는 설명하지 못했다. 작성의도 정도만 설명할 뿐이었다. 테스트를 작성하고 나서 메커니즘이 잘못된 것을 알게 되었다.
TDD

설정

트랙백

댓글

TDD를 위한 이클립스 메소드 생성 템플릿에서 소개한 UnsupportedOperationException을 활용한 기법은 상당히 유용하다. Test Driven Development: By Example에서 가장 먼저 나오는 코드는 다음과 같다.

 @Test public void multiplication(){
  Dollar five = new Dollar(5);
  five.times(2);
  assertEquals(10, five.amount);
 }

JUnit 3.x 코드는 JUnit4 용으로 바꿨을 뿐이다. 이를 만족시키기 위한 최소한의 작업을 했을 때 생성된 코드(production code)는 다음과 같다.

public class Dollar {
 public int amount = 10;
 public Dollar(int amount) {}
 public void times(int i) {}
}

코드를 향상 시키기 위해서는 먼저 테스트를 더 엄밀하게 해볼 수 있다.

 @Test public void multiplication(){
  Dollar five = new Dollar(5);
  five.times(2);
  assertEquals(10, five.amount);
 
  Dollar six = new Dollar(6);
  six.times(2);
  assertEquals(12, six.amount);
 }


(중복에 대한 문제를 배제하고 보면) 위와 같이 새로운 사례를 추가하면 코드의 품질이 향상된다. 왜냐하면, 속성에 상수값을 설정하여 문제를 해결하는 방식은 더 이상 통하지 않게 되니까. 이러한 개선 방법을 삼각측량(triangulation)이라고 한다.

이 정도로 간단한 것을 면밀하게 살펴보는 것에 의아한 사람도 있을 것이다. 이러한 과정은 자기가 짠 코드에 대해 완벽하게 통제하기 위한 의도로 훈련하고 있는 것이다. 그간의 나는 내가 짠 코드가 미치는 영향에 대해서 충분히 숙지하지 못한채 빠르게 타이핑을 하는데만 열중했다. 단계를 모두 밟아 가는 과정을 모두 경험한 상태에서 빠르게 일을 수행하는 것과 한번에 문제를 해결하려고 드는 것은 엄청난 차이가 있다.

관점에 대한 이야기는 이 정도로 마무리하고, TDD를 위한 이클립스 메소드 생성 템플릿에서 소개한 로드 존슨의 기법을 써보자.

 public int amount = 10;

 public Dollar(int amount) {
  throw new UnsupportedOperationException();
 }

 public void times(int i) {
  throw new UnsupportedOperationException();
 }

예외를 발생하는 구문을 유심히 보자.


 @Test public void multiplication(){
  Dollar five = new Dollar(5);
  five.times(2);
  assertEquals(10, five.amount);
 }

굵게 표시한 부분에서 이들 메소드를 호출한다. 당연히 예외가 발생한다. 기능을 제공하는 측 즉, Dollar 객체에선 지원하는 않는 오버레이션이다. 그런데 이를 요청하니 문제가 생긴 것이다.

만일, 위의 두 구문이 제 구실을 하지 않음에도 테스트가 통화했다면
1) 테스트 코드가 잘못 되었거나
2) 이들 메소드가 불필요한 기능이거나
3) 이들 메소드의 구현이 잘못된 것으로 볼 수 있다.

여기서 빠르게 녹색불을 보기 위해서 Ctrl+D 단축키를 이용하여 throw 구문을 지워버릴 수 있다. 하지만, 엄밀하게 따지고들면 테스트 성공을 위해서 불필요한 기능이 호출되는 것을 방관하는 짓이 된다. 따라서, 불필요한 것이라면 테스트에서부터 제거한다. 그렇지 않으면, 구현이 잘못된 것이므로 구현을 수정하면 된다. 이는 삼각측량이 제공하는 도움과 같다. 결론적으로 로드 존슨의 기법을 사용하면 삼각측량의 필요성을 조금은 줄일 수 있고(그렇다고 해도, 삼각측량은 여전히 매우 유용하다.) 불필요한 기능을 방치해서 API를 오염시키는 일을 줄일 수 있다.

설정

트랙백

댓글

작성 일시: 2004/06/18 (금) 17:02

테스트 주도의 개발(TDD)가 기본적으로 아키텍처 중심적 접근보다 좋아 보이는 것은 보다 더 고객 지향적이라는 점이다. 최초의 Acceptance Test(보통 인수테스트라고 부르지만, 이 말은 프로젝트 막판에 결과물을 넘겨준다는 듯한 뉘앙스가 느껴져 쓰지 않겠다.)에서는 사용자 요구사항을 명확하게 한다. 그리고 점차 내부 구성으로 들어가서 단위 테스트 단계에서도 먼저 테스트를 작성한다.

먼저 테스트를 작성한다는 것은 특정 단위(그게 단일 객체든, 더 큰 덩어리든)의 사용자 입장에서의 유용성을 먼저 설정한다는 것과 같은 의미다. 이렇게 애초부터 사용자(최종 사용자든 특정 객체의 사용자든)에게 가치가 있도록 설정해놓고, 내부를 고민하는 것이다.

아키텍처 중심적으로 가면 무엇이 더 좋을까? TDD랑 비교해서 장점을 생각해보면 가장 먼저 떠오르는 것이 효율성이다. 초기에 아키텍처를 견고하게 만들어놓고, 요구사항이 거기에 맞게 흘러가도록 하는 것. 그러나, 여기에는 함정이 있다고 생각된다. 아키텍처가 잘못되었으면 어떻게 할까?

결국 요구사항 변경을 골칫거리로 여기는 근저에는 잘못된 아키텍처 설정으로 인해 요구사항을 반영하는 것을 회피하는 것이라고 이해할 수 있을 것 같다. 아키텍처란 개념을 모르고 막 개발하더라도, 일련의 규칙들을 사전에 설정하거나, 알게 모르게 코드로 프로그램에 삽입된다.

물론, 아키텍처 중심적 방법에서도 반복적이고, 점진적인 방식은 중요시된다. 그럼에도 불구하고, 효율보다는 고객의 가치를 중시되어야 할 것 같다.

설정

트랙백

댓글