Tlemock을 만들게 된 배경(Mock이 왜 필요한지 설명) TDD(Mock객체)

Tlemock을 이용하여 Mock 객체를 생성하고 사용하는 방법에 대해서 살펴본다.

Tlemock을 만들게 된 배경

하나의 소프트웨어는 수많은 객체들로 구성된다. 아주 간단한 웹 어플리케이션 조차도 MVC 패턴과 DAO 패턴을 사용할 경우, 3~4개의 객체를 필요로 한다. 각 객체들은 결합도(coupling)를 낮추기 위해 인터페이스를 이용해서 통신을 하게 되는데, 예를 들어, BoardService 인터페이스를 구현한 BoardServiceImpl 클래스는 BoardDao 인터페이스를 구현한 BoardDaoImpl 클래스에 의존하기 보다는 BoardDao 인터페이스에 의존함으로써 구현 객체 간의 결합도를 낮춘다.

    public interface BoardService {
        ...
    }
    
    public interface BoardDao {
        ...
    }
    
    public class BoardServiceImpl implements BoardService {
        private BoardDao boardDao;
        
        public Board createBoard(...) {
            ...
            boardDao.insert(..);
        }
    }
    
    public class BoardDaoImpl implements BoardDao {
        ...
    }

위와 같이 BoardServiceImpl 클래스가 BoardDao에 의존하고 있기 때문에, BoardServiceImpl 클래스를 테스트하려면 다음과 같이 BoardDao 인터페이스의 구현체를 생성자나 setter를 통해서 전달한 뒤에 테스트를 수행하게 될 것이다.

    public void testCreate() {
        BoardServiceImpl boardService = new BoardServiceImpl();
        boardService.setBoardDao(new BoardDaoImpl());
        
        Board createdBoard = boardService.createBoard(..);
        assertNotNull(createdBoard);
        ...
    }

프로젝트의 규모가 커지면 모든 레이어를 한명이 개발하기 보다는 각 레이어를 여러명의 개발자가 나눠서 작업을 진행하게 된다. 이런 경우, 내가 BoardServiceImpl을 구현하고 동안에 아래의 상항이 발생하곤 한다.

  • BoardServiceImpl에서 필요로 하는 Dao 인터페이스가 아직 없다.
  • BoardServiceImpl에서 필요로 하는 Dao 인터페이스는 있는데, 아직 인터페이스를 클래스가 존재하지 않는다.
  • BoardServiceImpl에서 필요로 하는 Dao 인터페이스를 구현한 클래스가 있는데, 아직 메소드가 완전히 구현되지 않았다.
즉, 구현과정에서 BoardServiceImpl이 의존하게 되는 인터페이스의 구현 클래스가 완벽하지 않은 상황이 발생할 수 있다. 이런 상황이 벌어질 때 BoardServiceImpl 클래스를 만들고 있던 개발자는 필요한 클래스가 완성되기 전까지는 BoardServiceImpl의 테스트를 수행할 수 없을 것이다. 이런 경우, BoardServiceImpl 클래스의 개발자는 필요한 클래스가 완성될 때 까지 놀고 있어야 할까? 아니면, 필요한 부분의 코드를 직접 만들어야 할까? 두 가지 방법 모두 개발자의 생산성을 떨어뜨리는 요소가 될 것이다. BoardServiceImpl 클래스의 개발자가 의존하는 클래스의 완성 여부에 상관없이 BoardServiceImpl 클래스의 테스트를 수행할 수 있다면 생산성을 향상시킬 수 있을 것이다. 바로 이런 상황을 해결하기 위해서 도입된 것이 Mock 객체이다.

기존 mock 라이브러리의 불편함

Mock 객체는 말 그대로 가짜 객체이다. 예를 들면, BoardServiceImpl 클래스는 BoardDao 인터페이스를 구현한 클래스가 있어야 테스트가 가능한데, 이때 가짜로 만든 BoardDao 구현 클래스를 사용해서 BoardServiceImpl 클래스를 테스트할 수 있도록 할 수 있다.

    BoardDao mockBoardDao = ... // 가짜 BoardDao 구현 객체 생성
    BoardServiceImpl service = new BoardServiceImpl();
    service.setBoardDao(mockBoardDao); // 가짜 객체 전달
    service.createBoard(...); // 내부적으로 가짜 객체가 사용됨
    

