Quality Control: get rid of the CRUD and get REST

Introduction

I know that the title sucks but I’m pretty sure that it will make the entry float to the top in Google (ha!). The title is not all bad as we are looking to extend the project that we have been blogging about. The last instalment was: Extending the CRUD Example Utilizing JAX-B and Jersey’s JSON. We are introducing a couple of quality assurance type of application into the project: Cobertura and PMD.  These tools are great for looking over your code and providing constructive feed back.  More about that below.

So far we have been working with out test case!  This was not a Test Driven Development project but going foward we can mend our ways and we have a good infrastructure to forge that path with.  We are dealing with a complex environment in this project which requires special tools and there are some great ones available from Spring and Jersey which work with JUnit4.  In the article we will be testing the code from within the Spring environment and also test the application as a webservice.

If you have been following along from the other articles you will note that we have improved the services to untilze JSON more effectively for communication.  We also stepped back from using a modified Prototype.js library from AJAX to introducing our own, albiet heavily based upon the Prototype.js implementation.

As always the source code complete with a Maven2 pom.xml is available for download at the end of the article.
 

Adding Cobertura and PMD

Cobertura is a free Java tool that calculates the percentage of code accessed by tests. It can be used to identify which parts of your Java program are lacking test coverage. It is based on jcoverage.

PMD scans Java source code and looks for potential problems like:

  • Possible bugs – empty try/catch/finally/switch statements
  • Dead code – unused local variables, parameters and private methods
  • Suboptimal code – wasteful String/StringBuffer usage
  • Overcomplicated expressions – unnecessary if statements, for loops that could be while loops
  • Duplicate code – copied/pasted code means copied/pasted bugs

The combination of these two tools are a great aid to maintaining a robust code base.

All the work required for adding Cobertua and PMD is in the pom.xml.

<reporting>
		<plugins>
			<plugin>
				<!-- mvn pmd:pmd  -->
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-pmd-plugin</artifactId>
				<configuration>
					<targetJdk>1.5</targetJdk>
					<rulesets>
						<ruleset>/rulesets/basic.xml</ruleset>
						<ruleset>/rulesets/unusedcode.xml</ruleset>
						<ruleset>/rulesets/imports.xml</ruleset>
						<ruleset>/rulesets/codesize.xml</ruleset>
						<ruleset>/rulesets/optimizations.xml</ruleset>
						<!--
						<ruleset>/rulesets/internal/all-java.xml</ruleset>
						 -->
					</rulesets>
				</configuration>
			</plugin>
			<!-- Unfortunate not free -->
			<!--
				<plugin> <groupId>com.atlassian.maven.plugins</groupId>
				<artifactId>maven-clover2-plugin</artifactId>
				<version>2.4.2</version> <configuration>
				<generateHtml>true</generateHtml> <generatePdf>true</generatePdf>
				<generateXml>true</generateXml>
				<generateHistorical>true</generateHistorical>
				<targetPercentage>50%</targetPercentage> </configuration> </plugin>
			-->
			<plugin>
				<!-- mvn cobertura:cobertura -->
				<groupId>org.codehaus.mojo</groupId>
				<artifactId>cobertura-maven-plugin</artifactId>
				<configuration>
					<check>
						<branchRate>85</branchRate>
						<lineRate>85</lineRate>
						<haltOnFailure>true</haltOnFailure>
						<totalBranchRate>85</totalBranchRate>
						<totalLineRate>85</totalLineRate>
						<packageLineRate>85</packageLineRate>
						<packageBranchRate>85</packageBranchRate>
						<regexes>
							<regex>
								<pattern>com.persistent.*</pattern>
								<branchRate>90</branchRate>
								<lineRate>80</lineRate>
							</regex>
							<regex>
								<pattern>com.example.boringcode.*</pattern>
								<branchRate>40</branchRate>
								<lineRate>30</lineRate>
							</regex>
						</regexes>
					</check>
				</configuration>
			</plugin>
		</plugins>
	</reporting>

To support these utilities and to write our missing unit tests the following dependencies also need to be added:

