본문 바로가기

Dev.../웹서비스

[펌] Test Driven Development & Eclipse - V

Cactus를 이용한 Http 테스트

 

앞에서 JUnit을 이용하여 비즈니스 Layer에 대한 테스트를 진행하였다. 하지만 JUnit은 EJB, Servlet 등과 같이 Container 상에서 작동하는 클래스들에 대한 테스트는 어렵다. Servlet, JSP, Session Bean Entity Bean 등은 반드시 해당 Container가 있어야지만 수행이 가능하고 이러한 환경하에서만 테스트가 가능하다.

이렇게 서버 측에서 운영되는 프로그램에 대한 테스트를 지원하는 것이 apache의 cactus이다.

 

실제 개발자들이 테스트하기 어렵게 여기는 부분도 웹브라우저에서의 사용자의 Action에 따라 시스템의 각 기능이 정상적으로 동작하는지에 대한 테스트이다.

 

Http와 관련된 사항에 대한 테스트를 수행하는 원리는 테스트를 위한 framework(여기서는 cactus)에서 HttpRequest를 강제적으로 생성하여 Container로 해당 request를 전송 시켜 테스트를 수행한다. 이때 HttpRequest에 테스트할 request parameter를 설정할 수 있으며,  수행된 결과인 HttpResponse에 대한 검증이 가능하다.

더 나아가 JSP 페이지에서 생성된 HTML 코드에 대한 검증도 가능하기 때문에 전체 웹 시스템에 대해 비즈니스 로직뿐만 아니라 화면 부분까지 자동화된 테스트가 가능하게 된다.

 

cactus 다운로드 및 설치

 

cactus는 apache의 http://jakarta.apache.org/cactus/index.html에서 자세한 정보를 얻을 수 있다. cactus는 명령행으로 실행시키는 모드와 eclipse plugin 두가지 모드를 지원하고 있으나 현재 eclipse plugin은 정식으로 release 된 상태는 아니다.

cactus의 eclipse plugin 관련 문서는 다음 url에서 찾아 볼 수 있다.

 

http://jakarta.apache.org/cactus/integration/eclipse/index.html

 

eclipse plugin의 다운로드는 nightly build에서 다운로드 가능한데 현재 cactus는 계속 진행 중인 프로젝트 이기 때문에 계속해서 버전이 바뀌고 있어 최신 버전을 다운로드 받으면 된다. http://cvs.apache.org/builds/jakarta-cactus/nightly/ 페이지에서 최근의 파일을 다운로드 받아 설치하면 된다.

위의 페이지에는 ant를 지원하는 모드와 eclipse를 지원하는 모두 같이 있다. 여기서 모두 다운로드 받은 다음 jakarta-cactus-eclipse-runner-<build version>.zip 파일과 jakarta-cactus-eclipse-webapp-<build version>.zip 두 파일의 압축을 해제한 다음 eclipse의 pulgin 디렉토리에 복사하면 설치는 끝난다.

나머지 한 파일은 jakarta-cactus-13-1-<build version>.zip 파일은 cactus 테스트 프로그램 컴파일 시 필요한 파일들이 있기 때문에 일단 적당한 디렉토리에 압축을 푼 상태로 둔다.

cactus가 제대로 설치 되었는지 여부는 eclipse에서 Windows → Preferences 메뉴에서 아래 화면과 같이 Cactus에 대한 설정이 나타나면 제대로 설치가 된 것이다.

 

 

Cactus 설정에서 사용하고 있는 서버 Container의 Home 경로를 지정해 준다. 필자의 경우 Tomcat를 사용하였기 때문에 Tomcat 부분에 Tomcat의 Home 디렉토리를 설정해 주었다.

 

이제 프로젝트로 돌아와서 프로젝트의 CLASSPATH에 이 cactus를 추가하여야 한다. 이때 조금 주의해야 할 사항이 있는데 cactus에서 사용하고 있는 junit의 버전과 CLASSPATH상에 있는 junit의 버전이 틀릴 경우 컴파일 시 에러가 발생할 수 있다. 이유는 cactus의 여러 클래스들은 junit의 클래스를 상속 받아 사용하고 있는데 이들에 super클래스의 생성자 등에서 버전에 따라 틀릴 경우 제대로 컴파일이 되지 않는다. 앞의 예제에서는 JUnit의 junit.jar 파일을 CLASSPATH 상에 설정하여 컴파일 하였는데 cactus를 이용하게 되면 cactus 다운로드시 다운로드 받은 junit.jar 파일을 이용하여야 한다. 이 파일은 cactus의 eclipse plugin에 있는 것이 아니라 파일은 jakarta-cactus-13-1-<build version>.zip 압축을 해제한 디렉토리 아래에 lib 디렉토리에 존재한다.

 

 

추가로 common-logging-1.0.jar 파일도 추가로 jar 파일에 등록한다.

