EasyMock(old)을 활용한 협업 테스트 에서 소개한 불편한 EasyMock 1.2. Mock 객체를 사용한 테스트 자체도 이해하는데 장벽이지만, EasyMock 1.2는 벽을 이중으로 만든다. 기억하기도 어려울 뿐더러 직관성도 떨어진다. 우선 EasyMock과 ClassExtension의 차이가 나타나지 않도록 createControl 팩토리 메소드를 래핑하는 선에서 그냥 썼다. 어제 밤 팀원에서 Mock을 이용한 테스트에 대해 안내하면서 API가 어려워 리듬을 잃어버리는 일이 발생했다.

취침에 들어가려고 발을 씻던 중에 아이디어를 실험하기 위해 컴퓨터 앞에 앉았다. 내일을 위해 오버하면 안되는데... 일단 EasyMock 문서가 부족하다고 불평했었는데, 1.2 안에 sample이 있었다. 이런... 샘플을 무시하는 습관이라니..ㅡㅡ;

API 대치/래핑을 통해 샘플의 코드를 개선할 수 있다면 1차적으로는 성공이다. 그리고, 그것이 최소한 EasyMock 1.2를 써본 팀원에게 좋은 평을 받는다면 졸린 눈으로 버틴 시간이 보상받을 것이다.

        control = MockControl.createControl(IMethods.class);
        mock = (IMethods) control.getMock();

예제의 setUp에서 나온 코드. 전형적인 쌍이다. 사실 난 MockControl이란 것이 마음에 들지 않는다. MockControl은 두 가지 역할을 갖는다. 하나는 Mock객체에 대한 기대값을 설정하는 것이고, 다른 하나는 테스트 수행을 위한 컨트롤 역할이다. 어찌 되었든 MockControl 보다는 MockExpections이 나은 듯 하다. 위키피디아에서 본 Setting expectations 문구가 힘을 실리게 해준다. 위의 두 줄의 문장을 하나로 바꾸고 싶은데, 리턴이 두 개인지라 어려워보인다.

사실상 저 둘은 불가분의 관계로 보이니까 Spring의 ModelAndView 처럼 하나로 묶어보자.

  expectations = new ExpectationsOn(IMethods.class);
  mock = (IMethods) expectations.getMock();

MockExpections 라는 클래스로 합치는 것을 시도해보았으나 결국은 Mock 객체 호출이 필요했다. 여기까지는 별반 차이가 없어 보인다. 하지만, 실행 코드는 좀 간결하고 직관적으로 바뀌었다. 아래와 같은 코드였는데

     mock.throwsNothing(true);
     control.setReturnValue("Test");
     control.setReturnValue("Test2");
     control.replay();

     assertEquals("Test", mock.throwsNothing(true));
     assertEquals("Test2", mock.throwsNothing(true));

     control.verify();


안 불러도 별 차이가 없는, verify()는 생략해버리고 Control보다는 '기대 값'이란 점을 강화했다. API 스타일은 Fluent Interface를 채용했다.

  mock.throwsNothing(true);
  expectations.returns("Test").returns("Test2").assert();

  assertEquals("Test", mock.throwsNothing(true));
  assertEquals("Test2", mock.throwsNothing(true));

ready 가 작위적인 느낌이 있지만, 익숙하지 않은 사람에게 replay 역시 큰 차이가 없다. 여기까지만 하고... 위키피디아의 Mock 객체에 대한 메모를 남겨둔다.


a mock object in its place:

  • supplies non-deterministic results (e.g. the current time or the current temperature);
  • has states that are difficult to create or reproduce (e.g. a network error);
  • is slow (e.g. a complete database, which would have to be initialized before the test);
  • does not yet exist or may change behavior;
  • would have to include information and methods exclusively for testing purposes (and not for its actual task).  
이올린에 북마크하기(0) 이올린에 추천하기(0)

EasyMock1.2 스타일의 API는 EasyMock2와는 스타일이 많이 다르다. EasyMock2 사용법은 아래와 같다.


빠른 습득을 위해 스프링의 문체를 분석하고, API를 토대로 주요 구문의 의미를 이해하는 방식을 취하겠다.

        MockControl dsControl = MockControl.createControl(DataSource.class);
        DataSource ds = (DataSource) dsControl.getMock();
        ...
        ds.getConnection();
        dsControl.setReturnValue(con, 2);
        ...
        dsControl.replay();
        ...
        HsqlMaxValueIncrementer incrementer = new HsqlMaxValueIncrementer();
        incrementer.setDataSource(ds);
        ...
        assertEquals(0, incrementer.nextIntValue());
        ...
        dsControl.verify();

막상 살펴보니 별 차이는 없다. 문장이 좀 지져분해질뿐...ㅡㅡ;

