Sunday, April 27, 2014

Restfull services in Java using Apache CXF with integration testing using embedded JETTY - Part 2

This post is following previous one. In previous one it was only one service which was returning text message. Now - "level up" - it will be few DTO's,  few services for them and of course few integration tests. To decrease amount of code I will use generics and inheritance.

Structure of application:



Main (pom.xml) file:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.demien</groupId>
    <artifactId>cxf-sprig-rest-js</artifactId>
    <packaging>war</packaging>
    <version>1.0.0</version>

    <name>CXF Spring REST JS</name>
    
    <properties>
        <maven.test.skip>true</maven.test.skip>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring.version>3.1.1.RELEASE</spring.version>
    </properties>   
    
    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.cxf</groupId>
            <artifactId>cxf-rt-frontend-jaxrs</artifactId>
            <version>2.5.3</version>
        </dependency>

        <dependency>
            <groupId>javax.ws.rs</groupId>
            <artifactId>jsr311-api</artifactId>
            <version>1.1.1</version>
        </dependency>

        <!-- TEST -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.8.2</version>
            <scope>test</scope>
        </dependency>
                
        <dependency>
            <groupId>org.codehaus.jackson</groupId>
            <artifactId>jackson-jaxrs</artifactId>
            <version>1.9.11</version>
        </dependency>       
        
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-server</artifactId>
            <version>9.1.0.M0</version>
        </dependency>

        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-webapp</artifactId>
            <version>9.1.0.M0</version>
        </dependency>
        
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.3.3</version>
        </dependency>

    </dependencies>
    
    <build>
        <finalName>spring-cxf-rest-js</finalName>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                </configuration>
            </plugin>
        </plugins>
    </build>


</project>



1. DTO's(domain package)

It will be three DTO objects : Region, Country and Location and they will be  inherited from BaseDomain class. I made it this way just for ID field which will be present in every my DTO.
 So listing of BaseDomain:

package com.demien.cxfspringrestjs.domain;

public abstract class BaseDomain {
    private Integer id;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }
    
}


I will show example of only one - Location DTO because other are similar: 



package com.demien.cxfspringrestjs.domain;

import java.io.Serializable;

import javax.xml.bind.annotation.XmlRootElement;

//@XmlRootElement(name = "Location")
public class Location extends BaseDomain implements Serializable {

    private static final long serialVersionUID = 3477913378452357045L;
    private String streetAdress;
    private String postalCode;
    private String city;
    private String stateProvince;
    private Country country;

    public Location() {
    }

    public String getStreetAdress() {
        return streetAdress;
    }

    public void setStreetAdress(String streetAdress) {
        this.streetAdress = streetAdress;
    }

    public String getPostalCode() {
        return postalCode;
    }

    public void setPostalCode(String postalCode) {
        this.postalCode = postalCode;
    }

    public String getCity() {
        return city;
    }

    public void setCity(String city) {
        this.city = city;
    }

    public String getStateProvince() {
        return stateProvince;
    }

    public void setStateProvince(String stateProvince) {
        this.stateProvince = stateProvince;
    }

    public Country getCountry() {
        return country;
    }

    public void setCountry(Country country) {
        this.country = country;
    }

    @Override
    public String toString() {
        return "[ Id=" + getId() + ", streetAdress=" + streetAdress
                + ", postalCode=" + postalCode + ", city=" + city
                + ", stateProvince=" + stateProvince + ", country=" + country
                + ']';
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Location)) {
            return false;
        }
        Location location = (Location) o;
        if (this.getId().equals(location.getId())) {
            return true;
        } else {
            return false;
        }
    }

}


As you can see, it's a regular DTO. On class definition I put annotation @XmlRootElement which is commented. If you want to work with objects in XML format - you need that annotation(uncomment it) for Jaxb parser. In this example I use JSON format, objects are transmitted as Json strings so I don't need that annotation.

2.Utilites.

In project there are some util classes. Some of them were present in previous post(RestTestClient, RestTestServer), some(JsonHelper, ObjectDataPopulator) are new.   

