Extremely Agile/TDD2008.11.04 14:20
앞서 남이 짠 클래스 코드를 어떻게 리팩토링하면 jMock을 통해 테스트하기가 좀 더 쉬워지는지를 살펴봤습니다. 그런데 현실 세계의 클래스들이 전부 그렇게 간단하게 만들어져 있으면 문제가 쉬운데, 실제로 테스트를 진행하다보면 그렇지 않은 경우를 종종 만나게 됩니다.

가장 골치아픈 경우 중 하나는, 생성자 안에 해당 클래스의 비즈니스 로직(?)이 들어가 있는 경우죠. 그러니까 좀 돌려서 말하면, 클래스의 생성자 코드 안에 테스트해야하는 알고리즘의 일부가 들어가 있는 경우입니다. 이 알고리즘이 대상으로 하는 객체들 중 하나를 Mock 객체로 만들어 테스트해야 한다면, 상황은 간단히 풀리지 않습니다. 다음 코드를 보시죠.

class Foo {
    public Foo() {
        ...
        bar.doSomething();
        ...
    }
    ...
};

bar에게 뭔가를 하고 있습니다. 그런데 bar에 doSomething을 호출하는 순간 네트워크 접속이 발생하기 때문에, 그 상황을 피하기 위해 bar를 mock 객체로 만들고 싶다고 해 보죠. 앞선 글에서 사용한 방법(아마 패턴을 아시는 분이라면 그 방법이 소위 '템플릿 메소드 패턴'인건 아시겠습니다만...)을 그대로 써먹자면 다음과 같이 해야 할 텐데요.

class Foo {
    protected void doSomething() {
        ..
        bar.doSomething();
        ..
    }

    public Foo() {
        ...
        doSomething();
        ...
    }
    ...
}

얼핏 보면 그럴듯해 보입니다만, 다음과 같이 하는 순간 문제가 복잡해집니다.

class FooT extends Foo {

    private Bar bar;

    protected void doSomething() {
        ...
        // do something with bar
    }

    public FooT() {
        super();
        // do something with bar
    }
}

왜 문제가 복잡해지죠? FooT의 생성자가 불리는 순간, 상위 클래스의 생성자가 불리면서 doSomething()이 호출되는데, 이 때 호출되는 doSomething은 하위 클래스에 정의된 doSomething이어야 하거든요. 그런데 아직 객체가 완전히 초기화도 되지 않은 상태이니, 하위 클래스의 doSomething이 제대로 수행될 리가 없죠. 이런 문제는 C++이건 Java건 공통적으로 발생하는 문제입니다. C++에서는 가상 함수 포인터 초기화에 관련된 오류가 발생하면서 프로그램이 죽는 현상이 발생하고, Java에서는 멤버 변수 초기화에 관한 오류 메시지가 뜨면서 프로그램이 죽습니다.

이 문제는 "Don't call subclass methods from a superclass constructor"라는 글에 잘 정리가 되어 있습니다. 요지는 When designing a class for subclassing, it’s important to avoid calling any method that the subclass could or must override from the superclass constructor. 인데, 간단히 요약하면 "하위 클래스가 오버라이드할 가능성이 있는 메소드를 생성자 안에서 호출하는 것은 피해라"라는 뜻입니다. 하위 클래스에서 오버라이드를 해버리면, 이런 문제가 생길 수 있으니까요.

그러니까 생성자 안에 테스트 해야할 알고리즘의 일부가 들어있는 경우에는, 원래 클래스의 코드를 수정하는 일이 불가피해지고 맙니다. 그런 일이 자주 생기면 좋지 않기 때문에, 가급적 생성자 안에는 "초기화에 관련된 코드들"만을 두는 것이 바람직하겠습니다.

그럼 "어쩔수 없이 원래 클래스 코드를 수정해야 하는 경우"에는 어떻게 수정하는 것이 바람직 할까요? 가령 다음과 같은 클래스가 있다고 해 봅시다. 제가 실무에서 만난 클래스입니다.

