티스토리 툴바

LogDigger test page with tab in the Firebug

위와 같이 브라우저에서 로그를 보고 싶다면. 자바 프로그램을 개발할 때 이클립스 콘솔에서 로그를 보는 것이 흔한 일이다. 그러나, LogDigger 홈페이지에 소개한 바와 같은 상황.

It’s ideal for monitoring of application log messages in cases when multiple users access the application as each user receives only events generated by his/her request

통합 테스트 시점에 공용으로 사용하는 테스트 서버 로그 대신 테스터가 자신이 발생시킨 이벤트 로그를 확인할 때 쓸만하다. 아쉬운 점은 FireBug 콘솔에서 결과를 볼 수 있다는 제약. IE에서는 방법이 없다. X-internet 솔루션을 쓴다거나 IE 중심으로 개발하는 국내 정보시스템 개발에서는 큰 도움이 안될 듯 하다. 반면에 Firebug를 이용하여 디버깅을 해야 하는 화면 개발을 함께 하는 자바 개발자라면 유용할 듯 하다.

Firebug에 다시 LogDigger extension를 설치해야 한다. 그리고 나서, jar 파일을 lib에 넣어주고, 필터를 등록해준다. log4j.jar 도 필요하다.


    <filter>
       <filter-name>LogDigger Servlet Filter</filter-name>
       <filter-class>org.logdigger.servlet.filter.LogDiggerServletFilter</filter-class>
   </filter>

    <filter-mapping>
       <filter-name>LogDigger Servlet Filter</filter-name>
       <url-pattern>/*</url-pattern>
   </filter-mapping>


설정

트랙백

댓글

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

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

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

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

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

설정

트랙백

댓글

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

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로 개발할 때는 자신이 통제하지 못하는 상황에서 앞으로 나아가기가 어렵습니다.

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



설정

트랙백

댓글

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가 분명 구분할 만 하다. 앞으로 나도 '마이크로테스트'라는 표현을 써야겠다.


설정

트랙백

댓글


3. XML 파싱 단위 테스트를 위한 Mock 생성때 큰웃음(?)을 선사하는 DOMElement
스프링도 사용하는 dom4j에 들어있음.. Thanx

    /**
     * Test do parse element bean definition builder.
     */
    public void testDoParseElementBeanDefinitionBuilder() {
       
        MockControl control = MockClassControl.createControl(BeanDefinitionBuilder.class);
        BeanDefinitionBuilder builder = (BeanDefinitionBuilder) control.getMock();
        MockElement mockElement = new MockElement("service");
       
        control.expectAndDefaultReturn(builder.addConstructorArgValue("value of id"), builder);
        control.expectAndDefaultReturn(builder.addConstructorArgValue("value of name"), builder);
        control.expectAndDefaultReturn(builder.addConstructorArgReference("value of ref"), builder);
        control.expectAndDefaultReturn(builder.addConstructorArgValue("value of description"), builder);
        control.expectAndDefaultReturn(builder.addConstructorArgValue("value of available"), builder);
        control.expectAndDefaultReturn(builder.addConstructorArgValue("value of not-available-message-id"), builder);
        control.replay();
       
        serviceBeanDefinitionParser.doParse(mockElement, builder );
        control.verify();
    }
   
    class MockElement extends DOMElement{

        public MockElement(String name) {
            super(name);
            setAttribute("id", "value of id");
            setAttribute("name", "value of name");
            setAttribute("ref", "value of ref");
            setAttribute("description", "value of description");
            setAttribute("available", "value of available");
            setAttribute("not-available-message-id", "value of not-available-message-id");
        }}

2. JUnit
    public void testAssertEqualsOnDoubleHelper() throws Exception {
        new UnitTests().assertEquals("정확하게 같은 값이 아닙니다.", 0.0000D, 0.0000D);
        new UnitTests().assertEquals("정확하게 같은 값이 아닙니다.", 0.0000D, 0.0000f);
        new UnitTests().assertEquals("정확하게 같은 값이 아닙니다.", 1.0000D, 1.0000f);
       
        try{
            new UnitTests().assertEquals(1.000001D, 1.0000f);
            fail("delta가 존재합니다.");
        }catch (AssertionFailedError e) {
            // junit은 사용자가 지정한 오류 메시지 뒤에 대괄호를 붙이고 그 안에 expected와 actual 값을 문자열로 붙인다.
            assertTrue(e.getMessage().startsWith("완벽하게 동일한 값은 아닙니다."));
        }
    }