2.1 JsonHelper - utility for converting objects Json->Object, Object->Json.





package com.demien.cxfspringrestjs.utils;

import java.io.IOException;
import java.util.List;

import org.codehaus.jackson.JsonGenerationException;
import org.codehaus.jackson.map.JsonMappingException;
import org.codehaus.jackson.map.ObjectMapper;

public class JsonHelper {
    
    private static ObjectMapper mapper = new ObjectMapper(); 
    
    public static Object Json2Object(String json, Class cl) throws JsonHelperException {    
        return Json2ObjectMain(json, cl, false);
    }   
    
    public static Object Json2ObjectList(String json, Class cl) throws  JsonHelperException{
        return Json2ObjectMain(json, cl, true);
    }
    
    private static Object Json2ObjectMain(String json, Class cl, boolean asList) throws JsonHelperException {
        if (json==null || json.length()==0) {
            return null;
        }
        Object result=null;
        try {
            if (asList) {
                result=mapper.readValue(json, mapper.getTypeFactory().constructCollectionType(List.class, cl));
            } else {
                result=mapper.readValue(json, cl);  
            }
        } catch (Exception e) {
            throw new JsonHelper.JsonHelperException("Exception in Json2Object. Exception:"+e.getMessage()+". JSON="+json);
        }
        
        return result;
    }
    
    public static String object2json(Object object) throws JsonGenerationException, JsonMappingException, IOException {
        String result=mapper.writeValueAsString(object);
        return result;
    }
public static class JsonHelperException extends Exception { public JsonHelperException(String message) { super(message); } }}

2.2. Object data populator 

It's util for tests - it fills empty object with random generated values.  Here I made population only for Integer and String type. In other projects it has to be extended for supporting other types. 

package com.demien.cxfspringrestjs.utils;


import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

import com.demien.cxfspringrestjs.domain.BaseDomain;
import com.demien.cxfspringrestjs.domain.Country;

public class ObjectDataPopulator {

    public static void main(String[] args) throws IllegalArgumentException,
            IllegalAccessException, InstantiationException {

        Country country = new Country();
        country = (Country) ObjectDataPopulator.populate(country);
        System.out.println(country.toString());
    }

    public static Object populate(BaseDomain instance)
            throws IllegalArgumentException, IllegalAccessException,
            InstantiationException {
        instance.setId(getRandomInteger());
        List<Field> fields = getAllFields(instance);
        for (Field eachField : fields) {
            eachField.setAccessible(true);
            if (eachField.getType().equals(Integer.class)) {
                eachField.set(instance, getRandomInteger());
            } else if (eachField.getType().equals(String.class)) {
                eachField.set(instance, getRandomString());
             // here should be generators for other standard types like Float, Long....
            } else {
                // processing aggregated objects
                if (eachField.getType().newInstance() instanceof BaseDomain) {
                    eachField.set(instance, populate((BaseDomain) eachField
                            .getType().newInstance()));
                }               
            }
        }

        return instance;

    }

    private static Integer getRandomInteger() {
        return new Integer((int) (Math.random() * 1000));
    }

    private static String getRandomString() {
        StringBuffer result = new StringBuffer();
        String[] letters = new String[] { "A", "B", "C", "D", "E", "F", "G" };
        int length = (int) (Math.random() * 15) + 5;
        for (int i = 0; i < length; i++) {
            int pos = (int) (Math.random() * letters.length);
            result.append(letters[pos]);
        }
        return result.toString();
    }

    private static List<Field> getAllFields(Object instance) {
        Field[] fields = instance.getClass().getDeclaredFields();
        List<Field> result = new ArrayList<Field>();
        for (int i = 0; i < fields.length; i++) {
            if (!java.lang.reflect.Modifier.isFinal(fields[i].getModifiers())
                    && !java.lang.reflect.Modifier.isStatic(fields[i]
                            .getModifiers())) {
                result.add(fields[i]);
            }
        }
        return result;
    }
}