public class Connection implements Definition {
   ...
   public Connection(String ip, int port, int type) throws Exception {
      ...
   }
   ...
}

이 생성자는 내부적으로 Socket 객체를 생성하여 그 메소드를 호출합니다. 골치아픈 생성자죠. ㅋㅋ 우리가 해결해야 할 목표는, Socket 객체를mock객체로 바꿔칠 수 있는 방법을 Connection 클래스에 추가하는 것이었습니다. 그래서 코드를 다음과 같이 변경했습니다.

public class Connection implements Definition {
   ...
   public Connection(String ip, int port, int type) throws Exception {
      init(ip, port, type, null);
   }
   
  Connection(String ip, int port, int type, Socket alternativeSocket) throws Exception {
      init(ip, port, type, alternativeSocket);
   }

   private void init( ... ) {
      // if alternativeSocket is not null, do something with it.
      // or, create Socket object and deal with it
   }
   ...
}

간단히 요약하면, (1) initialization 로직은 별도의 private 함수로 refactoring하고 (하위 클래스에 의해 오버라이드 되지 않아야 하기 때문에 private입니다) 그 함수의 네번째 인자로 외부에서 정의한 소켓 객체 (mock 객체겠죠)를 받은 경우 그 객체를 가지고 필요한 작업을 하도록 변경했으며, (2) 테스트를 위한 별도의 생성자를 정의하고, 그 생성자의 네번째 인자로 mock 객체를 인자로 받아 init 함수에 넘기도록 한 다음 (3) 원래 생성자는 init 함수를 호출하되 네번째 인자로 null을 넘기도록 코드를 변경했습니다.

이렇게 함으로써 코드에 그다지 '큰' 변경을 가하지 않고서도 테스트 가능성을 확보할 수 있었습니다. 새로 추가된 생성자는 그 scope가 package 이어야 합니다. 그래야 테스트 코드 안에서만 사용이 가능할테니까요.

[다음 글에서 계속...]

신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

Extremely Agile/TDD2008.10.31 14:31

jMock은 Mock 객체를 이용해 테스트를 지원하는 라이브러리입니다. www.jMock.org에서 다운받으실 수 있습니다. 요즘 회사에서 '다른 사람이 작성한 코드'를 좀 더 '테스트 가능'(testable)하게 만드는 동시에 test coverage를 높이는 일을 하고 있습니다. 작년까지 했던 일과는 조금 다른 일이라, 재미있습니다.

jMock 계열의 라이브러리로는 EasyMock 같은 것도 있습니다만, 써보니까 'Easy'하다고 꼭 좋은것만도 아니어서 다시 jMock으로 돌아왔습니다. 돌아와보니 jMock이라고 또 그리 사용법이 복잡하지는 않습니다. 사용법이 좀 까다로와 보여서 진입장벽이 '약간' 있을 뿐이죠.

Mock 라이브러리를 사용해 테스트 코드를 작성하는 목적은, '테스트 대상 알고리즘을 그 알고리즘이 대상으로 하는 자원과 분리'하는 것이라고 볼 수 있겠습니다. 가령 알고리즘 foo가 어떤 자원 bar들을 대상으로 돌아간다고 해 봅시다. bar의 구현 상태와는 독립적으로, foo의 정확성만을 테스트하고 싶다고 해 봅시다. 그 경우에는 '원하는 대로 동작하는 것이 보장된' 가짜 bar 객체들만을 만들어 두고 그 위에서 foo를 돌리게 되면 foo의 정확성을 무리없이 검증할 수 있을 것입니다.

그러니 결국 Mock 라이브러리들이 하는 일은, '분리(separation)'라고 요약할 수 있겠습니다.

실제 '남의 코드'를 테스팅하는 사례를 살펴보기전에, 우선 jMock 라이브러리 사용법의 '골격'부터 알아보도록 하겠습니다. jMock라이브러리를 사용하기 위해서는, 대략 다음과 같은 클래스들을 임포트해 둘 필요가 있습니다. (뭐 eclipse가 알아서 다 해주긴 하지만..ㅎㅎ) 전 JUnit 4를 가지고 프로그래밍을 했기 때문에, JUnit4Mockery 클래스를 임포트하도록 했습니다.

