6/10/2014

Unit Test for Web Services Client who use Spring Template

Recently was doing one client project which will call the web services provided by our core platform. That project is using SpringTemplate to call the Web services. How to write Junit tests for the client code brought up my interest.

After doing some research, the most commonly used method is to use Mock, of course. Some uses Mockito or EasyMock, which are all fine to me. But what I thought was we are using Spring framework anyway, there should be some Mock classes specially for SpringTemplate, and it should already be there. Come on, it's SPRING! In this case, we do not need to write Mock classed for SpringTemplate ourselves. There it is, as I thought, there are plenty of Mock classes sitting there for easy usage.

I made several test cases, although setup the dependencies costs me some time, but that is because of our project's limitation, which is not...you know. All the test cases are easy to implement, and covers well. I'm pretty satisfied. So just gives a record here, because I think it's interesting and the problems that I have encounted may have been faced by other devs too.

Dependencies:

The lib that I use is

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test-mvc</artifactId>
    <version>1.0.0.M2</version>
    <scope>test</scope>
</dependency>

Actually this library has already been integrated into spring-test in Spring version 3.2.X, but our projects are all using Spring 3.1.3.RELEASE. So I have to add it saparately. And be cautious on the dependency conflict(This will really cause trouble...when there are multiple versions of lib exist or same name of class exist but with different content and if the wrong lib/class is loaded, the class not found exception/method not found exception will be thrown, you dont want to see this...).

There are several ways to check the dependencies.

1. Use mvn dependency:tree/mvn dependency:analyze. It will print the structure of the dependencies.
2. You can check on the website: http://mvnrepository.com/artifact/junit/junit/4.10. It will normally tells you which dependencies the jar depends on.
3. You could do Ctrl+shift+T in Eclipse to check how many classes with same name exist in your work space.

The best practice is to make sure there is no dependency conflict. You will never know when it will gives you that exception.

Junit 4.10 and Mockito-all:1.9.5 CANNOT be used, because they all include hamcrest-core:1.1 inside its package which will not work with spring-test-mvc:1.0.0.M2 and there is no way to exclude them. So use Junit 4.11 and Mockito-core:1.9.5 instead.

But, Junit 4.11 and Mockito-core:1.9.5 both have dependency org.hamcrest:hamcrest-core, but they require different version of them. So they are not compatible, and we need to exclude hamcrest-core in both of them and use org.hamcrest:hamcrest-all:1.3

So the dependencies that I use is like:

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11/version>
    <exclusions>
        <exclusion>
            <artifactId>hamcrest-core</artifactId>
            <groupId>org.hamcrest</groupId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>1.9.5</version>
    <exclusions>
        <exclusion>
            <artifactId>hamcrest-core</artifactId>
            <groupId>org.hamcrest</groupId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-all</artifactId>
    <version>1.3</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test-mvc</artifactId>
    <version>1.0.0.M2</version>
    <scope>test</scope>
</dependency>

So if you are using different versions of the dependencies, check before you put it!

Ok, we can go to the main part.

Code to be tested: 

The code that I was testing is:

    @SuppressWarnings("rawtypes")
    @Override
    public long count(UserRef ref) throws IntegrationException {
        try {
            UserMessageCountCriteria criteria = new UserMessageCountCriteria();
            criteria.setIncludeAgentMessage(true);
            criteria.setOrgName(ref.getLoginOrganizationName());
            criteria.setReadStatus(MessageReadStatusType.unRead);
            criteria.setLabels(new ArrayList<String>());
            criteria.getLabels().add(serviceInfo.getMessageCountLabel());
            criteria.setUserName(ref.getUser().getUserName());
            
            ResponseEntity<Map> responseEntity = null;
            HttpEntity<Map<String, Object>> entity = new HttpEntity<Map<String, Object>>(this.getUserAuthRequestMap(criteria, ref), this.getHttpHeaders());
            responseEntity = restTemplate.exchange(serviceInfo.getServiceMessageCountUrl(), HttpMethod.POST, entity, Map.class);
            
            return (Long) getResponseBody(responseEntity, Long.class);
        } catch (Exception ex) {
            ServiceExceptionHandler.handle(ex, this.getClass().getSimpleName());
        }
        
        return 0L;
    }

protected Object getResponseBody(ResponseEntity<Map> responseEntity, Class clazz) throws IntegrationException {

        if (LOGGER.isDebugEnabled()) {
            try {
                LOGGER.debug(JsonCodec.marshal(responseEntity));
            } catch (Exception ex) {
                throw new IllegalArgumentException(ex);
            }
        }

        if (responseEntity == null) {
            throw new RuntimeException("Response is empty");
        }

        HttpHeaders headers = responseEntity.getHeaders();
        if (headers == null) {
            throw new RuntimeException("Response Header is empty");
        }

        Map<String, Object> responseBody = responseEntity.getBody();
        Map<String, String> status = (Map<String, String>) responseBody.get("status");
        String statusCode = status.get("code");
        String statusMsg = status.get("message");
        if (!StringUtils.equals(statusCode, serviceInfo.getAPISuccessCode())) {
            throw new IntegrationException(statusCode, statusMsg);
        }

        Object object = responseBody.get("content");
        if (clazz == null) {
            return object;
        }
        
        if(object == null)
            return null;

        try {
            String json = JsonCodec.marshal(object);
            if (StringUtils.isEmpty(json)) {
                return null;
            }
            return JsonCodec.unmarshal(clazz, json);
        } catch (Exception ex) {
            throw new IllegalArgumentException(ex);
        }
    }

It's pretty basic.

Test Case:

First I create one Base class to setup my Mock server, this class can be extended by all the other service test class.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath:spring/test-portalintegration-servlet.xml" })
public class ServiceClientImplTestBase {

    private static final Logger logger = Logger.getLogger(ServiceClientImplTestBase.class);
    
    protected MockRestServiceServer mockServer;
    
    @Autowired
    private ApplicationContext appContext;
    
    @Autowired
    protected MessageServiceClient messageServiceClient;
    
    @Autowired
    protected ServiceInfo serviceInfo;
    
    @Before
    public void setUp() {
        mockServer = MockRestServiceServer.createServer((RestTemplate) appContext.getBean("restTemplate")); // (1)
        
    }
    
    protected String loadFile(String path){
        
        byte[] content;
        try {
            content = IOUtils.toByteArray(this.getClass().getClassLoader().getResourceAsStream(path));
        } catch (IOException e) {
            logger.error(String.format("failed to load file from classpath: %s", path), e);
            return null;
        }
        
        return new String(content);
    }
}

Then I create one class to test one of the service client.

public class MessageServiceClientImplTest extends ServiceClientImplTestBase{
    @Test
    public void countTest() throws Exception {
        mockServer.expect(RequestMatchers.requestTo(serviceInfo.getServiceMessageCountUrl()))
        .andExpect(RequestMatchers.method(HttpMethod.POST))
        .andRespond(ResponseCreators.withSuccess(loadFile("testdata/sample-response/message-count-success-response.json"), MediaType.APPLICATION_JSON)); // (2)

        UserRef userRef = this.getUserRef("dummy"); //this method just helps me create one dto. Nothing special.
        
        long result = messageServiceClient.count(userRef);
        
        Assert.assertEquals(1, result); // (3)
        mockServer.verify(); // (4)
    }
}

Then you can run the countTest() and it works perfect.

This example is just a very basic Mock, showing you how it works. Depends on your service and client requirement, you need to customize the expect rule to verify the functionality of your client impl.

This is really easy. I like it.

End.