<properties>
	<spring.version>2.5.5</spring.version>
	<hibernate.version>3.3.2.GA</hibernate.version>
	<commons-dbcp.version>1.2.2</commons-dbcp.version>
	<commons-logging.version>1.0.4</commons-logging.version>
	<jersey-version>1.1.1-ea</jersey-version>
	<log4j.version>1.2.15</log4j.version>
	<junit.version>4.4</junit.version>
	<commons-lang.version>2.4</commons-lang.version>
</properties>
<dependencies>
	<dependency>
		<groupId>junit</groupId>
		<artifactId>junit</artifactId>
		<version>${junit.version}</version>
		<scope>test</scope>
	</dependency>
	<dependency>
		<groupId>commons-lang</groupId>
		<artifactId>commons-lang</artifactId>
		<version>${commons-lang.version}</version>
	</dependency>
	<dependency>
		<groupId>com.sun.jersey.test.framework</groupId>
		<artifactId>jersey-test-framework</artifactId>
		<version>1.0.3</version>
		<scope>test</scope>
	</dependency>
	<dependency>
		<groupId>org.springframework</groupId>
		<artifactId>spring-test</artifactId>
		<version>${spring.version}</version>
		<scope>test</scope>
	</dependency>

 Both of these utilities can be run from the following Maven2 command:

mvn pmd:pmd cobertura:cobertura

Cobertura Reports

You will find the results of running Cobertura through the index.html file located off the main project directory in target/site/cobertura.  The output will look as follows:

Cobertura report

The main things show here is the code coverage, branch coverage and the complexity of the classes. 

Code coverage insures that there are test cases for each method and branch coverage refers to testing of the various branching code is tested (if statements and the like).  As one navigates into classes the areas that are or are not covered by unit tests are highlighted.

Below is an example of the source code as marked-up by Cobertura.  Note the red highlighting around the if statments.  This red marking indicate that the branch conditions of the statement has not been tested.

Cobertura Branching

 

The code complexity is a calculation based upon the complexity of the graph the code generates.  One can view these number as described in this table:

Table 1. Standard Values of Cyclomatic Complexity

Cyclomatic Complexity Risk Complexity
1-10 a simple program, without much risk
11-20 more complex, moderate risk
21-50 complex, high risk
51+ untestable, very high risk

 

 

PMD Reports

 PMD does a sanity check on your source code.  It gives good insight into where things need fixing up.  A lot of it, at times, might be suggestion on the final on variables.  There are options to setup your own rules so you can go wild.  Here is an example of the out put PMD creates.  Again you can find the HTML results generated off of the target directory in the directory site in the index.html file.

PMD Report

 

 Unit Testing – Java, Springframework, JPA and Jersey

It is wonderful how tools have been created to test projects.  In the cases of Spring and Jersey we have some extraordinary requirements that JUnit tests alone have a difficult time satisfying.  Luckily both the Springframework project and Jersey have made these tools available to us.

Our unit test are broken into two parts.  The first part is checking the Springframework application against the database.  The second part is test the REST Webservice. 

Springframework Unit Testing

Two annotations, at the class level, quickly pull in all the resources required to integrate the Spring context into the JUnit4 environment.  These are the annotations:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/applicationContext.xml" })

 Below is the complete Spring unit test for the project.

package com.persistent.service.jpa;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import java.util.List;

import org.apache.log4j.Logger;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.persistent.entity.Person;
import com.persistent.service.PersonService;

