Spring Test MVC로 컨트롤러 테스트하기

기본

기선이 형의 Spring Test MVC 프로젝트 소개글을 보고 언제 한 번 써봐야겠다 하다가 요새 좀 살펴보고 있습니다.

사실 고백하자면 컨트롤러 테스트 케이스는 작성 해본적이 거의 없는데요.. 이거 테스트 작성할 시간에 그냥 브라우저로 돌려보는게 낫겠다 싶어서요. 그러다가 요새 Spring Test MVC를 써보니 테스트 하기도 쉽고 또 상당히 재미있네요 ㅎㅎ

Maven 디펜던시

아직 정식 릴리즈가 안돼서 메이븐 레파지토리를 추가해 줘야 합니다:

<repository>
	<id>spring-snapshot</id>
	<name>Spring Maven SNAPSHOT Repository</name>
	<url>http://repo.springsource.org/libs-snapshot</url>
</repository>

그리고 디펜던시도 추가해 줘야겠죠:

<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-test-mvc</artifactId>
	<version>1.0.0.BUILD-SNAPSHOT</version>
</dependency>

몇 가지 샘플코드

Spring Test MVC는 빌더 패턴 형태로 테스트 템플릿을 만들고 세부적인 것은 다양한 Matchers를 통해 만들어 나가는 식으로 구성됩니다. hamcrest랑 비슷하게요. (그러고보니 hamcrest Matchers를 바로 사용할 수 있는 부분도 본 것 같습니다)
org.springframework.test.web.server.result 패키지를 확인해보면 다양한 Matcher를 볼 수 있죠. 이걸 쓰려고 전부 static import 할 필요는 없고, 다음 2개 클래스만 static import 해줍니다:

import static org.springframework.test.web.server.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.server.result.MockMvcResultMatchers.*;

Spring MVC의 컨트롤러를 테스트 하기 위해선 먼저 스프링 컨텍스트가 필요한데요. 예전 같았으면 @RunWith, @ContextConfiguration 어노테이션을 붙여서 시작했겠지만 Spring Test MVC는 MockMvcBuilders 클래스를 사용합니다:

MockMvc mockMvc = MockMvcBuilders.xmlConfigSetup(new String[] {"classpath:/applicationContext.xml", "file:web/WEB-INF/spring-servlet.xml"}).build();

XML을 사용하는 방법 외에 @Configuration, WebApplicationContext, 단일 Controller를 사용해 테스트 컨텍스트를 꾸리는 방법도 있습니다. MockMvc를 생성했으면 테스트 할 URL과 파라미터 등을 세팅하면 됩니다:

mockMvc.perform(post("/message/post").param("author", "mOer").param("body", "some contents...").param("title", "some title"))
.andExpect(status().isOk())
.andExpect(redirectedUrl("/list"));</code>

앞에서 static import를 한 이유는 perform() 메소드에 사용하는 get(), post() 등의 메소드가 MockMvcRequestBuilders 클래스에 정의돼 있기 때문입니다. 반대로 다른 Matchers 클래스를 static import 할 필요가 없는 이유는 MockMvcRequestBuilders 클래스 정의된 각각의 메소드가 빌더 형태로 만들어져 있어서 각각의 필요한 Matchers 인스턴스를 리턴하기 때문입니다.

MockMvcResultMatchers 클래스에는 해당 컨트롤러를 호출한 뒤 다양한 검사를 할 수 있는 static 메소드가 정의돼 있습니다.

andExpect() 메소드를 통해서 Assertion을 할 수 있기 때문에 구지 Assert.. 메소드들을 사용할 필요가 없습니다. 또한 andReturn() 메소드를 호출하면 MvcResult가 리턴되는데, 이 안에 ModelAndView 객체가 있어서 실질적으로 어떤 모델이 뷰에 리턴되는지 검증 할 수도 있습니다. 물론, andExpect() 에서도 할 수 있구요:

.andExpect(model().attributeExists("message"));

@PathVariable을 사용하는 것도 문제 없습니다. 만약 @RequestMapping(“/delete/{messageId}”) 이렇게 매핑된 컨트롤러가 있다면, /delete/125 이렇게 테스트 할 수도 있겠지만 그대로 URI Template을 사용하는 방법도 제공해 줍니다:

mockMvc.perform(request(HttpMethod.GET, "/message/delete/{messageId}", 125))

헐.. 테스트하기 엄청 편해요. Spring Test MVC 프로젝트가 아직 정식 릴리즈는 안됐지만 기선이 형 포스팅에는 공식 프로젝트와 합쳐질 거란 말도 있더라구요. 이상 @MVC 테스트의 끝판왕인 Spring Test MVC의 간단한 소개 였습니다 ㅋ

OpenJDK로 https 접근 시 the trustAnchors parameter must be non-empty 예외 발생

기본

새해가 밝아오면서 전사적으로 SNS를 이용한 컨텐츠 공유에 힘쓰려나 봅니다. 그래서 트위터, 페이스북의 Open API를 둘러보다가 트위터 타임라인을 가져오는 간단한 예제 프로그램을 만들어 보려고 마음 먹었습니다.

