Testing the Java REST-MVC Server Tier

Article Summary
This article is part of a series. We use the "book club" project to explore various programming languages and frameworks. Details of the book club's business and data requirements are detailed in a prior article, "Leveraging Ruby on Rails and ClojureScript.".

We've created a new version of the book club's server tier using a stack of Java components. See the article REST-MVC using Java for details of implementation. This article details automated testing for the Java/REST-MVC server tier.

Automated Unit Tests
In this article we will not enforce a strict differentiation between "Unit" and "Integration" test.

This article refers to "automated testing". That means, the programmer writes a separate program(s) to "test" functionality. We use the Junit testing framework along with some plugin/extensions (Mockito, JsonPath and Hamcrest).

Organizing Tests Into A Plan
In this section of the article we discuss "what" we test.

We need to consider the business and functional requirements of our application and then plan our test suite. So, let's recap our application and organize our test plan. We have entities (and a relationship). Entities are Authors, Books, Categories and Reviews. Book-Categories are relationship between a book and a category (E.G. Tom Sawyer is Fiction).

We also have application services.

  • A repository is responsible for reading and writing data, to and from the database (and the application data objects).
  • A controller accepts requests, dispatches the request to a handler, then routes the response. Our server has 2 types of controllers. We have controllers which route the response to server generated view (E.G. jsp/html/css). We also have controller which generate a JSON response (the client renders the view).

Finally, our application has some specific implementation/optimization requirements. We require the database (as opposed to the application code) enforce unique Authors, Books, Categories and Book-Categories. We require the database to delete related records. For example, books require an author. If we delete an author, the database should automatically delete all of the books written by that author.

So here are the "operation/method" tests:

  • Select records
  • Insert a record
  • Modify a record
  • Delete a record
  • Attempt to insert a duplicate record.
  • Attempt to modify an existing into a duplicate.
  • Delete an Author and verify all related books, reviews and book-categories are also deleted.
  • Delete a Book and verify all related reviews and book-categories are also deleted.

For each of the above we perform for the following services:

  • Repository
  • Controller
  • Rest Controller

For each service, we test the following entities :

  • Author
  • Book
  • Category
  • Review

For each service, we test the following relationship :

  • Book-Category

Setup -- Creating Test Data
Tests may be run concurrently. In addition, some test framework version do not allow you to specify the test sequences. Thus, if you have a test to delete a record, you test class instance should create a unique record (to that class instance) which is dedicated to the "delete record" test.

Thus, when we create test case data, we use dynamic data that prevents collisions between test class instances.

For example, need to create an Author for the "delete Author test". We can use the current time in milliseconds plus a few characters from the test class name and/or test goal (E.G. "darc" delete author rest controller). Fields like Author first name have a length limit (25 characters). Thus, we select only a few characters. We don't want to create a value which exceeds a field length.

When we look back the above section "Organizing Tests Into A Plan", we have quite a bit to test. However, we have a lot of reusable patterns we can apply when we start writing test details. For example, when we write tests that involve "Authors". We always need to create a test record to:

  • Add and Author
  • Delete an Author
  • Modify an Author
  • Select an Author
  • Select all Authors
  • Attempt to add a duplicate Author
  • Attempt to modify an Author into a duplicate

Thus, our set up phase, where we create the test case data, is pretty much the same regardless if we are testing the Author:

  • Repository
  • View Controller
  • REST/JSON Controller

To generate the test case data (records), I decided to write a small program using a simple JDBC connection. I called the program CaseGen, located under the test source at org.pa.dbUtil.CaseGen.

Note! The server application uses a combination of the Hibernate ORM and Java Persistence API (JPA). Typically, you want the ORM (Hibernate) to perform all the database operations. You don't want a direct JDBC connection and Hibernate running simultaneously. Thus. all of the tests run the Case generator prior to the "mock" server instance instantiation. In other words, CaseGen runs first. It generates test data. When CaseGen completes, then the "mock" server environment is started. The mock server initiates Hibernate and JPA. Thus, we segregate the two processes (CaseGen/JDBC and Hibernate/JPA).

Implementing Test Details

Since the tests are Java programs, the tests are housed in Java classes. Each Java class contains one to many member tests. Each member test includes one to many assertions (verification). An example of an assertions is "assert list contains at least one book".

