Friday, October 18, 2013

Testing REST Client using MockRestServiceServer

Problem : I have a Web Application which is deployed on tomcat server and I needed to write unit test cases which can test all the controllers. Secondly It internally also hits another Web server using RestTemplate that means for the success of all unit test cases both the applications should be up and running. 
 
Solution : After spending some time on Google, I found that Spring provide a new feature “MockRestServiceServer” by which testing a REST Client is simple. This new feature is in Spring 3.2.x but also available in Spring 3.1.x via the extra jar named “spring-test-mvc”. So, You can test your Spring MVC controllers very easy without starting any Web Application.

MockRestServiceServer takes the approach of mocking the server and allowing you to specify expected behavior and responses in your junit test class. It also handles server exception classes.
MockRestRequestMatchers offers many hamcrest matchers to check your request URL, headers, HTTP method, and even json and xpath matchers to check body content. MockRestResponseCreators allows you to easily build both success and error responses.

This blog will show you how the mock server works.

My Configuration :
  1. Spring 3.1.x
  2. Junit 4

Step 1 : Add following dependency in your pom.xml
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.2</version>
<scope>test</scope>
</dependency>
<!-- This is not in Maven Central yet must include Spring snapshot repository -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test-mvc</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<scope>test</scope>
</dependency>

<!-- For testing against latest Spring snapshots -->
<repositories>
<repository>
<id>org.springframework.maven.snapshot</id>
<name>Spring Maven Snapshot Repository</name>
<url>http://maven.springframework.org/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository >
</repositories>

Step 2: The next step is to define a Spring MVC controller that we will be writing unit test for :

@Controller
@RequestMapping("/test")
public class RestController {
private static final Logger LOG = LoggerFactory
.getLogger(RestController.class);
@Autowired
RestTemplate template;
@Autowired
IRestService iRestService;
@RequestMapping(value = "/google", method = RequestMethod.GET)
@ResponseBody
public String hitGoogle() {
LOG.info("Notification : hit google command received from REST");
return iRestService.hitGoogle();
}
}

Step 3: Write Impl class which actually perform REST Client operation to get information from some another end Web application.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestTemplate;

import com.waheed.mockserver.service.IRestService;

@Service
public class RestServiceImpl implements IRestService {

private static final Logger LOG = LoggerFactory
.getLogger(RestServiceImpl.class);

@Autowired
RestTemplate restTemplate;

/**
* REST client that makes a call to a URL and handle success and error by
* returning them in error String.
*/
public String hitGoogle() {
LOG.info("Testing google :");
String response;
try {
response = restTemplate.getForObject("http://google.com",
String.class);
LOG.info("Response : {}", response);
} catch (HttpStatusCodeException e) {
LOG.error("Error while testing google", e);
return "FAILED : " + e.getStatusCode();
} catch (Exception e) {
LOG.error("Error while testing google", e);
return "FAILED : " + e.getStackTrace();
}
return response;
}
}
Step 4 : Finally the unit test class that uses Spring-test-mvc to mock the controller that returns the response as String.

import static org.junit.Assert.assertEquals;
import static org.springframework.test.web.client.match.RequestMatchers.method;
import static org.springframework.test.web.client.match.RequestMatchers.requestTo;
import static org.springframework.test.web.client.response.ResponseCreators.withSuccess;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.web.client.RestTemplate;

import com.waheed.mockserver.controller.RestController;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="file:WebContent/WEB-INF/spring-servlet.xml")
public class TestMockRestServiceServer {

@Autowired
private RestTemplate restTemplate;

private MockRestServiceServer mockServer;
@Autowired
RestController restController;

// Execute the Setup method before the test.
@Before
public void setUp() {
//create a mock Server instance for RestTemplate
mockServer = MockRestServiceServer.createServer(restTemplate);
}

@Test
public void testGoogleSuccess() {
mockServer
.expect(requestTo("http://google.com"))
.andExpect(method(HttpMethod.GET))
.andRespond(
withSuccess("SUCCESS",
MediaType.TEXT_PLAIN));
String response = restController.hitGoogle();
mockServer.verify();
assertEquals("SUCCESS",response);
}
}
Here :
@RunWithSpring(SpringJunit4ClassRunner.class) runs the test with Spring custom runner that will load Spring application context from @ContextConfiguration(locations="file:WebContent/WEB-INF/spring-servlet.xml")

@Before annotation executes the setup method before the test. It will create the mockServer for the restTemplate Instance. 
@Test executes the actual test
    mockServer.expect(requestTo("URL")).andExpect(method(Method Type))
.andRespond(withSuccess("RESPONSE MESSAGE”,”MESSAGE FROMAT LIKE JSON OR XML”));

If the controller hit some URL which has exactly the same URL and method type as we have set above then the spring-test-mvc will mock the response as set above.

Note : You must have observed that in unit test case there are few imports which are static. To find the Static imports in your Eclipse IDE you have to configure a bit. Goto Java>Editor>Content Assist>Favorites and Add

    If using Spring 3.2.x:
    org.springframework.test.web.client.match.MockRestRequestMatchers
    org.springframework.test.web.client.response.MockRestResponseCreators

    If using Spring 3.1.x, the static import classes are named differently:
    org.springframework.test.web.client.match.RequestMatchers
    org.springframework.test.web.client.response.ResponseCreators

Common issue:

java.lang.SecurityException: class "org.hamcrest.TypeSafeMatcher"'s signer information does not match signer information of other classes in the same package
at java.lang.ClassLoader.checkCerts(Unknown Source)

There are few ways to resolve the above issue :
Mockito use Hamcrest 1.1 API in the classpath and Eclipse JUnit 4 distribution also has the Hamcrest Core 1.1 API.
  1. You may get this error when Hamcrest API is invoked from Eclipse JUnit’s and Not Mockito. The solution is change the ordering of the jar files in “Order & Export” tab of Eclipse build path.
  2. Or simply removing the JUnit 4 library also fixes the problem apparently
     (junit and hamcrest will be in the maven deficiencies anyway).
You can download the above tutorial form here. 
 
References :
http://docs.spring.io/spring/docs/3.2.x/javadoc-api/index.html?org/springframework/test/web/client/MockRestServiceServer.html
https://github.com/spring-projects/spring-framework/tree/master/spring-test-mvc/src/test/java/org/springframework/test/web/client/match

1 comment:

  1. Thanks man you made my day I was stuck on writing the test case on a deadline project and your example provided me exactly what I was supposed to do.

    ReplyDelete

How TOPT Works: Generating OTPs Without Internet Connection

Introduction Have you ever wondered how authentication apps like RSA Authenticator generate One-Time Passwords (OTPs) without requiring an i...