(2008.4.18 추가)

    public void testInitBinder() throws Exception {
        MockControl control = MockClassControl.createControl(ServletRequestDataBinder.class);
        ServletRequestDataBinder mockBinder = (ServletRequestDataBinder) control.getMock();
        // record
        mockBinder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat(), false));
        control.setMatcher(new ArgumentsMatcher() {

            public boolean matches(Object[] expected, Object[] actual) {
                return expected[0].equals(actual[0]) && expected[1].getClass().equals(actual[1].getClass());
            }

            public String toString(Object[] arg0) {
                throw new UnsupportedOperationException();
            }
        });

        control.replay();
        controller.initBinder(new MockHttpServletRequest(), mockBinder);

        control.verify();

    }

EasyMock2와 달리 ArgumentsMatcher를 사용하여 매개변수 내용을 확인할 수 있다.

이올린에 북마크하기(0) 이올린에 추천하기(0)

3. 논리 연산 함수 사용 연습
앞서 언급한 EasyMock의 편의 함수에 대해서 연습해보자. 테스트 대상 객체는 의미없이 DomainObject라고 하자. DomainObject는 내부적으로 계산기 기능을 하는 Calculator 타입의 객체를 콜레보레이터(collabrotator)로 요구한다.

 @Test public void logicalOperator(){
  DomainObject domain = new DomainObject();
  Calculator mockCalulator = createMock(Calculator.class);
  domain.setCalculator(mockCalulator);
 
  mockCalulator.calc(anyInt(), anyInt());
  replay(mockCalulator);
 
  domain.doSomething(1,2);
  verify(mockCalulator);
 }


테스트 대상이 되는 객체의 정의는 다음과 같다.

public class DomainObject {

 private Calculator calculator;

 public void setCalculator(Calculator calulator) {
  this.calculator = calulator;
 }

 public void doSomething(int i, int j) {
  calculator.calc(i, j);
 }

}

public interface Calculator {

 public void calc(int i, int j);

}

테스트는 통과한다.

  mockCalulator.calc(1,2);
  replay(mockCalulator);

  domain.doSomething(1,2);
  verify(mockCalulator);

위 테스트 코드 블럭의 첫 줄은 다음가 같이 수정해도 동일한 내용이 된다.

mockCalulator.calc(eq(1),eq(2));

eq() 메소드는 인자로 제공되는 값과 동일한 값을 갖는 인자를 기대하게 (녹화) 한다. 반환 값으로 인자와 같은 값을 반환하기 때문에 calc() 메소드의 인자로 1, 2가 순서대로 전달되는데 아무런 문제가 없다. 이러한 방식으로 설계된 메소드 인터페이스를 Fluent Interface라고 한다.

gt(), lt()를 사용해보자. 이들은 각각 "Greater Than"과 "Less Than"의 "크다", "작다"의 의미다.

  mockCalulator.calc(gt(0),lt(2));
  replay(mockCalulator);

  domain.doSomething(1,2);
  verify(mockCalulator);

gt(0)은 문제가 없지만, lt(2)는 통과하지 못한다. leq() 메소드의 경우는 통과한다.

  mockCalulator.calc(gt(0),leq(2));

leq()와 geq()는 각각 "Less than or EQual to"와 "Greater than or EQual to"를 나타낸다. 비슷하게 생각하여 다음 코드를 실행해보면 실패가 되는 것을 알 수 있다.

 mockCalulator.calc(not(5), not(3));

not()주어진 예상 결과가 인자의 값과 다름을 나타내는 것이므로 예상되는 인자값 설정에는 부적절하다. 게다가 항상 반환 값이 0이 된다.

여타 다른 논리 연산 메소드는 추가로 살펴보겠다.


관련 글:
- EasyMock2을 활용한 협업 테스트 1
- EasyMock2을 활용한 협업 테스트 2

참고:
- EasyMock 2.2 Readme
- EasyMock 2.2 API Documetation

이올린에 북마크하기(0) 이올린에 추천하기(0)

2. collaborator의 메소드 호출 기대하기

@Before public void setUp(){
  addressBook = new AddressBook();
  mock = createMock(AddressDao.class);
  addressBook.setAddressDao(mock);
 }

@Test public void invokeTheMethod(){

  Address address = new Address();
  replay(mock);
  addressBook.add(address);

 }

위 테스트에서 add() 메소드에 아무런 내용이 없으면 테스트는 성공한다. 따라서, IDE 자동 완성 템플릿 수정을 권장한다.

테스트를 작성한다는 것은 다른 측면으로는 addressBook 객체의 add() 메소드를 어떻게 구현할지 결정하는 일이다. addressBook 객체만으로는 address 객체의 영속적인 저장을 보장하지 못한다. 위임이 필요하다. 영속성 보장을 위해서는 DAO 패턴을 적용하여 AddressDao 타입의 객체와 협업하기로 결정했다.