1. abstract 클래스 테스트하기
    public void testAutowireMode() throws Exception {
        assertEquals("디폴트 Autowire 모드가  AUTOWIRE_BY_NAME이 아닙니다.", AutowiredIntegrationTests.AUTOWIRE_BY_NAME,
                new MyIntegrationTests().getAutowireMode());
    }
   
    class MyIntegrationTests extends AutowiredIntegrationTests{}


설정

트랙백

댓글

테스트를 담당하는 팀원이 물었다.

'지금 메일 Sender 테스트 열심히 돌리고 계시죠?'

다른 일을 하고 있던 나는 아니라고 대답했다. 메일 Sender에서 수신처를 본인의 실제 메일 주소로 설정한 모양이다. 메일 박스를 확인했는데, 테스트로 보낸 메일이 쌓여 있어서 내가 테스트를 많이 해서 그런 것이라고 추측한 듯하다. 원인은 CI툴이었다. 정확히 말하면 CI툴에 설정해놓은 자동화 테스트였다. 테스트 계정을 만들어두거나 Mock을 활용한 것이 아니라 마치 자가스팸 발송 스케줄러를 돌릴 꼴이었다. 물론, CI 설정을 본인이 하지 않았으니 이런 일을 미리 예상하지는 못했으리라. 아직 개발초기라 모니터링 차원에서 Commit 시점마다 테스트를 수행하도록 설정해두었기에 꽤나 메일이 많이 보내졌으리라. 어쨌든 덕분에 웃을 수 있었다.

설정

트랙백

댓글

IDataSet dataset = new XlsDataSet(....)

위 구문을 서로 다른 프로젝트에서 실행시켰더니 한 곳에서는 정상 동작하고, 다른 곳에서는 예외가 발생했다. 예외는 dbunit이 512 kb인지 bytes인지 단위로 데이터를 읽나본데, 데이터를 잘못 읽는 것이었다. 한 줄도 다르지 않은 코드이기에 원인을 알 수 없었다. 답답한 마음에 엉뚱한 것을 의심했다. 비스타라 그런가? 엑셀 버전 문제인가? 이런 류의... @@

원인은 dbunit 라이브러리 버전 차이에서 왔다. 정상 실행하는 경우 dbunit.jar 버전은 dbunit-2.1.jar 이고, 수행 오류가 나는 버전은 dbunit-2.2.1.jar 이다. API만 봐서는 이유를 알 수 없다. 원인을 알았으니 2.1 버전을 쓰자로 귀결을 지을지, 정확한 원인 수사(?)에 들어갈지 아직 결정하지 못했다. 우선 중요한 일부터...

설정

트랙백

댓글

EasyMock(old)을 활용한 협업 테스트 에서 소개한 불편한 EasyMock 1.2. Mock 객체를 사용한 테스트 자체도 이해하는데 장벽이지만, EasyMock 1.2는 벽을 이중으로 만든다. 기억하기도 어려울 뿐더러 직관성도 떨어진다. 우선 EasyMock과 ClassExtension의 차이가 나타나지 않도록 createControl 팩토리 메소드를 래핑하는 선에서 그냥 썼다. 어제 밤 팀원에서 Mock을 이용한 테스트에 대해 안내하면서 API가 어려워 리듬을 잃어버리는 일이 발생했다.

취침에 들어가려고 발을 씻던 중에 아이디어를 실험하기 위해 컴퓨터 앞에 앉았다. 내일을 위해 오버하면 안되는데... 일단 EasyMock 문서가 부족하다고 불평했었는데, 1.2 안에 sample이 있었다. 이런... 샘플을 무시하는 습관이라니..ㅡㅡ;

API 대치/래핑을 통해 샘플의 코드를 개선할 수 있다면 1차적으로는 성공이다. 그리고, 그것이 최소한 EasyMock 1.2를 써본 팀원에게 좋은 평을 받는다면 졸린 눈으로 버틴 시간이 보상받을 것이다.

        control = MockControl.createControl(IMethods.class);
        mock = (IMethods) control.getMock();

예제의 setUp에서 나온 코드. 전형적인 쌍이다. 사실 난 MockControl이란 것이 마음에 들지 않는다. MockControl은 두 가지 역할을 갖는다. 하나는 Mock객체에 대한 기대값을 설정하는 것이고, 다른 하나는 테스트 수행을 위한 컨트롤 역할이다. 어찌 되었든 MockControl 보다는 MockExpections이 나은 듯 하다. 위키피디아에서 본 Setting expectations 문구가 힘을 실리게 해준다. 위의 두 줄의 문장을 하나로 바꾸고 싶은데, 리턴이 두 개인지라 어려워보인다.