(위의 cactus 설치 사항은 2003.07월 기준으로 작성된 것이기 때문에 그 동안의 변경 사항에 대해서는 cactus 사이트에서 참고를 하고 설치 사항이 변경된 것이 있는지 확인학 설치하기 바란다.)

 

cactus 동작원리

 

JUnit은 setUp(), testXXX(), tearDown() 순으로 동작하여 초기 값 설정 등을 setUp() 메소드에서 처리하게 하였지만 Http관련 테스트를 수행하기 위해서는 Session의 설정, HttpRequest 에 파라미터 추가 등의 작업을 해주어야지만 원하는 테스트를 수행할 수 있다.

Cactus는 이러한 HTTP의 특성을 테스트에서 처리하기 위해 다음과 같은 순서로 동작하게 된다.

 

beginXXX() 메소드의 XXX는 테스트할 메소드의 명칭을 의미한다. 예를 들어 testBoardList()라는 테스트 메소드가 있다면 startBoardList() 가 될 것이다.

beginXXX(), endXXX() 메소드의 파라미터로 WebRequest, WebResponse가 넘겨져 오는데 이 파라미터를 이용하여 Request 정보를 설정한다.

다음은 cactus doc 문서를 번역한 것인데 요약해서 설명하면 Client Side에서 개발자가 작성한 테스트 클래스의 객체를 생성하여 beginXXX() 메소드를 수행한 다음 cactus에서 제공하는 proxy 서버에 Http 접속을 하여 beginXXX()에서 설정한 값들을 proxy로 전송한다. proxy에서는 테스트 클래스를 객체화하여 setUp, testXXX, tearDown 메소드를 수행하고 결과를 다시 Request, Response에 담아 Client Side로 보내는 방식으로 테스트가 수행된다.

1.       JUnit은 YYYTestCase의 runTest() 메소드를 호출한다.

runTest() 메소드는 beginXXX(WebRequest) 메소드가 있으면 이 메소드를 호출한다. 이 beginXXX(WebRequest) 메소드는 Server Container가 아인 독립적인 clinet side(별도의 JVM)에서 수행된다. beginXXX() 메소드에는 WebRequest아는 파라미터가 전돨되는데 이것은 HTTP Header, Parameter 등을 설정하여 step2에서 proxy로 전송하는데 사용되어 진다.

2.       YYYTestCase.runTest() 메소드는 Redirector proxy에 HTTP 접속을 한다. beginXXX() 메소드에서 설정된 모든 파라미터는 이 HTTP request에 담겨 전송된다.

3.       Redirector proxy는 테스트 클래스에 대해 server side 에서 proxy같이 작동한다. 이것은 테스트 클래스가 두 번 객체화 되는 것을 말하는데 한번은 Junit 의 Test Runner에 의해 client side에서, 한번은 이 Redirector proxy에 의해 server side에서 발생한다. client side의 instance는 beginXXX()와 endXXX()을 수행하는데 사용되고 server side의 instance는 testXXX() 메소드를 수행하는데 사용된다.

4.       테스트 클래스의 setUp(), testXXX(), tearDown() 순서로 수행된다. 이들 메소드는 reflection(자바 API중 메소드 명, 멤버변수 등과 같은 클래스 정보를 가져오는 API)을 이용하여 Redirector proxy에 의해 호출된다. 물론 setUp(), tearDown()는 optional 이다.

5.       testXXX() 메소드는 server side의 테스트를 수행하는 코드를 호출하게 되는데 여기에는 테스트를 수행하고 JUnit의 assert API를 이용하여 수행결과를 검증한다.

6.       테스트가 실패하면 testXXX() 메소드는 exception 을 발생시킨다.

7.       exception이 발생하면 Redirector proxy는 이 exception에 대한 정보를 client side로 보낸다. 이 exception에 대한 정보는 JUnit에 의해 console에 나타난다.

8.       예외가 발생하지 않은 경우 YYYTestCase.runTest() 메소드는 endXXX(org.apache.cactus.WebResponse) 또는 endXXX(com.meterware.httpunit.WebResponse)- HttpUnit을 이용하는 경우-를 찾아 있는 경우 이를 수행한다. 이 단계에서 cactus나 JUnit의 assert API를 이용하여 response의 HTTP header, cookie, output stream 의 값을 검증할 수 있다.

cactus 를 이용한 Servlet 테스트(Front Controller)

 

Cactus는 Servlet, JSP, Filter, TagLib. EJB에 대해 테스트 기능을 지원한다. 여기서는 Servlet에 대한 테스트에 대해서 알아보기로 하자. Cactus의 환경설정, 테스트 작성방법, 테스트 수행방법만 알면 나머지에 대해서는 동일한 방법에 대해 수행하면 된다.