2.3 RestTestClient
This class was extended. In previous post it supported GET requests only. Here it is supporing  POST requests also. 

package com.demien.cxfspringrestjs.utils;

import java.io.BufferedReader;

import org.apache.http.NameValuePair;

import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;

import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;

import com.demien.cxfspringrestjs.utils.JsonHelper.JsonHelperException;

public class RestTestClient {

    public static final int RESPONSE_OK = 200;

    public static class GetResponceDataException extends Exception {
        private static final long serialVersionUID = 5296532593594106875L;

        public GetResponceDataException(String message) {
            super(message);
        }
    }

    public static String getResponseData(HttpResponse response)
            throws Exception {
        StringBuilder result = new StringBuilder();
        try {
            BufferedReader rd = new BufferedReader(new InputStreamReader(
                    response.getEntity().getContent()));
            String line = "";
            while ((line = rd.readLine()) != null) {
                result.append(line);
            }
        } catch (Exception e) {
            throw new Exception("Exception in getResponseData:"
                    + e.getMessage() + ".");
        }
        return result.toString();
    }

    private Object Json2Object(String src, Class cl, boolean asList)
            throws JsonHelperException {
        Object result = null;
        if (asList) {
            result = JsonHelper.Json2ObjectList(src, cl);
        } else {
            result = JsonHelper.Json2Object(src, cl);
        }
        return result;
    }

    public HttpResponse sendGet(String url) throws ClientProtocolException,
            IOException {
        HttpClient client = HttpClientBuilder.create().build();
        HttpGet get = new HttpGet(url);
        get.setHeader(
                "Accept",
                "text/html,application/xhtml+xml,application/xml,application/json;q=0.9,*/*;q=0.8");
        get.setHeader("Content-Type", "application/json");
        HttpResponse response = client.execute(get);

        return response;
    }

    public String sendGetStringResult(String url) throws Exception {
        return getResponseData(sendGet(url));
    }

    public Object sendGetObjectResult(String url, Class cl) throws Exception {
        return sendGetObjectResult(url, cl, false);
    }

    public Object sendGetObjectResult(String url, Class cl, boolean asList)
            throws Exception {
        String src = sendGetStringResult(url);
        Object result = null;
        if (asList) {
            result = JsonHelper.Json2ObjectList(src, cl);
        } else {
            result = JsonHelper.Json2Object(src, cl);
        }
        return result;
    }

    public HttpResponse sendPost(String url, List<NameValuePair> postParams)
            throws Exception {

        HttpClient client = HttpClientBuilder.create().build();
        HttpPost post = new HttpPost(url);
        post.setHeader("Accept",
                "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
        post.setHeader("Content-Type", "application/x-www-form-urlencoded");

        if (postParams != null) {
            post.setEntity(new UrlEncodedFormEntity(postParams));
        }

        HttpResponse response = client.execute(post);

        return response;
    }

    public HttpResponse sendPost(String url, Object object) throws Exception {

        HttpClient client = HttpClientBuilder.create().build();
        HttpPost post = new HttpPost(url);
        String json = JsonHelper.object2json(object);
        StringEntity input = new StringEntity(json);
        input.setContentType("application/json");
        post.setEntity(input);

        post.setHeader(
                "Accept",
                "text/html,application/xhtml+xml,application/xml,application/json;q=0.9,*/*;q=0.8");
        post.setHeader("Content-Type", "application/json");
        HttpResponse response = client.execute(post);

        return response;
    }

    public String sendPostStringResult(String url,
            List<NameValuePair> postParams) throws IllegalStateException,
            IOException, Exception {
        return getResponseData(sendPost(url, postParams));
    }

    public Object sendPostObjectResult(String url,
            List<NameValuePair> postParams, Class cl)
            throws IllegalStateException, IOException, Exception {
        return sendPostObjectResult(url, postParams, cl, false);
    }

    public Object sendPostObjectResult(String url,
            List<NameValuePair> postParams, Class cl, boolean asArray)
            throws IllegalStateException, IOException, Exception {
        String src = sendPostStringResult(url, postParams);
        return Json2Object(src, cl, asArray);
    }

}

2.4 RestTestServer
This class was not changed from previous post.
 

package com.demien.cxfspringrestjs.utils;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.WebAppContext;

public class RestTestServer {
    public final static String TEST_CONTEXT = "/spring-cxf-rest";
    public final static int TEST_PORT = 8080;
    private Server server;
    private String mode;
    