사실상 저 둘은 불가분의 관계로 보이니까 Spring의 ModelAndView 처럼 하나로 묶어보자.

  expectations = new ExpectationsOn(IMethods.class);
  mock = (IMethods) expectations.getMock();

MockExpections 라는 클래스로 합치는 것을 시도해보았으나 결국은 Mock 객체 호출이 필요했다. 여기까지는 별반 차이가 없어 보인다. 하지만, 실행 코드는 좀 간결하고 직관적으로 바뀌었다. 아래와 같은 코드였는데

     mock.throwsNothing(true);
     control.setReturnValue("Test");
     control.setReturnValue("Test2");
     control.replay();

     assertEquals("Test", mock.throwsNothing(true));
     assertEquals("Test2", mock.throwsNothing(true));

     control.verify();


안 불러도 별 차이가 없는, verify()는 생략해버리고 Control보다는 '기대 값'이란 점을 강화했다. API 스타일은 Fluent Interface를 채용했다.

  mock.throwsNothing(true);
  expectations.returns("Test").returns("Test2").assert();

  assertEquals("Test", mock.throwsNothing(true));
  assertEquals("Test2", mock.throwsNothing(true));

ready 가 작위적인 느낌이 있지만, 익숙하지 않은 사람에게 replay 역시 큰 차이가 없다. 여기까지만 하고... 위키피디아의 Mock 객체에 대한 메모를 남겨둔다.


a mock object in its place:

  • supplies non-deterministic results (e.g. the current time or the current temperature);
  • has states that are difficult to create or reproduce (e.g. a network error);
  • is slow (e.g. a complete database, which would have to be initialized before the test);
  • does not yet exist or may change behavior;
  • would have to include information and methods exclusively for testing purposes (and not for its actual task).  

설정

트랙백

댓글

EasyMock1.2 스타일의 API는 EasyMock2와는 스타일이 많이 다르다. EasyMock2 사용법은 아래와 같다.


빠른 습득을 위해 스프링의 문체를 분석하고, API를 토대로 주요 구문의 의미를 이해하는 방식을 취하겠다.

        MockControl dsControl = MockControl.createControl(DataSource.class);
        DataSource ds = (DataSource) dsControl.getMock();
        ...
        ds.getConnection();
        dsControl.setReturnValue(con, 2);
        ...
        dsControl.replay();
        ...
        HsqlMaxValueIncrementer incrementer = new HsqlMaxValueIncrementer();
        incrementer.setDataSource(ds);
        ...
        assertEquals(0, incrementer.nextIntValue());
        ...
        dsControl.verify();

막상 살펴보니 별 차이는 없다. 문장이 좀 지져분해질뿐...ㅡㅡ;

(2008.4.18 추가)

    public void testInitBinder() throws Exception {
        MockControl control = MockClassControl.createControl(ServletRequestDataBinder.class);
        ServletRequestDataBinder mockBinder = (ServletRequestDataBinder) control.getMock();
        // record
        mockBinder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat(), false));
        control.setMatcher(new ArgumentsMatcher() {

            public boolean matches(Object[] expected, Object[] actual) {
                return expected[0].equals(actual[0]) && expected[1].getClass().equals(actual[1].getClass());
            }

            public String toString(Object[] arg0) {
                throw new UnsupportedOperationException();
            }
        });

        control.replay();
        controller.initBinder(new MockHttpServletRequest(), mockBinder);

        control.verify();

    }

EasyMock2와 달리 ArgumentsMatcher를 사용하여 매개변수 내용을 확인할 수 있다.

설정

트랙백

댓글

이클립스를 쓸 때 헛슨의 커버리지를 보는 것은 불편하다. 클로버를 쓸 수가 없어서, 오픈소스를 찾던 중에 동국씨 블로그를 통해 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

설정

트랙백

댓글

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가 가능하게 하려면 테스트 메소드의 가독성을 높이는 작업이 필요합니다. 흠 이건... 작업자에게 숙제로 남겨줘야겠군요. 테스트 코드에서 한 단락 단위로 테스트 메소드를 만들어주면 대강은 작업자의 의도와 드러맞겠죠.

설정

트랙백

댓글