Mockito를 사용할 때 가장 아쉬운 것은 property를 확인하는 argument matcher가 없다는 것이다. 보통 오브젝트간에 주고 받는 파라미터는 단순 값들보다는 복잡한 오브젝트이다. 특히 DAO로 넘기는 것들은 대부분 도메인 모델/엔티티 구조이니 기존의 파라미터 오브젝트를 직접 비교하는 방법은 그다지 유용하지 못하다.

name, age라는 프로퍼티를 가진 User라는 도메인 오브젝트가 있다고 하자.

UserDao 인터페이스 타입의 목을 만들어서 테스트에 사용했다고 하자. 그리고 add(User u) 메소드로 넘어간 User오브젝트의 name이나 age가 어떤 값을 가지고 있는지를 확인하고 싶다면, 그리 간단치 않다.

verify(mockUserDao).add(…) 에 arguement matcher를 넣어서 검증해야 하는데, 스트링이나 기본형이라면 모르겠지만, 파라미터의 getName()으로 가져온 값을 바로 비교하게 만들기란 쉽지 않다. 결국 Custom Argument Matcher를 만들어서 사용해야 하는데, 이게 거의 커스톰 목 클래스를 정의하는 것 만큼 귀찮은 일이다.

그래서 EasyMock에서는 bean property를 비교할 수 있는 뭔가가 있는 것 같은데, 내가 쓰는 Mockito 1.7에서는 아직 발견하지 못했다. Hamcrest Matcher를 사용할 수 있지만, 거기서도 못찾았고. 아마 누군가 만들어둔게 있을거라고 생각하지만.. 더 찾아볼까 하다가 아예 발상을 바꿔봤다. 한번에 여러개의 프로퍼티를 비교해야 한다면 한 개 이상의 비교를 하려면 파라미터 타입을 어떻게 쓸지가 고민이다. 맵을 사용해야 하겠지만, 이놈의 Map은 한방에 정의하기도 힘들다. 왜 자바는 맵이나 해쉬를 언어차원에서 지원하지 않는지 원망스러울 때가 많다.

그래서 생각한 것이 바로 스프링 3.0의 익스프레션 언어지원기능. 왠만한 시중에 나와있는 EL류보다 월등히 뛰어나다고 자부하는 바로 그 것이다. 아직은 사용 시나리오나 용도가 많지는 않지만, 잘 사용하면 알게 모르게 편리하게 이용할 수 있다.

 

그래서 스프링의 EL을 이용한 argument matcher를 만들어봤다. 하는 김에 assertThat에서 바로 사용할 수 있도록도 만들었다.

SpEL의 막강한 기능 덕에 몇줄 안되는 코드로 손쉽게 작성이 가능했다.

 

package org.opensprout.commons.test;

import org.hamcrest.Matcher;
import org.mockito.ArgumentMatcher;
import org.mockito.Matchers;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression;
import org.springframework.expression.ParseException;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

public class SpelMatcher<T> extends ArgumentMatcher<T>{
    private final String spel;
    private final Object[] args;
    private static final String ARG_PREFIX = "a";
    public SpelMatcher(String spel, Object… args) {
        this.spel = spel;
        this.args = args;
    }

    @Override
    public boolean matches(Object argument) {
        SpelExpressionParser p = new SpelExpressionParser();
        StandardEvaluationContext ctx = new StandardEvaluationContext();
        ctx.setRootObject(argument);
        for(int i=0; i<args.length; i++) {
            ctx.setVariable(ARG_PREFIX+(i+1), args[i]);
        }
        try {
            Expression exp = p.parseExpression(spel);
            return (Boolean)exp.getValue(ctx);
        } catch (ParseException e) {
            throw new IllegalArgumentException(e);
        } catch (EvaluationException e) {
            throw new IllegalArgumentException(e);
        }
    }
    public static <T> T argel(String spel, Object… args) {
        return  Matchers.<T>argThat(new SpelMatcher<T>(spel, args));
    }
    public static <T> Matcher<T> spel(String spel, Object… args) {
        return new SpelMatcher<T>(spel, args);
    }
}

 

사용방법은 간단하다.

argThat과 함께 SpelMatcher 인스턴스를 만들어서 사용하던가 아니면 간단하게 만들어둔 스태틱 메소드를 사용하면 된다.

argel은 Mockito의 ArgumentMatcher이고 spel은 Hamcrest의 Matcher이다.

다음과 같은 도메인 오브젝트가 있다고 하고.

class User {
        String name;
        int age;
        double saving;
        String[] nicks;
        User nested;

       // getters, setters

}

다음과 같이 초기화 시켰다면,

User user = new User("Toby", 30, 123.45, new String[] {"A","B","C"});
User userNested = new User("Mich", 10, 234.56, new String[] {"1","2","3"});
user.setNested(userNested);

 

다음과 같이 사용이 가능하다.

assertThat(user, spel("name == #a1", "Toby"));

