springmodules의 valang을 활용한 Validation 테스트

FRAMEWORK/SPRING 2013. 7. 31. 15:18

Expert Spring MVC and Web Flow (Expert)에 소개된 springmodules의 valang을 활용하면 Validator를 자바 코딩 없이 선언만으로 구현할 수 있다.(XML 설정만으로 가능하다)

복잡한 자체적인 문법을 갖고 있어서 테스트 필요성이 높다. Expert Spring MVC and Web Flow (Expert)의 p281에는 테스트 예제가 있다.

public class PersonValidatorTests extends TestCase {
public void testEmptyPersonValidation() {
Person person = new Person();
Validator validator = new PersonValidator();
BindException errors = new BindException(person, "target");
validator.validate(person, errors);
new ErrorsVerifier(errors) {
{
forProperty("firstName").hasErrorCode("person.firstName.required")
.forProperty("lastName").hasErrorCode("person.lastName.required")
.otherwise().noErrors();
}
}
}
}


ErrorsVerifier를 헬퍼(Helper class)로 사용하여 간결하게 작성했지만 문제가 있다. ErrorsVerifier 클래스는 spring은 물론 spring-modules에도 존재하지 않는다는 점이다. 구글을 통해 Spring 포럼에 올라온 글, 'Where is ErrorsVerifier class mentioned in Expert Spring MVC?'을 확인할 수 있었다.

하지만, spring-modules의 테스트 및 소스 코드를 조금 참조하면 어렵지 않게 테스트를 작성하 수 있다.

public class MemberValidationTest extends TestCase {
private Member member;
private ValangValidator validator;
BindException errors;
@Override
protected void setUp() throws Exception {
member = new Member();
errors = new BindException(member, "member");
validator = new ValangValidator();
}
public void testPasswordValidation() throws Exception{
validator.setValang(
   "{ password : ? is not blank : '' : 'member.password.empty' }"
);
validator.afterPropertiesSet(); // configure validation rules
validator.validate(member, errors);

assertEquals("member.password.empty", errors.getFieldError("password ").getCode());
}
}


spring-modules의 테스트는 spring의 컨텍스트 파일(xml 형식의) 설정을 통해 테스트를 한다. 그러나, 단위 테스트를 목적으로 한다면 외부 리소스에 의존하는 일은 가능한 제거하는 것이 좋다고 생각하여 위와 같이 직접 setValang()을 호출한 것이다. setValang()은 실제로는 다음과 같은 식으로 설정해야 한다.

<bean id="easyAddressValidator" class="org.springmodules.validation.ValangValidator ">
  <property name="valang">
  <value><![CDATA[{ password : ? is not blank : '' : 'member.password.empty' }]]></value>
  </property>
</bean>


spring-module의 의존성에 따라 테스트를 실행하려면, 다음 라이브러리가 필요하다.

  • commons-collections.jar


assertion을 위한 코드가 늘어서는 것도 가독성을 크게 저해하지는 않는다. 책에서의 의도와 비슷하게 Fluent Interface 스타일을 적용해서 조금 수정해봤다.

public void testPasswordValidation() throws Exception{
validator.setValang(
   "{ password : ? is not blank : '' : 'member.password.empty' }"
);
validator.afterPropertiesSet(); // configure validation rules
validator.validate(member, errors);
new ErrorsVerifier(errors)
  .forProperty("password").hasErrorCode("member.password.empty");
}
class ErrorsVerifier{
private Errors errors;
private String cursor; // one of successive properties

public ErrorsVerifier(BindException errors) {
  this.errors = errors;
}
public ErrorsVerifier forProperty(String property) {
  cursor = property;
  return this;
}
public ErrorsVerifier hasErrorCode(String errorCode) {
  assertEquals(errorCode, errors.getFieldError(cursor).getCode());
  return this;
}

}


아쉬운 것은 정작 감춰야 할 부분인 아래 구문이 그대로 노출된다는 점이다:
validator.afterPropertiesSet();

감추기


추가적으로, valang 사용시 주의할 사항 0순위는 조건문이라고 생각된다. 마치 오른쪽과 왼쪽이 헷갈리는 것처럼 valang으로 기입한 조건이 만족하지 않으면 BindError가 발생한다는 사실을 유념해야 한다. 간단한 valang 설정 샘플을 첨부한다.

validator.setValang(
   "{ id : length(?) >= 3 : '' : 'member.id.tooshort' }" +
   "{ password : length(?) >= 4 : '' : 'member.password.tooshort' }" +
   "{ name : length(?) >= 2 : '' : 'member.name.tooshort' }" +
   "{ alias : ? is blank and length(?) >= 2 : '' : 'member.alias.tooshort' }"
);

또한, valang의 기본적으로 제공하는 함수 중에 앞의 예에서 보는 length()와 함께 email()이 있는데 주의할 점은 is true와 함께 써야 의도한대로 작동한다는 점이다.

"{ email : email(?) is true : '' : 'member.email.invaldformat' }" // 좋은 표현
"{ email : email(?) : '' : 'member.email.invaldformat' }" // parser exception 발생


감추기


감추기


원하는 함수가 없는 경우 직접 만들어서 사용할 수 있다. valang에 의존적인 클래스를 상속해야 하고 테스트가 무척 어렵기 때문에 가능하면 쓰지 않는 것이 좋을듯하나 편리하다.

이하 사용자 함수 작성
public class UrlFunction extends AbstractFunction {
public UrlFunction(Function[] arguments, int line, int column) {
 super(arguments, line, column);
}
/*
  * (non-Javadoc)
  *
  * @see org.springmodules.validation.valang.functions.AbstractFunction#doGetResult(java.lang.Object)
  */
@Override
protected Object doGetResult(Object target) throws Exception {
 String url = getArguments()[0].getResult(target).toString();
 if (url != null) {
  try {
   new URL(url);
   return true;
  } catch (MalformedURLException e) {
   return false;
  }
 }
 return false;
}
}

등록
validator.setValang(
   "{ blog : url(?) is true :  '' : 'member.blog.invaldformat' }"
 );
 validator.addCustomFunction("url", "net.webapp2.util.valang.UrlFunction");


감추기


출처 - http://ahnyounghoe.tistory.com/99

: