Extremely Agile/TDD2008.11.03 16:09

그러면 오늘은 jMock을 이용해 남의 코드를 테스팅하는 과정을 한번 살펴보겠습니다. 정답이라고 할 수는 없고, 저 개인적인 경험에 따른 것이니, 참고 정도 하시면 좋을 것 같습니다.

남의 코드를 테스팅할 때 중점적으로 고려한 사항은 다음과 같습니다.

1. 남의 코드의 인터페이스는 손대지 않는다.
2. 남의 코드의 주 알고리즘은 손대지 않는다.


물론 2는 어쩔 수 없이 손대야 할 경우도 생기긴 합니다만, 가급적 그러지 않는 것이 정신건강상 이롭습니다. 왜 그런지는 아마 여러분들도 잘 아실 거라 생각합니다.

그러면 이제 실제 사례를 살펴보겠습니다. ConnectionManager라는 클래스입니다. 이 클래스는 대략 다음과 같은 골격을 가지고 있습니다.

public class ConnectionManager extends Thread implements Definition {
  ...
 
  ConnectionManager(PolicyAgent pa) {
  ...
  }
  ...

  public void connect() {
    Connection conn = null;
    // main algorithm
    ...
    conn = new Connection(this.policyAgent.primaryServer, this.svrPort, ...);
    ...
  }

  ...
}

이 코드를 테스트하는 데 있어서 가장 큰 문제는, 내부에서 생성하고 관리하는 Connection 객체가 어딘가로 TCP/IP 연결을 시도한다는 점이었습니다. 코드를 단위테스트하는 와중에는 해당 서버가 없었기 때문에, 우리는 Connection 객체를 어떻게든 'mock' 해야만 했습니다. 그런데 생성하는 코드가 주 알고리즘에 뒤섞여있었기 때문에 애매한 상태였죠.

그래서, 우선은 주 알고리즘에서 Connection 객체를 생성하는 코드를 별도의 메소드로 분리하고, 알고리즘 안에 포함되어 있던 new Connection 부분을 전부 createConnection을 호출하도록 변경했습니다.

public class ConnectionManager extends Thread implements Definition {
  ...
 
  ConnectionManager(PolicyAgent pa) {
  ...
  }
  ...

  protected Connection createConnection(String svr, int port, int type) {
    return new Connection(svr, port, type);
  }

  public void connect() {
    Connection conn = null;
    // main algorithm
    ...
    conn = createConnection(this.policyAgent.primaryServer, this.svrPort, ...);
    ...
  }

  ...
}

이렇게 한 다음에, JUnit 코드 안에서 ConnectionManager 클래스 코드를 계승하여 ConnectionManagerT라는 클래스를 정의했습니다.

class ConnectionManagerT extends ConnectionManager {
 private List<Connection> extConns = null;

 public ConnectionManagerT(PolicyAgent parent) {
  super(parent);
  this.extConns = new LinkedList<Connection>();
 }

 void addExternalConnection(Connection extConn) {
  synchronized( this.extConns ) {
   this.extConns.add( extConn );
  }
 }

 private Connection getExternalConnection() {
  synchronized( this.extConns ) {
   if ( this.extConns.isEmpty() ) return null;
   return this.extConns.remove(0);
  }
 }

 protected Connection createConnection(String svr, int port, int type) {
  Connection conn = null;
  if ( (conn = getExternalConnection()) == null ) {
   return super.createConnection(svr, port, type);
  }
  return conn;
 }
 ...
}

그리고 이 클래스 안에서 createConnection메소드를 오버라이드 했습니다. 이제 테스트 코드에서는 ConnectionManager 클래스를 사용하는 대신, ConnectionManagerT 클래스를 사용해서 테스트를 진행하면 됩니다. ConnectionManager 클래스에 정의된 알고리즘이 createConnection을 호출하는 순간, 하위 클래스에 정의된 createConnection이 호출될 것이고, 이 메소드는 new Connection을 하는 대신 getExternalConnection 메소드를 호출하여 connection을 얻을 것이기 때문에, mock 객체를 통해 주 알고리즘의 테스트가 진행되도록 만들 수 있습니다. setExternalConnection 메소드를 사용해 mock 객체를 ConnectionManagenrT 객체에 세팅해두면 되는 것이죠.

그러니까 테스트 케이스 코드 안에서는 다음과 같은 짓을 순차적으로 하면 된다는 겁니다.

  PolicyAgent pa = new PolicyAgent();
  pa.initiate();

  ConnectionManagerT cm = new ConnectionManagerT(pa);

  ...

  final Connection connMock1 = context.mock( Connection.class );
  cm.addExternalConnection( connMock1 );

  //
  // test no-signature error scenario
  //

  context.checking( new Expectations() {{
   ...
  }});

  cm.run(); // 알고리즘 테스트

  ... // 수많은 assertion 코드들...

어떻습니까? 이렇게 하면 남의 코드를 몇줄 고치지 않고서도 테스트를 진행할 수 있습니다. new Connection 때리는 부분을 별도의 overridable 메소드로 분리하는 것 만으로, 테스트 가능성을 높인 코드를 만들 수 있다는 이야깁니다.

이런 가능성은 TDD 형태로 테스트 코드를 먼저 작성하건, 아니면 작성된 코드에 테스트 코드를 추가하건, 순서 불문하고 테스트 코드를 작성하느라 고민하지 않으면 좀처럼 열리지 않습니다. 의외로 테스트 가능한 코드를 만드는 것은 아주 간단한 리팩토링(Refactoring)으로부터 시작되는데도 말이죠.

신고
Posted by 이병준

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