import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.integration.junit4.JUnit4Mockery;
import org.jmock.lib.legacy.ClassImposteriser;

그런 다음 이제 JUnit 테스트 클래스의 안을 채워나가야 하는데요. ConnectionManager라는 클래스가 있다고 치고, 그 클래스를 테스트하는 JUnit4 클래스를 구현한다고 가정하겠습니다. 그 클래스 제일 윗부분에 다음과 같은 부분을 둡니다.

public class ConnectionManagerTest {

  private Mockery context = new JUnit4Mockery() {{
    // enable mocking of class also (not only interface!)
    setImposteriser(ClassImposteriser.INSTANCE);
  }};
  ...

}

보통 Mock 라이브러리들은 Interface 클래스를 인자로 받아, 그 인터페이스가 하는 일을 흉내내는 가짜 객체를 생성합니다. 그런데 EasyMock이나 JMock은 확장을 통해 일반 클래스를 인자로 받아서 Mock 객체를 생성할 수 있도록 배려하고 있습니다. 위의 적색으로 표시된 부분은, 일반 클래스를 인자로 받아 Mock 객체를 생성할 수 있도록 지시하는 부분이라고 생각하시면 됩니다.

이렇게 하고 나면 이제 테스트 코드를 작성하면 되는데요. 대략 다음과 같은 순서를 따릅니다.

 @Test
 public void testRun() throws Exception {

  // 1. Mock 객체 생성
  final Connection mock = context.mock( Connection.class );

  // 2. Mock 객체를 테스트 대상 알고리즘에 세팅
  ...
 
  // 3. 알고리즘을 돌렸을 때 mock 객체에 무슨 일이 일어나게 되는지를 명시
  context.checking( new Expectations() {{
   allowing (mock).getInteger(); will(returnValue(34));
   oneOf (mock).writeInteger(56);
   allowing (mock).close();

  }});

  // 4. 알고리즘 구동
  ...

  // 5. 각종 assert 문들 삽입
  ...
 }

우선, Mock 객체를 만듭니다. context.mock의 인자로 객체를 찍어낼 클래스를 인자로 넘겨주어야합니다. 그런 다음, 만들어진 mock 객체를 테스트 대상 알고리즘에 세팅합니다. 알고리즘을 구동할 때 mock 객체를 인자로 넘겨주게 되어 있다면, 이 단계를 생략해도 무방하겠습니다.

그런 다음, 알고리즘을 실제로 돌리면 mock 객체에 어떤 행위가 일어나게 되리라 '기대'(expectations)하는지를 세팅합니다. context.checking 메소드에 Expectations객체를 만들어 넘겨주는 식으로 세팅하게 되는데, 그 안에 발생할 것으로 기대되는 행위 목록을 적어주면 됩니다. 일단 위의 예제의 의미를 간단히 설명드리면,

allowing... 으로 시작되는 줄은, 알고리즘 구동 과정에서 mock 객체의 getInteger() 함수가 여러번 불리게 될 수 있는데, 그 때 마다 34를 리턴하도록(will(returnValue(34))) 지시하고 있습니다.
oneOf... 로 시작되는 줄은, 알고리즘 구동 과정에서 mock 객체의 writeInteger 함수가 단 한번 불리게 되는데, 그 때 인자로 56이 전달되어 호출되어야 함을 명시하고 있습니다.
마지막의 allowing...으로 시작되는 줄은, 알고리즘 구동 과정에서 mock 객체의 close() 함수가 여러번 불리게 될 수 있다는 것을 명시하고 있습니다.

결국, mock 객체가 알고리즘의 요구에 어떻게 반응하여야 하는지를 명시하는 셈입니다. 이 부분을 잘 작성해주면 mock 객체에 전달되는 값이 예상치와 일치하는 지를 검사하여 알고리즘이 제대로 된 값을 사용해 mock 객체와 통신하는지를 검사할 수 있게 되고, 결국 알고리즘의 정확성을 검증할 수 있게 됩니다.