가짜 객체는 BoardServiceImpl이 BoardDao에게 기대하는 방식으로 동작을 함으로써 BoardServiceImpl이 정상적으로 실행될 수 있도록 돕게 된다. 가짜 객체를 만드는 방법은 크게 두가지 방식이 있다.

  • jMock, easymock 등의 Mock 라이브러리 사용
  • Mock으로 사용할 가짜 클래스 직접 구현
Mock 라이브러리는 현재 널리 사용되는 방식중의 하나이다. 예를 들어, 아래 코드는 jMock을 이용하여 Mock 객체를 생성하고 검증하는 코드의 예를 보여주고 있다.

    import org.jMock.Mockery;
    import org.jMock.Expectations;
    
    class BoardServiceImplTest extends TestCase {
        Mockery context = new Mockery();
    
        public void testOneSubscriberReceivesAMessage() {
            final BoardDao boardDaoMock = context.mock(BoardDao.class);
    
            BoardServiceImpl service = new BoardServiceImpl();
            service.setBoardDao(boardDaoMock);
            
            final Board board = ...;
            final Board returnBoard = ...;
            
            // expectations
            context.checking(new Expectations() {{
                one(boardDao).insert(board); will(returnValue(returnBoard));
                ...
            }});
    
            // execute
            Board createdBoard = service.createBoard(board);
            assertNotNull(createdBoard);
            ...
            
            // verify
            context.assertIsSatisfied();
        }
    }

jMock을 사용하면 BoardDao의 구현 클래스 없이 BoardServiceImpl을 테스트 할 수 있게 된다. jMock은 Mock 객체가 어떻게 동작해야 하는 지를 jMock의 문법을 이용해서 표시한다. 위 코드의 경우 boardDao Mock 객체의 insert 메소드에 board 객체가 전달되면, returnBoard 객체를 리턴하라고 지정하고 있다. 따라서, service.createBoard() 메소드가 내부적으로 BoardDao.insert() 메소드를 호출하면, 그 결과로 returnBoard 객체를 리턴받게 된다.

jMock은 또한 어떤 메소드는 1번 이상 호출되어야 하고, 어떤 메소드는 호출되어서는 안 된다는 등의 규칙도 지정할 수 있으며, 이를 검증하는 기능도 제공하고 있다. easymock 역시 jMock보다는 쉬운 방법으로 비슷한 기능을 제공하고 있어서, jMock이나 easymock을 사용하면 Mock 객체의 생성뿐만 아니라 검증을 수행할 수 있게 된다.

하지만, 필자의 경우는 jMock이나 easymock을 사용하는 것이 답답할 때가 많았다. 일단, 이 두 라이브러리를 사용하기 위해서는 각 라이브러리가 제공하는 규칙 생성 방법을 익혀야 했다. 또한, 규칙 생성하는 방식에 제약이 있어서 원하는 기능을 수행하는 Mock 객체를 생성하기 힘든 경우가 많았다. 그리고, Mock 라이브러리가 제공하는 검증 기능이 그다지 필요하지도 않았다.

필자가 필요했던 건, 규칙을 지정하는 방식이 아닌 실제 돌아가는 코드로 구현된 Mock 객체였다. 그래서 아래와 같이 인터페이스를 구현한 Mock 객체를 직접 생성해서 사용하곤 했다.

    public MockBoardDao implements BoardDao {
        public Board insert(Board board) {
            ...
            // 가짜로 동작하는 기능 구현
            return createdBoard;
        }
    }

하지만, 위와 같이 Mock에 해당하는 클래스를 만드는 것 역시 그다지 마음에 들진 않았다. 왜냐면, 인터페이스가 변경될 때마 인터페이스를 구현한 Mock 클래스 역시 변경해주어야 했기 때문이다.

필자는 jMock이나 easymock 처럼 다양한 기능을 제공하기 보다는, 아주 간단하게 내가 원하는 코드를 수행해주는 Mock 객체가 필요했다. 그리고, 인터페이스를 직접 구현하는 방식을 사용하고 싶지도 않았다. 그래서 tlemock을 개발하게 되었다.

Tlemock의 소개

