2010. 4. 21. 13:49
Mock 객체를 이용한 TDD
2010. 4. 21. 13:49 in JAVA
- 백명석(기술위원회), 2007년 1월
- 객체지향으로 어플리케이션을 개발하다 보면 특정 객체를 구현하다가 그 객체가 종속성을 갖는 다른 객체들이 아직 완성되지 않아서 몰두하여 구현하던 객체를 잠시 버려두고 종속성을 갖는 다른 객체를 구현해야 하는 경우가 발생한다. 만일 종속성을 갖는 객체가 다른 개발자나 팀에 의해 개발되어야 한다면 기다려야만 한다는 더 안 좋은 문제가 발생한다.
- 이 노트에서는 이러한 경우 종속성을 갖는 객체를 Mock 객체로 대체하여 개발자가 현재 자신이 몰두하고 있는 객체에만 전념하여 개발을 진행할 수 있는 방법을 알아본다.
TDD와 Mock 객체 #
이 절에서는 OOPSLA에 발표된 논문(Mock Roles, not Objects, http://www.jmock.org/oopsla2004.pdf) 내용을 참조하여 왜 TDD를 적용하여 어플리케이션을 개발할 때 Mock 객체가 필요한지에 대해 알아본다.
OOP의 특성 #
OOP로 개발한 어플리케이션은 유연성(이해, 재사용, 유지보수) 을 제공한다. 이러한 유연성은 그림 1과 같이 서로에게 메시지를 전달함으로써 공동 작업을 하는 객체들의 집합으로 어플리케이션이 개발됨으로써 내부적으로 응집력이 높고 시스템의 다른 부분들과는 최소한의 결합도를 갖는 모듈 구조를 가졌을 때 가능해진다.
. 그림 1 공동 작업을 수행하는 객체들의 집합
그림 1에서 잘 정의된 시스템의 객체들은 OOP의 유연성을 달성하기 위해 자신과 밀접한 객체(immediate neighbors)들에게만 메시지를 보내야 한다. 이 원칙이 지켜졌을 때 객체를 객체가 제공하는 서비스와 객체가 필요로 하는 서비스(인접 객체가 제공하는 서비스)로 기술할 수 있다. TDD 그리고 Mock 객체의 필요성 #
TDD는 어플리케이션을 작성할 때 어플리케이션을 구현하기에 앞서 테스트를 먼저 작성하는 개발 스타일이다. 테스트 작성에서 중요한 것은 대상 테스트 기능의 성공 여부를 확인하는 것이다. 일반적으로 테스트 결과 값(객체의 상태)을 조사함으로써 성공 여부를 확인할 수 있다. 우리가 객체의 상호작용에 전념할 수 있으면 객체가 제공하는 서비스 중 하나를 호출하고 결과 값(상태)을 조사하는 대신 다른 객체들과의 상호작용을 조사함으로써 객체를 테스트할 수 있다.
예를 들어 Dog라는 객체에 expressHappiness라는 메소드가 있고 이 메소드가 성공적으로 수행되기 위해서는 DogBody.wagTail이라는 메소드를 호출해야 한다고 가정하자. 이때 테스트에서 expressHappiness의 결과 값을 조사하는 대신 Dog.expressHappiness 메소드의 구현에서 DogBody.wagTail()을 호출하였는지 조사하여 성공 여부를 결정할 수 있다. 만일 DogBody 객체에 아직 wagTail() 메소드가 구현되어 있지 않으면 어떻게 해야 할까? 이때 개발자는 새로운 기능(DogBody.wagTail)을 구현하기 위해 현재 개발 중인 기능(expressHappiness) 구현을 멈추기를 원하지 않는다. 왜냐하면 이러한 멈춤은 현 작업에서 주의를 분산시키기 때문이다. 특히 wagTail() 메소드 구현 시 또 다른 요구사항이 발견된다면 이러한 분산은 더 심해진다. 대신 DogBody에 대한 Mock 객체를 도입하여 wagTail() 메소드가 호출되는지 확인할 수 있다면 우리의 요구는 만족된다. 이러한 이유에서 Mock 객체가 TDD에서 유용한 존재가 된다.
요약하면 객체를 테스트할 때 종속성을 갖는 인접 객체들을 Mock 객체로 대체하여 테스트를 수행한다. OOP에서 우리는 객체가 제공하는 서비스를 호출하고 결과 값을 조사하는 대신 결과적으로 나타나는 인접 객체들과의 상호작용을 조사함으로써 객체를 테스트할 수 있다.
예제 어플리케이션 #
이 노트에서는 하나의 테이블(BOARD)에 데이터를 생성, 검색하는 아주 간단한 웹 어플리케이션을 예로 TDD와 Mock 사용 방법 및 장점에 대해 알아본다. 예제로 사용한 어플리케이션은 다음과 같은 패키지 구조를 갖는다.
. 그림 2 예제 어플리케이션의 패키지 구조
각 패키지는 다음과 같은 역할을 담당한다. - web: 브라우저를 통한 사용자 요청 해석, 서비스 객체로 위임(delegate), 결과 페이지 출력
- service: 클라이언트(web)의 요청 처리를 위해 dao와 domain 객체들을 코디네이트(coordinate)
- dao: domain 객체들에 대한 데이터베이스 영속성(persistency - CRUD) 처리
- domain: 요구사항의 개념(concept)들에 대한 모델링 결과로써 업무 로직, 규칙을 구현
- 예제에서 비즈니스 계층 구현을 위해 Spring과 iBatis를 사용한다.
- 예제에서 Mock 테스트 라이브러리로 jMock(http://www.jmock.org/)을 사용한다.
Mock 객체를 이용한 TDD #
지금부터는 테이블에 데이터를 생성하는 하나의 기능을 예로 TDD를 적용하여 개발하는 과정을 살펴보고, 각 과정에서 TDD와 Mock 객체가 어떤 이점을 제공하는지 알아본다. Mock 객체를 사용했을 때 우리는 직관적인 방식인 Top-Down 방식을 사용할 수 있다는 추가적인 이점을 얻는다. 그림 2에서 우리의 최상위로부터 web, service, dao, domain 패키지를 갖는다. Mock 객체를 사용함으로써 유스케이스의 기능을 구현할 때 최상위 패키지부터 TDD를 적용하여 개발을 진행할 수 있다. 이제부터 각 패키지를 Mock을 적용한 TDD로 개발해 보도록 한다. 개발은 서비스 패키지부터 시작을 하고 최종적으로 웹 패키지를 구현한다.
서비스 패키지 구현 #
먼저 jMock을 이용하여 아래와 같이 테스트를 작성한다.
package net.daum.feedback.service; import org.jmock.cglib.Mock; import org.jmock.cglib.MockObjectTestCase; import net.daum.feedback.domain.Board; import net.daum.feedback.dao.BoardDao; import net.daum.feedback.exception.NullTitleException; public class ManageBoardServiceMockTest extends MockObjectTestCase { private Mock mockBoardDao; private BoardDao boardDao; protected void setUp() throws Exception { super.setUp(); mockBoardDao = new Mock(BoardDao.class); boardDao = (BoardDao) mockBoardDao.proxy(); } public void testCreateWithNotNullTitle() { String title = "title"; String content = "content"; Board board = new Board(title, content); mockBoardDao.expects(once()) .method("insert") .with(eq(board)) .will(returnValue(board)); ManageBoardService manageBoardService = new ManageBoardServiceImpl(boardDao); try { Board newBoardArticle = manageBoardService.createBoard(board); assertNotNull(newBoardArticle); } catch (NullTitleException e) { e.printStackTrace(); } } }
Kent Beck의 "Test Driven Development: By Example"에도 나와 있듯이 위와 같은 테스트는 한번에 작성된 것이 아니다. 조금 작성하고 컴파일, 테스트하고, 다시 조금 작성하는 반복적인 작업의 결과이다. 이 노트에서는 TDD에 대한 설명이 아니라 Mock 객체 사용에 주안을 두고 있기에 이 과정은 생략되었다.
위 코드에서 중요한 부분은 Mock 객체 사용을 위해 설정하는 setUp() 메소드와 실제 Mock 객체를 사용하여 테스트를 수행하는 testCreateWithNotNullTitle() 메소드이다.
setUp() 메소드에서는 BoardDao라는 인터페이스에 대해 Mock 객체를 생성하고 Mock 객체에 proxy() 메소드를 호출하여 Mock 객체에 대한 proxy 객체를 얻는다.
testCreateWithNotNullTitle() 메소드에서 먼저 dao 객체를 대체하는 mock 객체에 기대 값을 설정한다(expects 메소드를 통해). 즉 dao의 “insert”라는 메소드가 한번 호출되고, 파라미터로는 board 객체를 받고, 결과 값으로 board 객체를 반환해야 테스트가 성공한다는 것을 설정한다. 그리고 실제 dao 구현 없이 테스트를 수행하면서 서비스 객체를 완성한다. 먼저 아래와 같이 위에서 작성한 테스트가 컴파일이 성공하도록 서비스를 구현한다.
package net.daum.feedback.domain; import java.util.List; import org.apache.commons.lang.StringUtils; import net.daum.feedback.exception.NullTitleException; public class ManageBoardServiceImpl implements ManageBoardService { private BoardDao boardDao; public ManageBoardServiceImpl(BoardDao boardDao) { this.boardDao = boardDao; } public Board createBoard(Board board) throws NullTitleException { return board; } }
테스트를 수행하면 아래와 같은 오류가 발생한다.
junit.framework.AssertionFailedError: mock object mockBoardDao: expected method was not invoked expected once: insert( eq(<net.daum.feedback.domain.Board@defa1a[ id=<null> title=title content=content ]>) ), returns <net.daum.feedback.domain.Board@defa1a[ id=<null> title=title content=content ]>
서비스의 메소드에서 dao의 insert 메소드를 호출하도록 아래와 같이 수정한다.
public Board createBoard(Board board) throws NullTitleException { boardDao.insert(board); return board; }
다시 테스트를 수행하여 서비스 객체가 테스트에 정의된 대로 올바르게 동작하는지 확인한다.
DAO 패키지 구현 #
지금까지 서비스 객체를 구현하였다. 이제 서비스 객체가 종속성을 갖는 dao 객체를 서비스 객체와 동일한 방법으로 구현해 보도록 하자. 먼저 아래와 같이 테스트를 작성한다.
package net.daum.feedback.dao; import net.daum.feedback.domain.Board; import org.jmock.cglib.Mock; import org.jmock.cglib.MockObjectTestCase; import org.springframework.orm.ibatis.SqlMapClientTemplate; public class BoardDaoIbatisImplMockTest extends MockObjectTestCase { private Mock mockSqlMapClientTemplate; private BoardDao boardDao; private Board board; protected void setUp() throws Exception { super.setUp(); mockSqlMapClientTemplate = new Mock(SqlMapClientTemplate.class); SqlMapClientTemplate sqlMapClientTemplate = (SqlMapClientTemplate)mockSqlMapClientTemplate.proxy(); board = new Board(); boardDao = new BoardDaoIbatisImpl(sqlMapClientTemplate); } public void testFindBoard() { Long boardId = new Long(100); mockSqlMapClientTemplate.expects(once()) .method("queryForObject") .with(eq("Board.findBoard"), eq(boardId)) .will(returnValue(board)); Board result = boardDao.findBoard(boardId); assertEquals(board, result); } }
테스트에서처럼 dao 객체가 종속성을 갖는 객체는 SqlMapClientTemplate이라는 객체이다. 이 객체는 iBatis에 대한 Spring의 Wrapping 클래스로써 Spring을 사용한 어플리케이션에서 iBatis를 쉽게 사용할 수 있도록 해준다. 위의 코드도 서비스 테스트에서처럼 setUp() 메소드에서 종속성을 갖는 객체에 대한 Mock 객체와 Proxy를 생성하고 test 메소드에서 기대 값을 설정하고 테스트 대상 서비스를 호출하여 테스트를 수행한다.
웹 패키지 구현 #
웹 패키지 구현을 위해 이 노트에서는 struts를 사용한다. struts를 위한 mock 테스트 라이브러리로 StrutsTestCase(http://strutstestcase.sourceforge.net/)라는 것이 있다. StrutsTestCase는 서블릿 엔진에 대한 Mock 객체를 제공하여 Struts Action 객체를 서블릿 엔진 없이 테스트할 수 있도록 해준다. 아래 코드는 StrutsTestCase를 이용하여 작성한 테스트이다.
package net.daum.feedback.web; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import net.daum.feedback.domain.Board; import servletunit.struts.MockStrutsTestCase; public class ManageBoardActionTest extends MockStrutsTestCase { private static final Log log = LogFactory.getLog(ManageBoardActionTest.class); String title = "title-01"; String content = "content-01"; protected void setUp() throws Exception { super.setUp(); } public void testCreateBoard() { setRequestPathInfo("/createBoard"); addRequestParameter("board.title", title); addRequestParameter("board.content", content); addRequestParameter("method", "createBoard"); actionPerform(); verifyForward("boardCreateConfirmed"); assertEquals(title, ((Board) getSession().getAttribute("board")) .getTitle()); assertNotNull(((Board) getSession().getAttribute("board")) .getId()); verifyNoActionErrors(); } }
Struts Action은 서비스 객체에 종속성을 갖는다. StrutsTestCase가 이러한 종속성도 Mock 객체로 대체해 줄 수 있으면 좋겠지만 서블릿 엔진을 대체해 주는 것만으로 서블릿 엔진에 웹 어플리케이션을 배포하지 않고, 또 JSP를 작성하지 않고도 테스트를 수행할 수 있다는 장점을 제공한다.
결론 #
TDD에는 여러 가지 이점이 있겠지만 개발자 입장에서 실제로 TDD를 사용하면서 느낌 가장 강력한 이점은 아래와 같다.
- 객체가 제공하는 서비스를 어떻게 사용하는지가 테스트에 기술됨으로써 서비스 객체와 이를 사용하는 클라이언트 객체 개발자들 간의 효율적인 의사 소통 수단으로 사용된다.
- 각 코드 부분에 대한 테스트가 선행되어 코드 소유권을 여러 사람이 가질 수 있도록 하여 코드의 품질이 보다 높아진다.
- Mock 객체는 TDD를 수행할 때 종속성을 갖는 객체가 접근 불가하거나 아직 구현되지 않았어도 서비스를 제공하는 객체를 테스트할 수 있도록 해준다. 이러한 Mock 객체의 기능은 다음과 같은 이점을 제공한다.
- TDD 기반으로 어플리케이션 개발 시 발견되는 종속성을 갖는 객체로 인한 주의 분산을 제거하여 개발자가 현재 구현하고 있는 객체에만 전념할 수 있도록 해준다.
- 상대적으로 많은 시간이 소요되는 객체(예. SqlMapClientTemplate, 서블릿 엔진)를 대체하여 짧은 시간 내에 테스트를 수행할 수 있도록 해준다.
- 테스트 결과 확인을 위해 상호작용을 확인함으로써 단순히 테스트가 아니라 상호작용 발견을 위한 설계 수단으로 사용될 수 있다.
참고 문헌 #
- jMock - A Lightweight Mock Object Library for Java,http://www.jmock.org/
- Mock Roles, not Objects, http://www.jmock.org/oopsla2004.pdf
- StrutsTestCase,http://strutstestcase.sourceforge.net
[출처] Mock 객체를 이용한 TDD|작성자 atonikkaz