이렇게 expectation의 목록을 작성하고 나면, 그 다음으로는 알고리즘을 구동시키고, assert구문들을 사용하여 (assertTrue, assertFalse 등) 구동 후의 상태가 예상한 것과 일치하는지를 보면 됩니다.

expection을 작성하는 문법이나, JMock 사용법에 대해 더 자세히 알고싶으신 분은 JMock 웹사이트에 올라온 cheat sheet를 보시면 좋을 것 같습니다.

[다음 글에 이어서...]

신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

  1. 이상곤

    JMock에 대한 강좌 잘 읽었습니다. 헌데 따라 하는 중 ClassImposteriser 클래스를 임포트하는 과정에서 바로 막혔습니다. ㅠㅠ package가 org.jmock.lib.legacy 아래에 ClassImposteriser 있다고 나와 있고 API 문서를 봐도 그렇게 나와 있는데 stable 버전 2.5.1 jar 파일에는 그 이하에 legacy가 안보입니다. 혹시 제가 뭔가 간과 한 부분이 있는지요?

    2010.01.20 14:26 신고 [ ADDR : EDIT/ DEL : REPLY ]

Extremely Agile/TDD2007.09.23 20:45

아래의 그림은 TDD를 사용한 개발을 수행하는 절차를 요약하고 있습니다.

원본은 여기에 링크 걸어둔 PDF 파일에 나옵니다. 하도 예전에 다운받아둔거라 언제 받았는지도 기억이 잘 나질 않습니다만, PDF 파일을 열어 보면 어디서 다운받은 것인지는 나오는군요. :-) 참고하시기 바랍니다.

나온지 좀 오래된 자료이기는 합니다만, TDD의 개발 절차라는 것이 TDD 개념이 등장한 이래로 그렇게 많이 바뀌거나 하지는 않았습니다. 코드 작성 전에 테스트를 먼저 한다, 는 원칙은 그대로 있고, 그 적용에 따르는 세부사항만 좀 바뀐 정도일텐데, 그 나마 큰 변화가 있는 것 같지는 않아요.


사용자 삽입 이미지



절차는 간단히 요약하면 이렇습니다.
  1. 테스트 리스트를 만듭니다. (어떤 테스트를 수행할 것인지 협의하는 과정에서 나옵니다. 사용자 스토리를 만드는 과정에서 도출되는 일이 많습니다.)
  2. 다음의 3~7까지를 계속 반복합니다.
  3. 테스트 리스트에서 테스트 하나를 고릅니다.
  4. 우선, 컴파일조차 되지 않는 테스트 코드를 작성합니다. (컴파일이 되지 않으니 테스트를 아예 할 수가 없습니다.) [위의 다이어그램에서는 위에서 두 번째 줄의 왼쪽에서 두 번째 박스]
  5. 대충 뜯어 고쳐서 컴파일은 되도록 만듭니다. 테스트는 당연히 실패할 것입니다.
  6. 그런 다음, 우선 테스트가 통과되도록 만듭니다. (테스트가 통과되도록 만드는 것이 목적이니, 코드에 hardwired 된 로직이 존재해도 아직은 상관이 없습니다. 가령, 3이라는 반환값을 요구하는 테스트라면, 함수 안에서 그냥 3을 리턴해도 이 단계에서는 상관이 없다는 말입니다.
  7. 그런 다음 리팩토링(refactoring)을 시작합니다. 리팩토링을 해나가면서 중복을 제거하고, 말이 안되는 부분을 하나씩 말이 되도록 만듭니다. 리팩토링은 '아주 작은 단위'의 코드 변경입니다. 코드가 변경될 때 마다 컴파일하고 테스트를 돌려서, 그 리펙토링이 테스트를 fail시키지 않는지 확인해야 합니다. fail시켰다면, 너무 작은 리팩토링을 시도했거나, 잘못된 리팩토링 - 즉, 논리적인 오류를 발생시킨 리팩토링을 했다는 뜻입니다. 위의 그림에서는 '리팩토링'->'테스트' 사이의 순환 관계가 명시적으로 드러나지는 않습니다만, 리팩토링과 테스트는 반복적으로 계속해서 해나갈 수 있습니다.
뭐, 그림 자체는 그런 뜻입니다. :-) 그럼 테스트는 대체 어떻게 만드느냐.