트위터에서 소개된 OAuth 라이브러리 중 제일 간단해 보이는 Scribe를 선택했지요. 트위터에 요청하는 REST API는 다음과 같습니다:

https://api.twitter.com/1/statuses/home_timeline.json

물론 헤더에 OAuth 관련 파라미터들도 잔뜩 붙지요. 얼마 전에 졸업 프로젝트로 구글 캘린더 연동도 해봤던 터라 별 문제 없을 줄 알았는데 생전 처음보는 예외를 접했습니다.

javax.net.ssl.SSLException: java.lang.RuntimeException: Unexpected
error: java.security.InvalidAlgorithmParameterException: the
trustAnchors parameter must be non-empty

읭? 이건 무슨 에러인고. 구글링을 해보니 Truststore라는 인증서들을 보관하는 파일(JRE/lib/security/cacerts)이 있는데, 요 안에 해당 https 요청을 할 때 사용할 인증서가 들어있지 않거나 혹은 이 인증서가 유효하지 않아서 나는 에러메시지 같았습니다. 이 에러는 특히 OpenJDK를 사용할 때 발생하며 Sun, Oracle, Apple의 JDK를 사용하면 발생하지 않습니다.

첫 월급으로 맥북 에어를 지른 터라 맥을 사용하고 있는데요. 최근에 JDK 1.7 사용해 보려고 OpenJDK 1.7을 사용하고 있었습니다. 그래서 이 예외를 만날 수 있었지요 ㅋ

맥의 JRE(애플에서 제공하는 JDK 1.6.0_29-b11-402) 에 있는 cacerts는 사실 다음 파일을 링크하고 있습니다:

/System/Library/Java/Support/CoreDeploy.bundle/Contents/Home/lib/security/cacerts

이 파일의 크기는 203906 바이트인데, OpenJDK 쪽에 있는 cacerts 파일은 크기가 32바이트 밖에 안됩니다. OpenJDK에는 써드 파티들 중, 자신의 소스 공개를 원하지 않는 벤더들이 빠졌다고 그러더니 이와 관련해서 인증서도 빠진게 아닌가 하는 생각이 들었습니다.

cacerts 파일 내용은 다음의 명령어로 볼 수 있습니다:

keytool -list -v -keystore cacerts

클라이언트 쪽에 왜 이러한 파일이 필요할까? 하는 의문이 들어서 팀 분에게 물어보니 서버에서 인증서를 클라이언트에게 보내면 이 인증서가 유효한건지 아닌지 판단을 클라이언트에서 하는게 아니라 관련 인증서를 발급한 기관에 검증 요청(key를 클라이언트마다 보관할 수 없으므로)을 해야하기 때문에 클라이언트에서 이러한 리스트 형태의 정보를 가지고 있어야 하는 것 같았습니다.

그냥 JDK 1.6을 사용해도 되지만 애플에서 JDK 개발에 손을 떼서 앞으로는 OpenJDK를 써야 한다는 기사를 본 기억도 나고 그냥 JDK 1.7을 사용하고 싶어서 다음과 같이 OpenJDK 1.7의 cacert 파일을 지우고 심볼릭 링크로 교체했습니다:

ln -s /System/Library/Java/Support/CoreDeploy.bundle/Contents/Home/lib/security/cacerts cacerts

테스트 해봤더니 트위터 API 호출도 정상 동작하네요 🙂

참조 사이트

http://forum.springsource.org/showthread.php?30094-trustAnchors-parameter-must-be-non-empty

http://blog.naver.com/nsjkim?Redirect=Log&logNo=140148433832

MyBatis 어노테이션에서 Groovy “”” 사용하기

기본

정상혁 님의 포스트에 이클립스에 Groovy를 세팅하는 자세한 방법이 나와있는데요. 따라하다 보니 몇 가지 문제가 발생했습니다.

  • Maven에서 빌드 시, testCompile 골에서 예외 발생.
  • slf4j 사용시, 로거를 초기화 할 수 없어 예외 발생.
  • Groovy 클래스를 실행 할 수 없음.

마지막 것은 제 삽질입니다 (…) 이번 포스팅은 위 세 가지 문제를 해결해 MyBatis 어노테이션에서 Groovy 삼중 큰따옴표를 쓰는 것이 목표입니다.

1. Maven에서 빌드 시, testCompile 골에서 예외 발생.

로그에는 심볼을 찾을 수 없다고 나오는데 사실 ClassNotFound 예외입니다. Maven으로 빌드 도중 JUnit 테스트를 할 때, groovy 파일을 먼저 컴파일 하지 않아 발생합니다.

GMaven 홈페이지의 Building Groovy Projects 글을 보면 <goal> 설정 부분에 stub 만드는 골이 있는 것을 볼 수 있습니다.

<goals>
	<goal>generateStubs</goal>
	<goal>compile</goal>
	<goal>generateTestStubs</goal>
	<goal>testCompile</goal>
</goals>