    public RestTestServer() {
        
    }
    
    public RestTestServer(String mode) {
        this.mode=mode;
    }   

    public void start() throws Exception {

        if (server == null) {
            server = new Server(TEST_PORT);
            WebAppContext context = new WebAppContext();
            context.setDescriptor("src/main/webapp/WEB-INF/web.xml");
            context.setResourceBase("src/main/webapp");
            context.setContextPath(TEST_CONTEXT);
            context.setParentLoaderPriority(true);
            server.setHandler(context);
            server.start();
            System.out.println("Server started at http://localhost:"+TEST_PORT+TEST_CONTEXT);
            if (mode==null || !mode.equals("JUNIT")) {
                server.join();
            }
            //
        }
        //         
    }
    
    public void stop() throws Exception
    {
        if (server != null)
        {
            server.stop();
            server.join();
            server.destroy();
            server = null;
        }
    }
}

3. Services. 

As I said before, to decrease amount of code I will use generic and inheritance: all logic will be in BaseService class and other services(RegionService, CountryService, LocationService) will be just empty classes with service path declaration only!
BaseService class is the "mastermind" of application - all rest service logic(all rest services) defined here:


  


package com.demien.cxfspringrestjs.service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Response;

import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.JsonMappingException;

import com.demien.cxfspringrestjs.domain.BaseDomain;
import com.demien.cxfspringrestjs.utils.JsonHelper;
import com.demien.cxfspringrestjs.utils.JsonHelper.JsonHelperException;

public class BaseService<T extends BaseDomain> {

    private List<T> entityList = new ArrayList<T>();
    public static final String HELLO = "Hello world!";
    
    public static final String SERVICE_HELLO="sayhello";
    public static final String SERVICE_ENTITY_LIST="entitylist";
    public static final String SERVICE_GETBYID="getbyid";
    public static final String SERVICE_ADD="add";
    public static final String SERVICE_UPDATE="update";
    public static final String SERVICE_DELETE="delete";
    
    private Class<T> cl;
    
    public BaseService(Class<T> cl) {
        this.cl=cl;
    }

    @GET
    @Path("/sayhello")
    public String sayHello() {
        return HELLO;
    }

    @GET
    @Path("/entitylist")
    @Produces("application/json")
    public Collection<T> getEntityList() {
        return entityList;
    }

    public T getEntityById(Integer id) {
        T result = null;
        for (T entity : entityList) {
            if (entity.getId().equals(id)) {
                result = entity;
            }
        }
        return result;
    }

    @GET
    @Path("/getbyid")
    @Produces("application/json")
    public Response getEntity(@QueryParam("id") Integer id) {
        T result =getEntityById(id);
        return Response.ok(result).build();
    }

    @POST
    @Path("/add")
    @Consumes("application/json")
    @Produces("application/json")
    public Response addEntity(String json) throws JsonHelperException  {
        T entity=(T)JsonHelper.Json2Object(json, cl);
        
        T searchResult = getEntityById(entity.getId());
        if (searchResult == null) {
            entityList.add(entity);
        }
        return Response.ok(entity).build();
    }

    @POST
    @Path("/update")
    @Consumes("application/json")
    @Produces("application/json")
    public Response updateEntity(String json) throws JsonHelperException {
        T entity=(T)JsonHelper.Json2Object(json, cl);
        T forUpdate = getEntityById(entity.getId());
        if (forUpdate != null) {
            forUpdate = entity;
        }
        return Response.ok(forUpdate).build();
    }