요즘 J2EE를 이용하여 웹 시스템을 구축할 때 Servlet을 사용하는 경우는 거의 드물다. 일반적인 화면의 경우 대부분 JSP로 처리하기 때문이다. 하지만 예제의 경우 JSP Model2 구조로 되어 있기 때문에 Front Controller를 사용하여 모든 사용자의 요청은 한곳에 받고 각각의 사용자의 요청에 따른 처리는 Action 단위로 클래스를 구성하여 처리하도록 하였다.

이때 이 Front Controller 부분이 Servlet으로 구성되어 있다(Action이라는 용어 대신 Command라는 용어를 사용하였다. 구조에 대한 자세한 설명은 필자의 글 JSP Model2를 이용한 웹 시스템 구축 사례를 참조하기 바란다.).

이러한 구조에서 웹 Layer에서 가장 먼저 만드는 것이 Front Controller라 할 수 있다. 일반적으로 이 Front Controller는 전체 시스템에 하나 또는 몇 개만 존재하기 때문에 한번 작성되면 거의 수정사항이 발생하지 않는다.

필자의 경우 테스트를 다음과 같은 단위로 작성하였다. Front Controller의 기능을 테스트하기 위한 Tester를 구성한 다음 정상적으로 원하는 Command를 생성시키고, 처리 결과를 제대로 forward 시키는지 여부에 대해 테스트를 수행하였다. 그리고 각각의 Command에 대해 하나의 Tester 클래스를 만들었다. 이렇게 구성하다 보니 Tester 클래스가 너무 많이 생겼다. 하지만 개발 후 지속적으로 관리(유지보수)를 하기 위해서는 특정 단위로 나누어져 있어야지만 편리하기 때문에 이러한 방식을 택했다.

 

앞에서 JUnit을 이용하여 테스트 프로그램을 작성할 때 TestCase를 상속받아 테스트 프로그램을 작성하였다. Cactus에서 Servlet 테스트 프로그램을 만들기 위해서는 ServletTestCase 클래스를 상속받아 구현하면 된다.

비즈니스 레이어 측의 테스트를 수행하기 위해 패키지로 tdd.test.board 라는 패키지를 만들었는데 웹 계층을 테스트하기 위해 tdd.test.board.web 이라는 패키지를 만들어 여기에 웹 Layer관련 테스트 클래스들이 위치하도록 하였다.

 

 

Front Controller는 HttpRequest로부터 사용사의 처리 요청을 받아 특정 Action(Command) 객체를 생성시켜 이 객체를 수행 시키고 결과로 받은 forward 정보를 이용하여 결과 페이지로 forward 시키는 기능을 수행한다. 이러한 기능에 대한 테스트 코드를 먼저 작성한다.

 

TestCommandServletTest.java

package tdd.test.board.web;

 

import java.io.IOException;

 

import junit.framework.Test;

import junit.framework.TestSuite;

 

import org.apache.cactus.ServletTestCase;

import org.apache.cactus.WebRequest;

import org.apache.cactus.WebResponse;

 

import tdd.board.BoardServlet;

 

public class BoardServletTest extends ServletTestCase

{

public BoardServletTest(String name)

{

     super(name);

}

////////////////////////////////////////////////////////////////////////////

//Command 대한 처리 부분 테스트

public void beginGetCommand(WebRequest theRequest)

{

     theRequest.addParameter("command", "tdd.board.command.BoardDummyCommand");

}

public void testGetCommand()

{

       BoardServlet servlet = new BoardServlet();

       String command = servlet.getCommand(request);

            

       assertEquals("tdd.board.command.BoardDummyCommand", command);

}

////////////////////////////////////////////////////////////////////////////

 

////////////////////////////////////////////////////////////////////////////

//Dispatcher 기능에 대한 부분만 테스트 

public void testRequestDispatcherForward() throws Exception

{

       BoardServlet servlet = new BoardServlet();

       servlet.init(config);

       servlet.forwardNextUrl(request, response, "/jsp/test.jsp");

}

 

public void endRequestDispatcherForward(WebResponse theResponse)

throws IOException

{

       assertTrue("처리결과 This is Test 라는 문장이 있어야함[" +

theResponse.getText() + "]" ,

theResponse.getText().indexOf("This is Test") >= 0);

}

////////////////////////////////////////////////////////////////////////////

 

////////////////////////////////////////////////////////////////////////////

//Servlet service 대한 처리 테스트

public void beginService(WebRequest theRequest)

{

       theRequest.addParameter("command", "tdd.board.command.BoardDummyCommand");

}

public void testService() throws Exception

{

       BoardServlet servlet = new BoardServlet();

       servlet.init(config);

       servlet.service(request, response);

       assertNull(request.getAttribute("javax.servlet.jsp.jspException"));

       assertEquals("Dummy Command Message",

 (String)request.getAttribute("message"));

}

public void endService(WebResponse theResponse)

{

       //Forward <