첫번째 파라미터는 SpEL이고, 그 후로는 #a로 시작하는 변수에 바인딩할 파라미터들이다. varargs이므로 갯수에는 제한이 없다. spel에서는 user를 rootObject로 설정하고 시작한다. 따라서 name이라고 하면 user.name이 되는 것이다. 당연히 user.getName()이 호출될 것이고. #a1이면 첫번째 바인딩 파라미터를 사용한다. 그 뒤로 붙으면 계속 번호가 늘어나면 된다.

Mockito에서 사용한다면 이렇게 쓸 수 있다.

verify(collabor).add((User)argel("name == #a1", "Toby"));

SpEL과 바인딩 파라메터만 넘기기 때문에 Generics로 리턴타입을 캐스팅하기가 어려웠다. 타입추론을 적용하려고 했지만, 왜그런지 add() 메소드의 컨텍스트를 이용한 타입추론은 성공을 못했다. 결국 직접 캐스팅 하거나 아니면 타입 파라미터를 줘야 한다. 타입 파라미터는 스태틱 임포트된 메소드에는 또 사용이 불가능하다. 결국 클래스이름부터 줘야 한다.

verify(collabor).add(SpelMatcher.<User>argel("name == #a1", "Toby"));

재밌는 것은 같은 내용인데도 할당문을 거쳐서 사용하면 캐스팅이 필요없고 타입추론이 잘 먹는다는 점이다.

User u = argel("name == #a1", "Toby");
verify(collabor).add(u);

전형적인 타입추론 케이스인 리턴타입에도 Generics 스타일을 쓰는(Matcher<T>) spel에서는 잘 되는데, 메소드 컨텍스트에서 T타입으로 리턴하는 경우에는 안되는 것이.. 뭔가 아쉬움이다. 다음 자바 버전에서는 될지도.

 

어쨌든 SpEL의 막강한 기능을 이용해서 다양한 검증이 가능하다.

어레이나, 리스트, 맵도 지원하고 당연히 중첩된 오브젝트 그래프를 따르는 네비게이션도 된다. 복합조건도 당연히 잘 되고. 다음은 테스트 코드에서 확인해봤던 것들의 일부이다.

nicks[0] == ‘A’
age > 29 and age < 31
nicks.length == 3
name == ‘Toby’ and nested.nicks[0] == ’1′
"name == #a1 and age == #a2", "Toby", 30
"saving == #a1 and nicks[0] == #a2", 123.45, "A"

 

SpEL은 메소드 호출도 가능하다.

연산자도 재정의 할 수가 있고, 이런 저런 확장기능도 뛰어나다.

 

아무튼 이제 Mockito 사용시 귀찮은 커스톰 ArgumentMatcher 정의는 이제 그만. 참 JUnit assertThat 사용할 때도.

스프링 3.0 M3 버전 이상의 expression 모듈이 있어야 바르게 동작할 것이다. expression 모듈은 다른 의존관계가 없으니 아마 spring2.5에도 가져가 사용할 수도 있을 듯.

 

SpEL은 지난번 @Check를 이용한 익셉션의 메시지까지 확인하는 테스트기능 이후로 두번째 적용해봤는데, 응용가능성이 무한하다는 느낌이다. 다만, 텍스트 기반의 런타임 파싱구조이므로 리팩토링에 살짝 취약하다는게 단점.

Related posts:

  1. Spring 3.0 EL (Spel)을 이용한 AssertThrows 확장 (3)
  2. Spring 3.0 EL (Spel)을 이용한 AssertThrows 확장 (2)
  3. Spring 3.0 (59) 프로퍼티 파일 이용하기 – placeholder vs SpEL
  4. Spring 3.0 EL (Spel)을 이용한 AssertThrows 확장 (1)
  5. Spring 3.0 @MVC 메소드에서 자동으로 리턴 모델에 추가되는 것들
  6. Spring 3.0 (26) Spring Expression Language와 @Value
  7. Spring 3.0 (2) R-518 스프링의 새 모듈 OXM(Object/XML Mapping)
  8. Spring ROO 대충대충 분석 (3) ROO의 Inter-type declaration
  9. 테스트 코드에서 static import를 편하게 넣는 방법
  10. Spring기반의 Hibernate DAO Unit Test 만들기
  11. 테스트 할 수 없는 것을 테스트 하기. Spring ROO와 static method mocking.
  12. [토스3] 매핑 가능한 BeanPropertySqlParameterSource
  13. Spring 3.0 (46) Spring 3.0 M4 릴리스
  14. 미리 보는 Spring 3.0.1의 변경사항
  15. Spring 상식퀴즈 (1) – DI 태클하기

Facebook comments:

to “Spring Expressions(SpEL)를 이용한 Mockito Argument Matcher 만들기”

  1. Incredible lots of awesome data. cialis generic

  2. You stated this wonderfully! canada drug pharmacy

  3. Here is a superb Blog You may Locate Interesting that we Encourage You

Leave a Reply

(required)

(required)

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

© 2017 Toby's Epril Suffusion theme by Sayontan Sinha