tlemock은 Mock 객체를 생성해주는 유틸리티로서, 주요 특징은 다음과 같다.

  • 매우 간단하다.
    앞서 코드에서 보았듯이 tlemock은 MockFactory.createMock() 메소드만 제공한다. tlemock을 사용하기 위해서는 이 메소드의 사용방법은 익히면 되는데, 사용방법도 복잡하지 않다.
  • 사용하기 쉽다.
    tlemock은 실제로 실행될 코드를 제공하기 때문에, Mock 객체의 동작을 지정하기 위해 별도의 문법을 익힐 필요가 없다.
  • 검증 기능이 없다.
    tlemock은 자체 검증 기능을 제공하지 않는다.
예를 들어, 다음과 같은 인터페이스가 있다고 하자.

    public interface HelloService {
        public String hello();
        public String hello(String name);
    }

이 경우 tlemock이 제공하는 MockFactory.createMock() 메소드를 이용하여 Mock 객체를 생성할 수 있다.

    HelloService helloService = (HelloService) MockFactory.createMock(
        HelloService.class,
        new Object() {
                public String hello(String name) {
                        return "hello, " + name;
            }
        });
    
    helloService.hello(); // do nothing
    helloService.hello("madvirus"); // 두번째 파라미터로 전달한 객체의 hello(String name) 메소드 호출

위 코드를 보면 jMock이나 easymock과 달리 Mock 객체의 행동을 지정하기 위한 코드가 없다는 것을 알 수 있다. 대신, tlemock은 실제로 동작하는 코드를 담은 객체를 전달받는다.

위 코드에서 MockFactory.createMock()의 첫번째 파라미터는 Mock 객체가 구현할 인터페이스나 클래스의 Class 인스턴스이며, 두번째 파라미터는 Mock 객체가 실행할 코드가 된다. 직접 실행할 코드를 제공하기 때문에 개발자가 원하는 수준으로 동작하는 Mock 객체를 생성할 수 있다. 생성된 mock 객체의 메소드를 호출하면, 해당 메소드와 일치하는 코드를 제공받았는 지 검사하고, 일치하는 코드가 있다면 해당 코드를 실행하게 된다. 위 코드의 경우 String hello(String name) 코드를 제공받았으므로, hello("madvirus") 메소드를 호출할 때 제공받은 코드가 실행되어 결과로 "hello, madvirus"가 리턴된다.

tlemock을 통해 생성한 Mock 객체가 어떤 식으로 동작하는 지는 뒤에서 좀더 자세히 살펴보도록 하겠다.

Tlemock의 설치

tlemock은 현재 구글 프로젝트에 호스팅되어 있으며, http://code.google.com/p/tlemock/downloads/list 사이트에서 배포판을 다운로드 받을 수 있다. 배포판을 다운로드 받은 뒤 압축을 풀면 다음과 같은 파일 및 폴더가 생성될 것이다.

  • /tlemock-1.0.1.jar - tlemock 라이브러리
  • /lib/cglib-nodep-2.1.3.jar - tlemock 라이브러리가 필요로 하는 외부 라이브러리
  • /apidocs - Javadoc
tlemock을 사용하려면 tlemock-1.0.1.jar 파일과 cglib-nodep-2.1.3.jar 파일만 CLASSPATH에 추가해주면 된다.

만약 Maven 2를 사용하고 있다면 다음과 같이 의존 정보를 추가해주면 된다.

    <dependency>
        <groupId>net.tleproject</groupId>
        <artifactId>tlemock</artifactId>
        <version>1.0.1</version>
        <scope>test</scope>
    </dependency>

Tlemock을 사용한 Mock 객체 생성 및 사용

tlemock은 MockFactory.createMock() 메소드를 제공하고 있는데, 이 메소드를 이용하면 지정한 인터페이스나 클래스의 Mock 객체를 생성할 수 있다. MockFactory.createMock() 메소드는 두개의 파라미터를 전달받는다. 첫번째 파라미터는 Mock 객체의 대상이되는 인터페이스나 클래스이며, 두번째는 가짜 기능을 구현할 코드를 담고 있는 객체이다.

예를 들어, 인터페이스에 대한 Mock 객체는 다음과 같이 생성할 수 있다.

    public interface SomeService {
       public void service1();
       public int service2();
       public Object service3();
    }
    
    SomeService service = (SomeService)MockFactory.createMock(
         SomeService.class, // target interface
         new Object() {
             public void service1() { // mocking method
                 ...
             }
         } );
    service.service1(); // call mocking method
    service.service2(); // do nothing, return 0 (default value of int type)
    service.service3(); // do nothing, return null (default value of reference type)