    @POST
    @Path("/delete")
    @Consumes("application/json")
    @Produces("application/json")
    public Response deleteEntity(String json) throws JsonHelperException {
        T entity=(T)JsonHelper.Json2Object(json, cl);
        entityList.remove(entity);
        return Response.ok().build();
    }

}

Services RegionService, CountryService, LocationService which are visible "outside" for rest clients are just extending BaseService class with defenition of service base URL. Example of LocationService: 




package com.demien.cxfspringrestjs.service;

import javax.jws.WebService;
import javax.ws.rs.Consumes;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import com.demien.cxfspringrestjs.domain.Location;

@WebService(serviceName = "locationService")
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
@Path("/locationService")
public class LocationService extends BaseService<Location> {
    
    public LocationService(){
        super(Location.class);
    }
}

Other (RegionService, CountryService) are similar, differences are : another path in @Path annotation ("/regionService" and "/countryService") and another classes in constructor (Region.class and Country.class). 

All methods with services for add, update delete are inherited from BaseService class. So, in case of any changes in logic, only one class have to be changed, and that is great! 


4. Application context file

It is similar to previous post, only new services was added:


<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jaxrs="http://cxf.apache.org/jaxrs"
    xmlns:http-conf="http://cxf.apache.org/transports/http/configuration"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
        http://cxf.apache.org/jaxrs http://cxf.apache.org/schemas/jaxrs.xsd 
        http://cxf.apache.org/transports/http/configuration http://cxf.apache.org/schemas/configuration/http-conf.xsd"
    default-lazy-init="false">


    <import resource="classpath:META-INF/cxf/cxf.xml" />
    <import resource="classpath:META-INF/cxf/cxf-servlet.xml" />

    <context:component-scan base-package="com.demien" />


    <jaxrs:server id="myServices" address="/rest">
        <jaxrs:providers>
            <bean class="org.codehaus.jackson.jaxrs.JacksonJsonProvider" />
        </jaxrs:providers>
        <jaxrs:serviceBeans>
            <ref bean="RegionService" />
            <ref bean="CountryService" />           
            <ref bean="LocationService" />
        </jaxrs:serviceBeans>
        <jaxrs:extensionMappings>
            <entry key="xml" value="application/xml" />
            <entry key="json" value="application/json" />
        </jaxrs:extensionMappings>
    </jaxrs:server>
        
    <bean id="RegionService" class="com.demien.cxfspringrestjs.service.RegionService"/>
    <bean id="CountryService" class="com.demien.cxfspringrestjs.service.CountryService"/>
    <bean id="LocationService" class="com.demien.cxfspringrestjs.service.LocationService"/>

</beans>

5. Testing

Integration testing part in similar to previous post, but we have to test several services and they have several methods. For that I used similar services inheritance approach: one BaseServiceTest generic class with whole test logic, and three empty test classes(RegionServiceTest, CountryServiceTest, LocationServiceTest) which are extending BaseServiceTest class. All @Test methods will be inherited - so we have to define them only one time - in BaseServiceTest class. 



package com.demien.services;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.io.IOException;
import java.util.List;

import org.apache.http.HttpResponse;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

import com.demien.cxfspringrestjs.domain.BaseDomain;
import com.demien.cxfspringrestjs.service.BaseService;
import com.demien.cxfspringrestjs.utils.ObjectDataPopulator;
import com.demien.cxfspringrestjs.utils.RestTestClient;
import com.demien.cxfspringrestjs.utils.RestTestServer;

public abstract class BaseServiceTest<T extends BaseDomain> {
    protected static RestTestServer server = new RestTestServer("JUNIT");
    protected static RestTestClient client = new RestTestClient();

    protected final String BASE_URL = "http://localhost:"
            + RestTestServer.TEST_PORT + RestTestServer.TEST_CONTEXT
            + "/services/rest/";
    protected final String SERVICE_URL;
    private final Class<T> cl;

    public BaseServiceTest(String sevicePrexif, Class<T> cl) {
        SERVICE_URL = BASE_URL + sevicePrexif + "/";
        this.cl = cl;
    }

    private T getTestEntity() throws InstantiationException,
            IllegalAccessException {
        T entity = cl.newInstance();
        ObjectDataPopulator.populate(entity);
        return entity;
    }

    @BeforeClass
    public static void init() throws Exception {
        server.start();
    }

    @AfterClass
    public static void finish() throws Exception {
        server.stop();
    }

    @Test
    public void sayHello() throws Exception {
        String value = client.sendGetStringResult(SERVICE_URL
                + BaseService.SERVICE_HELLO);
        System.out.println("value=" + value);
        assertEquals(BaseService.HELLO, value);
    }

    @SuppressWarnings("unchecked")
    private T getById(Integer id) throws Exception {
        T result = (T) client.sendGetObjectResult(SERVICE_URL
                + BaseService.SERVICE_GETBYID + "?id=" + id, cl);
        return result;
    }

    private void addEntity(T entity) throws Exception {
        HttpResponse response = client.sendPost(SERVICE_URL
                + BaseService.SERVICE_ADD, entity);
        int responseCode = response.getStatusLine().getStatusCode();
        assertEquals(RestTestClient.RESPONSE_OK, responseCode);
    }

    private void updateEntity(T entity) throws Exception {
        HttpResponse response = client.sendPost(SERVICE_URL
                + BaseService.SERVICE_UPDATE, entity);
        int responseCode = response.getStatusLine().getStatusCode();
        assertEquals(RestTestClient.RESPONSE_OK, responseCode);
    }

    private void deleteEntity(T entity) throws Exception {
        HttpResponse response = client.sendPost(SERVICE_URL
                + BaseService.SERVICE_DELETE, entity);
        int responseCode = response.getStatusLine().getStatusCode();
        assertEquals(RestTestClient.RESPONSE_OK, responseCode);
    }

    @SuppressWarnings({ "unchecked" })
    private List<T> getEntityList() throws IllegalStateException, IOException,
            Exception {

        List<T> result = (List<T>) client.sendGetObjectResult(SERVICE_URL
                + BaseService.SERVICE_ENTITY_LIST, cl, true);
        return result;
    }

    @Test
    public void addEntityTest() throws Exception {
        T testEntity = getTestEntity();
        addEntity(testEntity);

        T resultEntity = getById(testEntity.getId());
        assertNotNull(resultEntity);
        assertEquals(testEntity, resultEntity);
    }

    @Test
    public void updateEntityTest() throws Exception {
        T testEntity = getTestEntity();
        addEntity(testEntity);
        Integer id = testEntity.getId();

        T forUpdate = getTestEntity();
        forUpdate.setId(id);
        updateEntity(forUpdate);

        T searchEntity = getById(id);
        assertEquals(forUpdate, searchEntity);
    }

    @Test
    public void deleteEntityTest() throws Exception {
        T testEntity = getTestEntity();
        addEntity(testEntity);
        Integer id = testEntity.getId();

        deleteEntity(testEntity);

        T searchEntity = getById(id);
        assertNull(searchEntity);
    }

    @Test
    public void getEntityListTest() throws Exception {
        T testEntity1 = getTestEntity();
        addEntity(testEntity1);
        T testEntity2 = getTestEntity();
        addEntity(testEntity2);

        List<T> entityList = getEntityList();
        assertNotNull(entityList);
        assertTrue(entityList.contains(testEntity1));
        assertTrue(entityList.contains(testEntity2));

    }

}

And now all we have to do is to extend this class and add correct constructor with service URL and data class. Example of LocationServiceTest (others are similar):


package com.demien.services;

import com.demien.cxfspringrestjs.domain.Location;

public class LocationServiceTest extends BaseServiceTest<Location> {
    public LocationServiceTest() {
        super("locationService", Location.class);
    }

}

And that's it! No code except constructor! All @Test methods will be inherited! 

Let's run test for our application: 



For every(which are "empty" - only constructor) test service class (RegionServiceTest, CountryServiceTest, LocationServiceTest) were executed 5 test (total 5*3=15) which were defined in parent BaseTestService class. 


 

Source codes can be downloaded from here.