이러한 상황에서 add()의 구현은 AddressDao 객체와 어떤 협업을 요구하게 될까? 데이터베이스에 address 객체가 갖는 데이터를 저장하는 메소드를 호출해야 할 것이다. 그 이름이 save가 될지, insert가 될지, add가 될지는 모르지만, 당장의 관심사(Concerns)는 아니다.1 이글에서는 동일하게 add()라고 해보자. 이제 add()는 AddressDao 객체의 add() 메소드를 호출해야 한다.

 @Test public void invokeTheMethod(){

  Address address = new Address();
  mock.add(address);
  replay(mock);
  addressBook.add(address);
  verify(mock);

 }

예상대로 호출되었는지 확인하는 기능을 제공하는 verify() 메소드를 추가하고 실행하면, java.lang.AssertionError를 만나게 된다. 테스트 성공을 위해서는 AddressBook.add()에 다음과 같은 코드가 요구된다.

 public void add(Address address) {
  addressDao.add(address);
 }

3. 인자에 대한 요건 설정하기
테스트 코드를 아래와 같이 수정하면 테스트가 실패한다.

 @Test public void invokeTheMethod(){
  mock.add(new Address());
  replay(mock);
  addressBook.add(address);
  verify(mock);
 }

녹화(record) 단계의 add() 메소드 인자로 쓰인 객체와 재생(replay) 단계의 address 객체가 다른 객체를 참조하고 있기 때문이다. 이를 확인하기 위해 Address 클래스의 equals() 메소드를 오버라이딩 해보면 테스트가 성공한다.

 @Override
 public boolean equals(Object object) {
  return this.getClass().equals(object.getClass());
 }

만일 인자가 객체 형태로 전달만 되기를 기대한다고 해보자. 이런 경우는 다음과 같이 표현할 수 있다.

 @Test public void invokeTheMethod(){
  mock.add((Address) anyObject());
  replay(mock);
  addressBook.add(address);
  verify(mock);
 }

anyObject()2 를 사용하는 경우라면 구현은 어떻게 다를까?

 public void add(Address address) {
  addressDao.add(new Address());
 }

AddressBook 객체의 add() 메소드에서 인자로 받은 객체를 전달하지 않고, 새로운 객체를 만들어서 전달할 수 있게 된다. 그렇게 구현하기로 결정하는 것이다. 이때, null을 입력하면 어떻게 될까? 테스트는 성공한다.

 @Test public void invokeTheMethod(){
  mock.add((Address) and(anyObject(),notNull()));
  replay(mock);
  addressBook.add(address);
  verify(mock);
 }

and() 메소드를 사용하면 인자의 조건을 복합적으로 구성할 수 있으며, notNull()도 편의 함수로 제공한다.

EasyMock은 8가지 기본형(primitive type)과 Generic 타입을 인자로 받는 and() 메소드를 API로 제공하고 있다. 논리 연산을 위해서 이외에도 gt(), lt(), leq(), isA(), isNull(), notNull(), geq(), not(), aryEq(), same() 등의 메소드를 제공한다.

한편 문자열 비교를 위해 contains(), startsWith(), endsWith(), find(), matches() 등도 제공한다.


관련 글: EasyMock2을 활용한 협업 테스트 1

참고:
- EasyMock 2.2 Readme
- EasyMock 2.2 API Documetation

  1. 작명의 중요성을 간과하는 것은 아니다. [본문으로]
  2. EasyMock은 anyObject() 외에도 anyBoolean(), anyInteger() 같은 8가지 자바 기본형(primitive type)을 위한 편의함수를 제공한다. [본문으로]
이올린에 북마크하기(0) 이올린에 추천하기(0)

테스트 대상이 되는 객체(단위)가 다른 객체(Collaborator)에 의존하여 혹은 다른 객체와의 협업을 통해 수행되는 경우는 흔히 있는 일이다. 이때, 구체적으로 어떻게 테스트 할 것인가를 결정하는 일은 그리 쉬운 것은 아니다.

단위 테스트(Unit test)를 택할 것인가? 통합 테스트(Integration test)를 택할 것인가와 같은 고민을 해야 한다. 요즘처럼 멀티 티어 환경과 이기종 시스템의 통합이 일반적인 상황에서는 단위 테스트의 의미조차 모호해질 수 있다. 이에 대해서는 이 글의 범위를 벗어나므로 아래 글들을 참조하길 바란다.

Collaborator가 요구되는 테스트 케이스를 처리하는 방법에는 크게 두 가지가 있다. 하나는 Stub을 이용하는 방법이고, 다른 하나는 Mock을 이용하는