위 코드에서 MockFactory.createMock()은 SomeService 인터페이스에 대한 Mock 객체를 생성하고 있다. MockFactory.createMock()이 생성한 Mock 객체는 메소드가 호출될 때, 두번째 파라미터로 전달받은 객체가 호출된 메소드와 동일한 시그너쳐와 리턴타입을 갖는 메소드를 갖고 있는 지 검사한다. 만약 존재한다면 두번째 파라미터로 전달받은 객체의 메소드를 호출한다. 그렇지 않다면 아무것도 수행하지 않는다.

위 코드의 경우 MockFactory.createMock() 메소드에 두번째 파라미터로 void service1() 메소드를 제공하고 있는 객체를 전달했다. 따라서, 생성된 Mock 객체의 service1() 메소드를 호출할 때 두번째 파라미터로 전달받은 객체의 service1() 메소드가 호출된다. 반면에 service2() 메소드와 service3() 메소드는 일치하는 메소드를 제공하지 않았기 때문에 아무것도 수행하지 않는다. 만약, 아무것도 수행하지 않는 메소드가 리턴타입을 갖고 있다면 해당 타입의 기본값을 리턴하게 된다.

클래스에 대한 Mock 객체를 생성할 수도 있다. 클래스의 경우도 인터페이스와 동일한 방법으로 Mock 객체를 생성할 수 있다. 아래 코드는 생성 예이다.

    public abstract class SomeClass {
       public void service1() { // original method
         ...
       }
       public int service2() {
         ...
         return value;
       }
       public abstract Object service3();
    }
    
    
    SomeClass some = (SomeClass)MockFactory.createMock(
         SomeClass.class, // target class
         new Object() {
             public void service1() { // mocking method
                 ...
             }
         } );
    service.service1(); // call mocking method
    service.service2(); // call original method (SomeClass.service2() method)
    service.service3(); // do nothing, return null (default value of reference type) 

위 코드에서 MockFactory.createMock() 메소드는 SomeClass에 대한 Mock 객체를 생성하고 있다. 앞서 인터페이스의 경우와 마찬가지로 Mock 객체의 호출된 메소드와 매칭되는 메소드를 두번째 파라미터로 전달한 객체가 갖고 있다면, 해당 메소드가 호출된다. 반면에 매칭되지 않는다면, Mock의 대상이 된 클래스가 메소드를 구현했느냐에 따라서 동작이 달라진다. 만약 Mock 대상 클래스가(또는 상위 클래스에서) 메소드를 구현했다면 해당 메소드가 호출되고, 구현하지 않았다면 아무것도 수행하지 않는다.

예를 들어, 위 코드에서 Mock 객체는 service1() 메소드에 대해서만 구현을 제공하고 있다. 그리고, SomeClass는 service1()가 service2()의 구현을 제공하고 있고, service3() 메소드에 대해서는 구현을 제공하고 있지 않다. 이 상태에서 세 메소드를 호출하면 다음과 같이 실행된다.

  • service1(): 비록 SomeClass가 service1() 메소드를 구현하고 있긴 하지만, Mock 객체가 제공하는 service1() 메소드가 실행된다.
  • service2(): SomeClass의 service2() 메소드가 실행된다.
  • service3(): SomeClass가 service3() 메소드를 구현하고 있지 않으므로, 아무것도 수행하지 않는다.
결론

tlemock의 장점과 단점을 정리하면 다음과 같다.

  • 복잡한 규칙을 알아야 할 필요가 없이 손쉽게 Mock 객체를 생성할 수 있다.
  • 내가 원하는 코드를 Mock 객체가 실행하므로 좀더 쉽게 Mock 객체를 제어할 수 있다.
  • 하지만, Mock 대상 인터페이스나 클래스가 변경되더라도 컴파일 에러가 발생하지 않기 때문에 주의해야 한다.
만약 jMock이나 easymock의 사용이 망설여져서 Mock을 이용한 단위 테스트를 수행하는데서 오는 이로움을 맛보지 못했다면, tlemock을 통해서 Mock을 이용한 테스트의 장점을 누려보기 바란다.

관련링크:

덧글

댓글 입력 영역