이상적으로 보자면 테스트가 '개발될 모듈이 가져야 할 이상적인 인터페이스'에 근거해서 만들어지는 게 가장 좋겠지만, 그렇게 하자면 머리속에 '개발될 모듈의 형태'가 우선 잡혀야 합니다.

TDD 개발 과정에 대한 책 ("테스트 주도 개발") 을 보신 분이면 아시겠지만 테스트는 고정적이 아니며 항상 변화합니다. 개발될 모듈의 형태가 어떠할 지 완벽하게 미리 예측하는 것이 불가능하기 때문입니다. 테스트를 하고, 중복을 제거하고 리팩토링을 하다보면 개발중인 클래스가 개발자가 예측한 것과 조금은 다른 방향으로 흘러가게 되는 일도 생길 수 있고 (물론 가급적 그렇게 되지 않는 것이 좋겠지만 말입니다) 다른 식으로 구현을 가져가는 것이 보다 효율적이겠다는 판단을 내리게 되는 일도 생길 수 있습니다.

그러니, 테스트를 '미리 잘 정돈하여 만들어두어야 겠다'는 생각은 버리는 게 좋겠습니다. '어떤 테스트를 해야 하느냐'는 고정적일 수 있지만, '테스트를 어떻게 해야 하느냐'는 것은 계속해서 달라질 수 있기 때문입니다.
신고
Posted by 이병준

소중한 의견, 감사합니다. ^^

  1. TDD를 적용하기 위한 절차를 찾다가 발견하게 되었습니다. 많은 도움 얻고 갑니다...
    즐거운 하루되세요~^^

    2008.04.22 11:38 신고 [ ADDR : EDIT/ DEL : REPLY ]
  2. TDD를 하면서 느끼는 것은 초보자 무리라는 것입니다. ^^ 제가 생각하기에는 그래요.
    일단 초보자는 상급자로부터 Requirement를 받기 때문에 상당히 제한적이고, 초반에 참여를 하더라도 경험이 적기 때문에 테스트 코드를 많이 못 만들어 내는 것 같습니다.
    또한, 초보자는 Refactoring을 해야할 때, Refactoring 자체에 대한 지식이 별로 없는 관계로 힘들고 때에 따라서 Design Pattern을 적용해야 할 때가 있는데, 그렇지 못 할 때도 있더군요. 또 Design Pattern을 몰라도 되지만, 바라던 최적화 코드는 그다지 나오지 않더라구요.

    하지만, 지속적으로 이렇게해서 개발경력을 쌓으면 무적이 될 것 같기도 하네요. 그래서 저도 TDD를 지향합니다.

    Rod Johnson의 인터뷰인가? 정확하게 기억나지는 않지만, 꼭 필요한 기술이라고 해서, 1. OFD(Oriented Framework Development), 2. DI & IOC, 3, TDD 등이 있었던 것 같네요.

    잘 읽고 갑니다.

    2008.06.11 15:05 신고 [ ADDR : EDIT/ DEL : REPLY ]
    • 반갑습니다. 초보자에게 좀 무리일 수는 있습니다만, 그 과정을 통해 배우는 것이 많다는 점에서 긍정적일수도 있을 것 같습니다. 저는 TDD를 최근에야 접했는데, 제가 통상적으로 프로그램을 짤 때 거치는 과정과 많이 닮아있어 오히려 프로그래밍을 배우기 시작하는 초기에 배웠더라면 어땠을까, 하는 생각을 자주 했습니다.

      2008.06.12 14:30 신고 [ ADDR : EDIT/ DEL ]