/**
 * This class test the PersonServiceJpa class
 *
 * @author <a href="mailto:david@persistentdesigns.com">DavidSells</a>
 * @version $Id$
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "/applicationContext.xml" })
public class PersonServiceJpaTest {
	private Logger log = Logger.getLogger(this.getClass());
	private static final String NAME = "David Who";
	private static final int AGE = 49;
	private static final int NEW_AGE = 12;
	private static final String NEW_NAME = "Dave Who";

	@Autowired
	PersonService personService;

	@Before
	public void setUp() throws Exception {
		assertNotNull(getPersonService());
	}

	@Test
	public void testGetById() {
		final Person person = getPersonService().getById(1);
		log.info("Person[1] is: "+person);
		assertNotNull(person);
	}

	@Test
	public void testGetAll() {
		List<Person> persons = getPersonService().getAll();
		assertNotNull(persons);
		assertTrue(persons.size()==10);
		log.info("The number of persons in the database is: "+persons.size());
		for (Person person : persons) {
			log.info(person);
		}
	}

	@Test
	public void testSave() {
		final Person person = new Person(NAME,AGE);
		getPersonService().save(person);
		log.info("Saved person: "+person);
		assertTrue("Different id than expected",person.getId() == 11);
		final Person person2 = getPersonService().getById(11);
		assertTrue("The object is not what we expected", person2.getAge()==AGE);
		assertTrue("The object is not what we expected", person2.getName().equals(NAME));
	}

	@Test
	public void testUpdate() {
		final Person person = getPersonService().getById(11);
		assertNotNull(person);
		assertTrue("The object is not what we expected", person.getAge()==AGE);
		assertTrue("The object is not what we expected", person.getName().equals(NAME));
		person.setAge(NEW_AGE);
		person.setName(NEW_NAME);
		getPersonService().update(person);
		final Person person2 = getPersonService().getById(11);
		assertTrue("The object is not what we expected", person2.getAge()==NEW_AGE);
		assertTrue("The object is not what we expected", 	person2.getName().equals(NEW_NAME));
	}
	@Test
	public void testDelete() {
		final Person person = getPersonService().getById(11);
		assertNotNull("The person was null",person);
		assertTrue(getPersonService().delete(person));
		assertFalse(getPersonService().delete(person));
	}
	public PersonService getPersonService() {
		return personService;
	}
	public void setPersonService(PersonService personService) {
		this.personService = personService;
	}
}

 

Jersey Unit Testing

 By extending JerseyTest we are able to create the web resource environment to execute web requests to validate our api.  Under the covers Jersey is using the services of Grizzly as a server.  There is, however, one difficulty with the use of Grizzly and that is due to its’ architecture Grizzly requires that we run the resource classes in a singleton scope whereas in our target environment tomcat we will be running the resource in request scope.  The request scope gets around syncronization issues in tomcat which is handled differently by the Grizzly server. So one needs to be cognisant of this to switch the scope as appropriate. I will have to take a deeper look into this issue for a better answer and update the blog later.

The task of configuration the Jersey tests has, again, been made trivial with Jersey’s testing framework.  To utilize the test framework we extend the JerseyTest class.  In the constructor of the test we provide the location of the application context, the web context path and the java package of the resources.  Then we write the methods to construction web requests to each of the webservice methods.  Below is the tests for this project:

package com.persistent.rest;

import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.util.HashMap;
import java.util.Map;

import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;

import org.apache.log4j.Logger;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.junit.Test;

import com.persistent.entity.Person;
import com.sun.jersey.core.util.MultivaluedMapImpl;
import com.sun.jersey.test.framework.JerseyTest;
import com.sun.jersey.test.framework.util.ApplicationDescriptor;

public class PersonResourceWsTest extends JerseyTest {
	private final Logger log = Logger.getLogger(this.getClass());

	public PersonResourceWsTest() throws Exception {
		super();
		final Map<String, String> contextParams = new HashMap<String, String>();
		contextParams.put("contextConfigLocation",
				"classpath:applicationContext.xml");
		final ApplicationDescriptor appDescriptor = new ApplicationDescriptor()
				.setContextPath("/crudtd")
				.setRootResourcePackageName("com.persistent.rest")
				.setServletClass(
						com.sun.jersey.spi.spring.container.servlet.SpringServlet.class)
				.setContextListenerClassName(
						"org.springframework.web.context.ContextLoaderListener")
				.setContextParams(contextParams);
		super.setupTestEnvironment(appDescriptor);
	}

	@Test
	public void testSimpleTest() {
		assertNotNull("The Webresource is null", webResource);
		final String responseMessage = webResource.path("persons/test").accept(
				MediaType.TEXT_PLAIN).get(String.class);
		assertTrue(responseMessage.equals("Hello Foo"));
		log.info("Response: " + responseMessage);
	}

	@Test
	public void testGetPersons() {
		assertNotNull("The Webresource is null", webResource);
		final JSONObject responseMessage = webResource.path("persons").accept(
				MediaType.APPLICATION_JSON).get(JSONObject.class);
		log.info("Its type is: " + responseMessage.getClass().getName());
		try {
			final JSONArray array = getPersonsInDatabase();
			assertTrue(array.length() == 10);
			for (int i = 0; i < array.length(); i++) {
				log.info(((JSONObject) array.get(i)).toString());
			}
		} catch (JSONException e) {
			fail("JSON Exception: " + e.getMessage());
			e.printStackTrace();
		}
	}

	private int getNumberOfPersonsInDatabase() throws JSONException {
		return getPersonsInDatabase().length();
	}

	private JSONArray getPersonsInDatabase() throws JSONException {
		assertNotNull("The Webresource is null", webResource);
		final JSONObject responseMessage = webResource.path("persons").accept(
				MediaType.APPLICATION_JSON).get(JSONObject.class);
		log.info("Its type is: " + responseMessage.getClass().getName());
		return responseMessage.getJSONArray("person");
	}

	@Test
	public void testGetPerson() {
		assertNotNull("The Webresource is null", webResource);
		final JSONObject responseMessage = webResource.path("persons/1")
				.accept(MediaType.APPLICATION_JSON).get(JSONObject.class);
		log.info("JSON Response: " + responseMessage.toString());
		try {
			assertTrue(responseMessage.get("name").equals("Frank Zappa"));
		} catch (JSONException e) {
			fail("JSON Exception: " + e.getMessage());
			e.printStackTrace();
		}
	}

	@Test
	public void testUpdateByJSONPerson() {
		log.info("Entering testUpdateByJSONPerson");
		final JSONObject person = webResource.path("persons/1").accept(
				MediaType.APPLICATION_JSON).get(JSONObject.class);
		try {
			final Person realPerson = new Person(person.getInt("id"), person
					.getString("name")
					+ "wow", person.getInt("age"));
			webResource.path("persons/update").type("application/json").put(
					realPerson);

			final JSONObject person2 = webResource.path("persons/1").accept(
					MediaType.APPLICATION_JSON).get(JSONObject.class);
			log.info("The Updated record retrieved: " + person2.toString()
					+ "\n\n\n\n");
		} catch (JSONException e) {
			fail("JSON Exception: " + e.getMessage());
			e.printStackTrace();
		}

	}

	@Test
	public void testAddByJSONPerson() {
		log.info("Entering testUpdateByJSONPerson");
		try {
			final int numberOfPersonsInDatabaseBeforeAdd = getNumberOfPersonsInDatabase();
			final Person realPerson = new Person("David Who", 49);
			webResource.path("persons").type("application/json").post(
					realPerson);
			assertTrue(getNumberOfPersonsInDatabase() == numberOfPersonsInDatabaseBeforeAdd + 1);
		} catch (JSONException e) {
			fail("JSON Exception: " + e.getMessage());
			e.printStackTrace();
		}

	}

	@Test
	public void testDeletePerson() {

		try {
			final int numberOfPersonsInDatabaseBeforeAdd = getNumberOfPersonsInDatabase();

			// Test delete one person by id
			final String path = "persons/1";
			webResource.path(path).delete();
			assertTrue("Single Delete Test failed",getNumberOfPersonsInDatabase() == numberOfPersonsInDatabaseBeforeAdd - 1);
			MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl();
			queryParams.add("id", "3");
			queryParams.add("id", "4");
			queryParams.add("id", "5");

			// Test delete multiple id's
			final String pathMultiDelete = "persons/2";
			webResource.path(pathMultiDelete).queryParams(queryParams).delete();
			assertTrue("Multi-delete test failed",getNumberOfPersonsInDatabase() == numberOfPersonsInDatabaseBeforeAdd - (1+4));

			// Test delete where id is already deleted
			final String alreadyDeleted = "persons/2";
			webResource.path(alreadyDeleted).delete();
			assertTrue("Multi-delete test failed",getNumberOfPersonsInDatabase() == numberOfPersonsInDatabaseBeforeAdd - (1+4));
		} catch (JSONException e) {
			fail("JSON Exception: " + e.getMessage());
			e.printStackTrace();
		}
	}

}

Trouble Shooting from the Commandline with cURL

Sometimes it is helpful to try to run commands against the web server using the cURL tool.

The following commands will execute the services of our webservice:

POSTCreate a new Person

curl  -XPOST -HContent-type:application/json --data '{"id":"0","name":"Frank","age":"3"}' http://localhost:8080/crudtd/webresources/persons

GET – with or without id Reads one or all Persons

curl  -XGET  http://localhost:8080/crudtd/webresources/persons/2
curl  -XGET  http://localhost:8080/crudtd/webresources/persons/

PUTUpdate Person

curl  -XPUT -HContent-type:application/json --data '{"id":"1","name":"Frank Zappa","age":"777"}'

DELETE – Delete Person or Persons

curl  -XDELETE  http://localhost:8080/crudtd/webresources/persons/1
curl  -XDELETE  http://localhost:8080/crudtd/webresources/persons/2?id=3&id=4 

AJAX Communication Javascript Library

 In my previous posting I was using a Prototype.js file that I had modified to accommodate my interest in utilizing HTTP PUT.  Prototype was channelling some of the requests through POST which was not acceptable for our purposes.  I have backed off of the modification and wrote a short AJAX communication class based largely on the Prototype.js implementation.

//=====================================================================
var MyAjax = {
  getTransport: function() {
    return Try.these(
      function() {return new XMLHttpRequest()},
      function() {return new ActiveXObject('Msxml2.XMLHTTP')},
      function() {return new ActiveXObject('Microsoft.XMLHTTP')}
    ) || false;
  },

  activeRequestCount: 0
};
  MyAjax.Base = Class.create({
  initialize: function(options) {
    this.options = {
      method:       'get',
      asynchronous: true,
      contentType:  'text/plain',
      encoding:     'UTF-8',
      parameters:   '',
      data:			'',
      evalJSON:     true,
      evalJS:       true
    };
    Object.extend(this.options, options || { });

    this.options.method = this.options.method.toLowerCase();

    if (Object.isString(this.options.parameters))
      this.options.parameters = this.options.parameters.toQueryParams();
    else if (Object.isHash(this.options.parameters))
      this.options.parameters = this.options.parameters.toObject();
  }
});

  MyAjax.Request = Class.create(MyAjax.Base, {
	_complete: false,
	_url:'',
	initialize: function($super,url, options) {
			$super(options);
			this._url = url;
			this.transport = MyAjax.getTransport();
		    this.send();
		},
	netResults: function() {
			 if (this.transport.readyState == 4) {
		         if ((this.transport.status == 200)||(this.transport.status == 204)) {
		        	 this.options.onSuccess.apply(this,new Array(this.transport));
		         } else {
		        	 this.options.onFailure.apply();
		         }
		         this.transport.onreadystatechange = Prototype.emptyFunction;
		      }

		},

	send: function() {
			     var transport = this.transport;
				 if( transport != null ) {
					 var params = Object.clone(this.options.parameters);
					 if (params = Object.toQueryString(params)) {
						 this._url += (this._url.include('?') ? '&' : '?') + params;
					 }
					 transport.onreadystatechange = this.netResults.bind(this);
					 transport.open(this.options.method, this._url, true);
					 transport.setRequestHeader("Content-type", this.options.contentType);
					 transport.setRequestHeader("Connection", "close");

					 transport.setRequestHeader("Content-length", 0);
					 if( (this.options.data != null)&&(this.options.data.length>0)) {
						transport.setRequestHeader("Content-length", this.options.data.length);
					 	transport.send(this.options.data);
					 } else {
						 transport.setRequestHeader("Content-length", 0);
						 transport.send(null);
					 }
				 }
				 else {
					 if( this.options.onFailure !=null) this.options.onFailure.apply();
					  alert("We do not have transport");
				 }
		}
	}
);

 Conclusion

That concludes this instalment.  This series of articles is not intended to dive deeply into any technology but rather to provide a foundation for understanding the integration of these packages.  If you have any question please do not hesitate to make a comment of send me an e-mail.

The Source Code

 Download the source code here: crudtd.zip

About The Author

David Sells is a computer consultant in Toronto, Ontario who specializing in Java Enterprise Development. He has  provided innovative solutions for clients including: IBM, Goldman Sachs, Merrill Lynch and Deutsche Bank.

He also holds the following certifications:

  • Sun Certified Enterprise Architect for the Java Platform, Enterprise Edition 5 (2009)
  • Sun Certified Web Component Developer
  • Sun Certified Java Programmer.

Contact: david@persistentdesigns.com

 

 

 

Leave a Reply

 

 

 

You can use these HTML tags

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>