Extremely Agile/TDD2007.09.26 22:11
HttpUnit에 대한 글을 쓴지도 얼마되지 않았는데, 웹 서핑을 하다보니 또다른 툴을 만나게 되는군요.

아래의 그림은 JWebUnit 웹사이트 (http://jwebunit.sourceforge.net/index.html) 에서 가져온 아키텍처 그림입니다. JWebUnit 역시 HttpUnit과 마찬가지로 JUnit에 기반하여 만들어진 단위 테스트 프레임워크임을 알 수 있습니다. 흥미로운 것은 플러그인 구조를 채택해서 확장가능토록 구성된 점인데, 현재로서는 HtmlUnit 플러그인만 제공됩니다. ^^;

자바 스크립트 지원이 어느 정도까지 가능한지는 아직 잘 모르겠습니다. 솔직히 웹 브라우저와 직접 연동해서 테스트를 해 주는 Selenium과 같은 솔루션이 아니라면 자바 스크립트 엔진을 직접 구현하는 테스트 솔루션이 되어야 하는데, 그런 정도까지는 무리지 않을까, 하는 생각도 드네요.

JWebUnit 아키텍처

JWebUnit 아키텍처



public class WebIntegrationTest extends net.sourceforge.jwebunit.WebTestCase {

    public void testIndex() {
        beginAt("/index.html");
        assertTextPresent("Hello world");
    }

    private org.mortbay.jetty.Server server;

    protected void setUp() throws Exception {
        server = new org.mortbay.jetty.Server(0);
        server.addHandler(
                new org.mortbay.jetty.webapp.WebAppContext("src/main/webapp", "/my-context"));
        server.start();

        int actualPort = server.getConnectors()[0].getLocalPort();
        getTestContext().setBaseUrl("http://localhost:" + actualPort + "/my-context");
    }
}

위의 코드를 보시면 아시겠습니다만, assertTextPresent같은 재미있는 assert 함수들을 많이 제공합니다. 이런 함수들을 통해서 웹 페이지에 대한 제약사항들을 검사할 수 있습니다. 위의 예제 코드는 http://www.brodwall.com/johannes/blog/2006/12/10/in-process-web-integration-tests-with-jetty-and-jwebunit/ 에서 가져왔습니다.


신고
Posted by 이병준

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

Extremely Agile/TDD2007.09.25 02:00
방금 웹 서핑을 하다가 HttpUnit에 대한 글을 잠깐 읽었습니다. 블로그들에는 없는 내용이 없군요 :-P

http://httpunit.sourceforge.net/

위의 URL이 httpunit의 주소입니다. jUnit과 함께 사용해야하는, Java 기반의 solution입니다.

Cookbook 문서를 읽어보면 알수 있는 것입니다만 (아래의 글은 Cookbook 문서를 보고 간단히 요약한 것입니다) HttpUnit 프레임워크는 WebConversion 클래스를 사용해서 웹 브라우저가 만드는 HTTP conversion을 흉내냅니다.

WebConversation wc = new WebConversation();
WebRequest     req = new GetMethodWebRequest( "http://www.meterware.com/testpage.html" );
WebResponse   resp = wc.getResponse( req );

WebResponse 객체를 받아온 다음에는 getText() 메소드를 불러서 텍스트 형태의 프로세싱을 할 수도 있고, getDOM() 메소드를 호출해서 DOM 기반의 조작을 할 수도 있습니다. (이 편이 좀 더 편하긴 하겠네요.)

다음 처럼 하면 링크를 따라가는 것도 가능합니다.

WebConversation wc = new WebConversation();
WebResponse resp =
            wc.getResponse( "http://www.httpunit.org/doc/cookbook.html" ); // read this page
WebLink link = resp.getLinkWith( "response" );                              // find the link
link.click();                                                                               // follow it
WebResponse jdoc = wc.getCurrentPage();                                  // retrieve the referenced page

폼 프로세싱이나 테이블 형태의 자료 처리도 가능합니다. 폼 프로세싱 예제만 살펴보면,

WebForm form = resp.getForms()[0];      // select the first form in the page
assertEquals( "La Cerentolla", form.getParameterValue( "Name" ) );
assertEquals( "Chinese",       form.getParameterValue( "Food" ) );
assertEquals( "Manayunk",      form.getParameterValue( "Location" ) );
assertEquals( "on",            form.getParameterValue( "CreditCard" ) );

위와 같이 해서 폼의 기본 파라메타 값이 어떻게 만들어져있는지 검사할 수도 있고,

form.setParameter( "Food", "Italian" );      // select one of the permitted values for food
form.removeParameter( "CreditCard" );         // clear the check box
form.submit();                                // submit the form

폼에 포함된 컨트롤들에 값을 넣어서 날릴 수도 있습니다.

웹 기반의 인터페이스 테스트를 하는 데 굉장히 유용할 것 같네요. :-) 하지만 이 프레임워크의 가장 큰 약점은...

자바스크립트 지원이 아직은 기본적인 수준이다

는 점이 되겠군요 -_-;


다들 알고 있는데 저만 모르는 사실들이 아주 많은것 같아 좀 우울합니다 ㅋㅋ



신고
Posted by 이병준

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

  1. 시월애

    잘보고갑니다. ^^
    많은도움얻고갑니다 ㅎ

    2008.03.24 13:38 신고 [ ADDR : EDIT/ DEL : REPLY ]
  2. 이것은 위대한이며 많은 사람들이이 공유를 주셔서 감사합니다 사랑 할거야 확신

    2011.11.22 04:12 신고 [ ADDR : EDIT/ DEL : REPLY ]