For each Java Test Class, you also have the option to implement a setup before the member tests are run. Likewise, you can implement a "tear down" after all of the member tests are run.

You can also implement a "setup" and "tear down", which are run, before and after each member test.

For this project, I implemented a setup and tear down for each class. I did not implement setup and tear downs for each individual member test.

Let's look at some actual code. We'll look at the test class responsible for testing the Author REST controller (org.pa.test.rest.TestAuthorRestController).

First we generate our test cases in the setUpClass method:

public static void setUpClass() {
   MARK_TWAIN_ID = CaseGen.getInstance().getTestAuthor(CaseGen.MARK, CaseGen.TWAIN);
   JOE_SMOE_ID = CaseGen.getInstance().getTestAuthor(CaseGen.JOE, CaseGen.SMOE);
   if (className == null) {
            className = "AuthorRestController";
   TEST_AUTHOR_DELETE_ID = CaseGen.getInstance().createTestAuthor(new Date().getTime() + "", className);
   TEST_AUTHOR_MODIFY_ID = CaseGen.getInstance().createTestAuthor(new Date().getTime() + "", className);

Note! As mentioned in the above section "Setup -- Creating Test Data", calls to JDBC program CaseGen are performed in the "setUpClass" methord. "setUpClass" runs before any member classes and/or Hibernate/JPA.

Before each member test we create the mock web context:

public void setUp() {
  this.mockMvc = webAppContextSetup(this.wac).build();

Next, let's write a test to modify an Author:

public void modifyAuthorTest() {
 boolean noErrorsFound = true;
 try {
    String newLastName = "NS" + new Date().getTime();
    String requestUrl = String.format(MODIFY_AUTHOR_URL, TEST_AUTHOR_MODIFY_ID, "Ed", newLastName);
 } catch (Exception ex) {
      Logger.getLogger(TestAuthorRestController.class.getName()).log(Level.SEVERE, null, ex);
      noErrorsFound = false;
 assertTrue("verify no exceptions raised", noErrorsFound);

Now let's test that the application rejects an attempt to modify an Author into a duplicate. In other words, an attempt to create a second "Mark Twain".

public void duplicateAddAuthorTest() {
  boolean noErrorsFound = true;
  try {
     String requestUrl = String.format(ADD_AUTHOR_URL, CaseGen.MARK, CaseGen.TWAIN);
     ResultActions andExpect = this.mockMvc.perform(post(requestUrl).accept(MediaType.APPLICATION_JSON))
  } catch (Exception ex) {
    Logger.getLogger(TestAuthorRestController.class.getName()).log(Level.SEVERE, null, ex);
     noErrorsFound = false;
  assertTrue("verify no exceptions raised", noErrorsFound);

When all member tests are done, the test runner calls the tearDownClass() method. We delete all of our temporary test case data.

public static void tearDownClass() {

Now let's look at the same 2 tests for our Author View Controller.

First we test modification of an Author:

public void testModAuthor() {
  boolean no_errors_found = true;
  try {       
     String firstName =  className;
     String lastName = new Date().getTime() + "m";
     ResultActions andExpect = mockMvc.perform(post("/author/edit").param("lastName", lastName)
       . param("id", TEST_AUTHOR_MODIFY_ID + "").param("firstName", firstName)
     Author authorResult = (Author) andExpect.andReturn().getModelAndView().getModelMap().get("author");
     assertTrue("verify firstName updated", authorResult.getFirstName().equals(firstName));
     assertTrue("verify lastName updated", authorResult.getLastName().equals(lastName));
     assertTrue("verify key/id is unchanged", authorResult.getId().intValue() == TEST_AUTHOR_MODIFY_ID);
  } catch (Exception ex) {
     Logger.getLogger(TestAuthorController.class.getName()).log(Level.SEVERE, null, ex);
     no_errors_found = false;
  assertTrue("verify no exceptions thrown", no_errors_found);

Now test, the application is preventing duplicate Authors:

public void testAddDuplicateAuthor() {
  boolean no_errors_found = true;
  try {
    ResultActions requestResult = mockMvc.perform(post("/author/add").param("firstName", CaseGen.MARK)
      .param("lastName", CaseGen.TWAIN)
      // get the error message is nested in the result. 
   BindingResult bindingResult = (BindingResult) requestResult.andReturn().getModelAndView()
         hasItem(new ObjectError("firstName", MessageDetailDefinitions.DUPLICATE_AUTHOR_EXCEPTION)));
  } catch (Exception ex) {
    Logger.getLogger(TestAuthorController.class.getName()).log(Level.SEVERE, null, ex);
    no_errors_found = false;
 assertTrue("verify no exceptions thrown", no_errors_found);

Categorizing Tests

The latest version of Junit allows you to categorize tests. You can add a category to either a java test class, and/or an individual member test.

So why categorize tests? There are several use cases.

First, automated tests are typically run as part of the build process. That's the default for Maven. What that means is, if a test fails, the application does not get built. You may have tests for minor enhancements, You may not want to include those tests in the build process.

Another example may occur during programming. Your programmer might be focuses on one area of the program. Let's say the programmers are focusing the "Author Repository". If the programmers can categorize the tests associated with "Author Repository", they can filter the test run appropriately.

Implementing Test Categories.
All you need to do is create a Java interface. No methods, just an empty interface.
package org.pa.test.category;

public interface RepositoryCategory {}

You annotate a Java class or method when you want to categorize. E.G.

public class TestAuthorRepository extends TestCase {

Running Tests by Category
I am using the Apache Maven build tool. You configure Maven with xml and/or command line arguments. Maven's naming convention calls for the "project" xml file to be pom.xml. I've add a set up xml tags (). Empty text between "textcase.groups" means run all tests regardless of category. You can also add one to many test category names (a list is separated by commas). A category name is the full path (separated by dots). For example:


Runs all test categorized as "Critical".

Another example is:

<testcase.groups>org.pa.test.category.Critical, org.pa.test.category.RepositoryCategory</testcase.groups>

Runs all testa categorized as "RepositoryCategory" or "Critical".

Using Maven Profiles
Maven also supports "profiles". A profile is a group of settings. The settings can be applied to what build phase you want. In this project. I created a few profiles which include settings for the test phase.

You can invoke Maven to use a specif profile from the command line. For example.
$dev> mvn -Ptest-recreate-db test

The profile is "test-create-db" the lifecycle phase is "test".

I set the database instance to "test", during the process-resources life cycle phase (run before tests), I invoke the CaseGen utility with some custom arguments.

NOTE! IMPORTANT. Always run the maven command line from the root directory directory of your project. The root directory is typically where your project's "pom.xml" file is located.

Running a single test.
You can also direct Maven to run a single test (or list of tests) from the command line. For example:
$dev> mvn -Dtest=TestAuthorRepository test

Some Background on Maven and Ant

Both tools have been around for quite a while. These tools are typically configured by experienced members of a development team. Thus, if you are a new developer, don't feel intimidated by the verbose configuration. A lot of Maven and Ant documentation assumes the typical user to be an "experienced user". There are beginner tutorials, you just need to search for them. Finally, if you are really in a bind, remember, neither Maven or Ant are required to build your application (or run your tests). They are tools that most shops use. However, you can compile and run Java applications without a build tool. Also, IDE's such as Netbeans or Eclipse can generate Maven and/or Ant configurations.

Developers coming from a ROR environment occasionally assume that Maven plays the same role as ROR's generate utility. Not true. It shares some of the same type of service as ROR's generate utility. However, Maven is an option, not a requirement.

Using the mock environments for Spring takes a little research. However, once you grasp the basics, writing the tests become fairly easy.

In addition. running the test suite is really fast. Specially if you are comparing the time it takes to re-deploy a java web application. On my old laptop. I can run the full 90 plus test suite in under a minute.

To summarize, moving automated testing to the front your development process makes a lot of sense. Specially when you are adding enhancements and performing maintenance.

Article references.
As mentioned at the top, this article is part of a series focused on the "book club" project.

For this release of the book club project:
Changes to the Clojurescript client discussed in ClojureScript - Single Page Application - A simple approach
Changes to the Server tier are discuses in REST-MVC using Java
Testing the Clojurescript Client is discussed in Testing Clojurescript - A simple approach
GitHub Repository for Server tier: book-site-jpa
GitHub Repository for Clojurescript Client: book-site-clojurescript

About the Author:
Lorin M Klugman - I'm an experienced developer. My main interest is in new technology. Please use our contact box here if you are interested in hiring me. Please no recruiters :)