이 스텁 골들의 역할은 maven-compiler-plugin이 compile 골과 testCompile 골을 실행하기 전에 groovy 클래스들을 java 소스로 변환시켜 주는 것입니다. 그래서 maven-compiler-plugin이 문제 없이 자신의 골을 수행할 수 있도록 만들어 줍니다.

2. slf4j 사용시, 로거를 초기화 할 수 없어 예외 발생.

GMaven은 Maven 플러그인이고 실제로 Groovy 실행을 위해서는 gmaven runtime API가 필요합니다.

<dependency>
    <groupId>org.codehaus.gmaven.runtime</groupId>
    <artifactId>gmaven-runtime-1.7</artifactId>
    <version>1.3</version>
</dependency>

위에 1.7은 어떤 버전의 Groovy를 실행할 지에 대한 것이고 아래 1.3은 gmaven-runtime API 자체 버전입니다.

이 API는 다른 API들과 의존성을 가지고 있는데요. 그 중 하나가 gshell-io 입니다. gshell-io는 다시 slf4j와 구현체인 gossip에 의존성을 가지고 있기 때문에 gossip으로 개발 중이 아니라면 제대로 Logger를 초기화 하지 못하거나 중복된 클래스가 있어서 예외가 발생할 수 있습니다. 해결 방법은 gshell-io에서 gossip을 exclude 하면 됩니다.

<dependency>
	<groupId>org.sonatype.gshell</groupId>
	<artifactId>gshell-io</artifactId>
	<version>2.4</version>
	<exclusions>
		<exclusion>
			<groupId>org.sonatype.gossip</groupId>
			<artifactId>gossip</artifactId>
		</exclusion>
	</exclusions>
</dependency>

3. Groovy 클래스를 실행 할 수 없음.

Groovy 클래스로 만들어 놓고 컴파일은 했는데 실행이 안되는 문제입니다.

http://dist.springsource.org/release/GRECLIPSE/e3.7/

위 주소의 이클립스 플러그인을 설치 할 때, Groovy 1.8 compile features를 설치할 수 있는데요. (옵션입니다.) 이것을 설치하게 되면 기본 Groovy 컴파일러가 1.8 용으로 잡히게 됩니다. 즉, 1.8 버전용으로 컴파일 된다는 뜻이고, GMaven 런타임 API의 경우 1.7 버전 용이기 때문에 1.8 버전의 Groovy API가 없어서 발생하는 에러입니다.

옵션에서 Groovy compiler 버전을 1.7로 바꿀 수가 있는데 이렇게 해도 에러가 계속 나더라구요. 해결책이라고 하기도 뭣하지만 결국 Groovy 1.8 compiler features를 언인스톨 해서 해결했습니다 (…)

4. class 파일로 컴파일도 완료 됐는데 cannot be resolved type 에러 발생

<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-compiler-plugin</artifactId>
	<version>2.3.2</version>
	<configuration>
		<source>1.7</source>
		<target>1.7</target>
		<encoding>${default.encoding}</encoding>
		<includes>
			<include>**/*.java</include>
			<include>**/*.groovy</include>
		</includes>
	</configuration>
</plugin>

이렇게 groovy 파일도 인클루드 시켜 버립니다.

MyBatis 어노테이션에서 Groovy “”” 사용하기

드디어 메인 주제네요. MyBatis 어노테이션에서는 constant가 아니면 안됩니다. 그래서 유틸 클래스를 사용할 수가 없습니다. 따라서 Mapper 인터페이스 자체를  groovy 파일로 만들어서 사용하면 됩니다.

public interface SqlMapper {

	@Select("""
		SELECT reservation.id id, client.id clientId, client.name, client.department, menu.title, menu.id menuId, menu.building, reservation.reserve_date reserveDate, reservation.type
		FROM reservation, client, menu
		WHERE menu.building = #{building}
		AND client.id = reservation.client_id
		AND menu.id = reservation.menu_id
	""")
	List<Reservation> getReservationList(Reservation reservation);
}

그냥 Java처럼 보이지만 사실 Groovy 클래스입니다.

[링크] Java에서 XML없이 SQL개발하기

기본

최근에 MyBatis 어노테이션을 사용해보니 간단한 쿼리는 괜찮은데 쿼리가 길어지면 한 줄에 다 써놓자니 너무 길고 +로 연결하자니 다른 툴 사용할 때, 여간 불편한게 아니더라구요.

Groovy에서 삼중 큰따옴표(“””) 를 지원 하는지도 몰랐었는데, “”” 이렇게 스트링을 시작하면 문자열을 리터럴 그대로 인식할 수 있다고 합니다.(각각의 라인은 \n으로 처리됩니다.) Groovy는 JVM위에서 돌아가는 언어고, Java 문법도 다 지원해주죠. 이클립스에서 플러그인만 하나 추가해 주면 사용하는데 불편함이 없으니 정상혁님의 글대로 쿼리 유틸 클래스를 Groovy로 만들어 주면 매우 편할 것 같다는 생각이 들었습니다 🙂