Developing a Web Application
with Spring Framework and OpenRules

Part 4 – Adding Unit Tests and a Form to the Application

This is the fourth part of a step-by-step account of how to develop web applications using Spring framework and OpenRules. The first part described how to setup a Tomcat-based development environment and create a trivial web application. The second part added major Spring modules and a view that displays current date and time. The third part added business logic and GUI using Java, Spring, and JSP with JSTL. In this part we will add  are now going to add some unit tests to the application and create a dialog with two views that allows a user to change customer information and regenerate greetings.


Step 22 – Add unit test for the HelloController

If you work under Eclipse you can run any Java test class as JUnit test directly from Eclipse menu. Otherwise, before we create any unit tests, we have to prepare Ant and our build script to be able to handle this. Ant has a built in JUnit target, but we need to add junit.jar to Ant's lib directory. You can use the one that came with the Spring distribution spring-framework-1.2/lib/junit/junit.jar. Just copy this file to the lib directory in your Ant installation. Also add the following target to our build script.

    <target name="junit" depends="build" description="Run JUnit Tests">
        <junit printsummary="on"
               fork="false"
               haltonfailure="false"
               failureproperty="tests.failed"
               showoutput="true">
            <classpath refid="master-classpath"/>
            <formatter type="brief" usefile="false"/>

            <batchtest>
                <fileset dir="${build.dir}">
                    <include name="**/Test*.*"/>
                </fileset>
            </batchtest>

        </junit>

        <fail if="tests.failed">
tests.failed=${tests.failed}
 *********************************************************** *********************************************************** **** One or more tests failed! Check the output ... **** *********************************************************** *********************************************************** </fail> </target>

Now add a new sub-directory 'tests' in the src directory. This directory will, as you might have guessed, contain all the unit tests.

After all this, we are ready to start writing the first unit test. The HelloController depends on both the HttpServletRequest, HttpServletResponse and our application context. Since the controller does not use the request or the response, we can simply pass in null for these objects. If that was not the case, we could create some mock objects using EasyMock that we would pass in during our test. The application context can be loaded outside of a web server environment using a class that will load an application context. There are several available, and for the current task the FileSystemXmlApplicationContext works fine.

hello.spring/src/tests/TestHelloController.java

package tests;

import hello.Customer;
import hello.GreetingEngine;

import java.io.IOException;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import junit.framework.TestCase;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
import org.springframework.web.servlet.ModelAndView;

import web.HelloController;

public class TestHelloController extends TestCase { 
	private ApplicationContext ac; 

	public void setUp() throws IOException { 
		ac = new FileSystemXmlApplicationContext("src/tests/WEB-INF/hello.spring-servlet.xml"); 
	} 

	public void testHandleRequest() throws ServletException, IOException { 
		HelloController sc = (HelloController) ac.getBean("helloController"); 
		ModelAndView mav = sc.handleRequest((HttpServletRequest) null, (HttpServletResponse) null); 
		Map m = mav.getModel(); 
		Customer customer = (Customer) ((Map) m.get("model")).get("customer"); 
		assertEquals("Robinson", customer.getName()); 
	}
}

The only test is a call to handleRequest, and we check the customer that are returned in the model. In the setUp method, we load the application context that we have to copy into a WEB-INF directory in the src/tests directory. Create a copy just so this file will work during tests with a small set of beans necessary for running the tests. So, copy hello.spring/war/WEB-INF/hello.spring-servlet.xml to hello.spring/src/tests/WEB-INF directory. You can then remove the “messageSource”, "urlMapping" and "viewResolver" bean entries since they are not needed for this test.
 

hello.spring/src/tests/WEB-INF/hello.spring-servlet.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<!--
- Application context definition for "hello.spring" DispatcherServlet.
-->


<beans>
<bean id="helloController" class="web.HelloController"> <property name="customer"> <ref bean="customer"/> </property> <property name="greetingEngine"> <ref bean="greetingEngine"/> </property> </bean> <bean id="customer" class="hello.Customer"> <property name="name"><value>Robinson</value></property> <property name="gender"><value>Female</value></property> <property name="maritalStatus"><value>Married</value></property> <property name="age"><value>23</value></property> </bean> <bean id="greetingEngine" class="hello.GreetingEngine"> <property name="result"><value>?</value></property> </bean> </beans>

When you run this test, you should see a lot of log messages from the loading of the application context:

...
2005-10-02 09:14:00,927 INFO [org.springframework.beans.factory.support.DefaultListableBeanFactory] - <Creating shared instance of singleton bean 'helloController'>
2005-10-02 09:14:00,958 INFO [org.springframework.beans.factory.support.DefaultListableBeanFactory] - <Creating shared instance of singleton bean 'customer'>
2005-10-02 09:14:00,958 INFO [org.springframework.beans.factory.support.DefaultListableBeanFactory] - <Creating shared instance of singleton bean 'greetingEngine'>
2005-10-02 09:14:00,958 INFO [web.HelloController] - <SpringappController - returning hello view>
2005-10-02 09:14:00,974 INFO [web.HelloController] - <returning hello view with Sun Oct 02 09:14:00 EDT 2005>
2005-10-02 09:14:00,974 INFO [web.HelloController] - <running greetingEngine for customer Robinson>
Generate Greeting for Customer Robinson: gender=Female age=23 Status=Married
Generated Greeting: Good Morning, Mrs. Robinson!


Step 23 – Adding a user interaction using two views

To demonstrate an interaction capabilities of the web application, we will add a form that will allow the user to change customer information and regenerate a new greeting. To create a new form "modifyCustomer.jsp" we will use a tag library named “spring” that is provided with the Spring Framework. We have to copy this file from the Spring distribution spring-framework-1.2/dist/spring.tld to the hello.spring/war/WEB-INF directory. Now we must also add a <taglib> entry to web.xml.

hello.spring/war/WEB-INF/web.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC '-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN' 'http://java.sun.com/dtd/web-app_2_3.dtd'>

<web-app>

<servlet> <servlet-name>hello.spring</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>hello.spring</servlet-name> <url-pattern>*.htm</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file> index.jsp </welcome-file> </welcome-file-list> <taglib> <taglib-uri>/spring</taglib-uri> <taglib-location>/WEB-INF/spring.tld</taglib-location> </taglib> </web-app>

We also have to declare this taglib in a page directive in the jsp file. We declare a form the normal way with a <form> tag and an <input> text field and a submit button.

hello.spring/war/WEB-INF/jsp/modifyCustomer.jsp

<%@ include file="/WEB-INF/jsp/include.jsp" %>
<%@ taglib prefix="spring" uri="/spring" %>

<html>
<head><title><fmt:message key="title"/></title></head>

<body>
  <h1><fmt:message key="modifyCustomer.heading"/></h1>
  <form method="post"> 
    <table width="95%" bgcolor="f8f8ff" border="0" cellspacing="0" cellpadding="5"> 
	<tr> 
	<td alignment="right" width="30%">Name:</td> 
	<spring:bind path="customer.name"> 
	<td width="30%"> 
	<input type="text" name="name" value="<c:out value="${customer.name}"/>"> 
	</td> 
	</spring:bind> 
	</tr> 
	<tr> 
	<td alignment="right" width="30%">Age:</td> 
	<spring:bind path="customer.age"> 
	<td width="30%"> 
	<input type="text" name="age" value="<c:out value="${customer.age}"/>"> 
	</td> 
	<td width="40%"> 
	<font color="red"><c:out value="${status.errorMessage}"/></font> 
	</td> 
	</spring:bind> 
	</tr> 
	<tr> 
	<td alignment="right" width="30%">Gender:</td> 
	<spring:bind path="customer.gender"> 
	<td width="30%"> 

	<input type="radio" name="gender" value="Male" 
	<c:if test='${customer.gender == "Male"}'>checked</c:if> 
	/> Male 
	<input type="radio" name="gender" value="Female" 
	<c:if test='${customer.gender == "Female"}'>checked</c:if>
	/> Female 
	</td> 
	</spring:bind> 
	</tr>
	<tr> 
	<td alignment="right" width="30%">Marital Status:</td> 
	<spring:bind path="customer.maritalStatus"> 
	<td width="30%"> 
	<input type="radio" name="maritalStatus" value="Married" 
	<c:if test='${customer.maritalStatus == "Married"}'>checked</c:if>
	/> Married
	<input type="radio" name="maritalStatus" value="Single" 
	<c:if test='${customer.maritalStatus == "Single"}'>checked</c:if>
	/> Single 
	</td> 
	</spring:bind> 
	</tr>
    </table> 
    <br> 
    <spring:hasBindErrors name="modifyCustomer"> 
       <b>Please fix all errors!</b> 
    </spring:hasBindErrors> 
    <br><br> 
    <input type="submit" alignment="center" value="Submit Changes">
  </form>
  <a href="<c:url value="hello.htm"/>">Back</a>
</body>
</html>

The <spring:bind> tag is used to bind an <input> form element to a command object Customer.java, that is used together with the form. This object is later passed in to the validator and if it passes validation it is passed on to the controller. The ${status.errorMessage} and ${status.value} are special variables declared by the framework that can be used to display error messages and the current value of the field. The validator class gets control after the user presses submit. The values entered in the form will be set on the command object by the framework. The method validate is called and the command object and an object to hold any errors are passed in.
 

hello.spring/src/hello/CustomerValidator.java

package hello;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

public class CustomerValidator implements Validator {
  private int DEFAULT_MIN_AGE = 1;
  private int DEFAULT_MAX_AGE = 150;
  private int minAge = DEFAULT_MIN_AGE;
  private int maxAge = DEFAULT_MAX_AGE;
  /** Logger for this class and subclasses */
  protected final Log logger = LogFactory.getLog(getClass());
  public boolean supports(Class clazz) {
      return clazz.equals(Customer.class);
  }
  public void validate(Object obj, Errors errors) {
     Customer c = (Customer) obj;
     if (c == null) {
        errors.rejectValue("age", "error.not-specified", null, "Value required.");
     }
     else {
        logger.info("Validating customer age: " + c.getAge());
        if (c.getAge() > maxAge) {
             errors.rejectValue("age", "error.too-high",
             new Object[] {new Integer(maxAge)}, "Value too high.");
       }
       if (c.getAge() <= minAge) {
             errors.rejectValue("age", "error.too-low",
             new Object[] {new Integer(minAge)}, "Value too low.");
       }
    }
  }

  public void setMinAge(int i) {
       minAge = i;
  }

  public int getMinAge() {
      return minAge;
  }
 
  public void setMaxAge(int i) {
      maxAge = i;
  }

  public int getMaxAge() {
      return maxAge;
  }

}
 

Now we need to add an entry in the hello.spring-servlet.xml file to define the new form and controller. We define properties for the validator. We also specify two views, one that is used for the form and one that we will go to after successful form processing. The latter which is called the success view is our new JSP page 'modifyCustomer.jsp'.
 

hello.spring/war/WEB-INF/hello.spring-servlet.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<!--
- Application context definition for "springapp" DispatcherServlet.
-->


<beans>
<bean id="helloController" class="web.HelloController"> <property name="customer"> <ref bean="customer"/> </property> <property name="greetingEngine"> <ref bean="greetingEngine"/> </property> </bean> <bean id="customer" class="hello.Customer"> <property name="name"><value>Robinson</value></property> <property name="gender"><value>Male</value></property> <property name="maritalStatus"><value>Single</value></property> <property name="age"><value>3</value></property> </bean>
    <!-- Validator and Form Controller for the "Modify Customer" page -->
    <bean id="customerValidator" class="hello.CustomerValidator"/>
    <bean id="modifyCustomerForm" class="web.ModifyCustomerController">
	<property name="sessionForm"><value>true</value></property>
	<property name="commandName"><value>customer</value></property>
	<property name="commandClass"><value>hello.Customer</value></property>
	<property name="validator"><ref bean="customerValidator"/></property>
	<property name="formView"><value>modifyCustomer</value></property>
	<property name="successView"><value>hello.htm</value></property>
	<property name="customer">
	     <ref bean="customer"/>
	</property>
    </bean>

    <bean id="greetingEngine" class="hello.GreetingEngine">
	<property name="result"><value>?</value></property>
    </bean>

    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename"><value>messages</value></property>
    </bean>

    <bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/hello.htm">springappController</prop>
		<prop key="/modifyCustomer.htm">modifyCustomerForm</prop> 
            </props>
        </property>
    </bean>

    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass"><value>org.springframework.web.servlet.view.JstlView</value></property>
        <property name="prefix"><value>/WEB-INF/jsp/</value></property>
        <property name="suffix"><value>.jsp</value></property>
    </bean>
</beans>        

Next, let's take a look at the controller for this form. The onSubmit method gets control and does some logging before it calls a 'update' method to save newly modified customer's characteristics. It then returns a ModelAndView passing in a new instance of a RedirectView created using the url for the successView.

hello.spring/src/web/ModifyCustomerController.java

package web;

import org.springframework.web.servlet.mvc.SimpleFormController;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Map;
import java.util.HashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import hello.Customer;

public class ModifyCustomerController extends SimpleFormController { 
  /** Logger for this class and subclasses */
  protected final Log logger = LogFactory.getLog(getClass());

  private Customer customer;

  public ModelAndView onSubmit(Object command) 
	throws ServletException { 
    Customer c = (Customer) command; 
    logger.info("Modify customer on submit: " + c); 
    update(customer,c);
    logger.info("returning from ModifyCustomer view to " + getSuccessView()); 
    Map myModel = new HashMap(); 
    String now = (new java.util.Date()).toString(); 
    myModel.put("now", now); 
    myModel.put("customer", getCustomer()); 
    return new ModelAndView(new RedirectView(getSuccessView())); 
  } 

  protected Object formBackingObject(HttpServletRequest request) throws ServletException { 
    Customer c = getCustomer(); 
    return c; 
  } 

  public Customer getCustomer() {
     return customer;
  }

  public void setCustomer(Customer customer) {
     this.customer = customer;
  }
  public void update(Customer c1, Customer c2) {
	c1.setName(c2.getName());
	c1.setAge(c2.getAge());
	c1.setGender(c2.getGender());
	c1.setMaritalStatus(c2.getMaritalStatus());
  }
}

We are also adding some messages to the messages.properties resource file.

hello.spring/war/WEB-INF/src/messages.properties

title=SpringApp
heading=Hello :: SpringApp
greeting=Greetings, it is now
modifyCustomer.heading=Customer Information error.not-specified=Value is required error.too-low=Value too low error.too-high=Value too high

Finally, we have to provide a link to the modifyCustomer page from the hello.jsp.
 

springapp/war/WEB-INF/jsp/hello.jsp
<%@ include file="/WEB-INF/jsp/include.jsp" %>
<html>
<head><title><fmt:message key="title"/></title></head>

<body>
  <h1> <fmt:message key="heading"/> </h1>


  <fmt:message key="greeting"/> <c:out value="${model.now}"/>

  <p>
  <b>Customer Information:</b> <br>
  <table border="1">
	<tr>
	  <td>Name</td>
	  <td><c:out value="${model.customer.name}" /></td>
	</tr>
	<tr>
	  <td>Age</td>
	  <td><c:out value="${model.customer.age}" /> </td>
	</tr>
	<tr>
	  <td>Marital Status</td>
	  <td><c:out value="${model.customer.maritalStatus}" /> </td>
	</tr>
	<tr>
	  <td>Gender</td>
	  <td><c:out value="${model.customer.gender}" /> </td>
	</tr>
  </table>

  <p>
   <b>Generated Greeting: </b><br>
   <c:out value="${model.greetingEngine.result}"/>
  </p>
  <br>
  <a href="<c:url value="modifyCustomer.htm"/>">Modify Customer</a>
  <br>

</body>

</html>

Compile and deploy all this and after reloading the application we can test it. This is what the form looks like with errors displayed.

After "Submit Changes", the main view will look like:




Up  Part 3 – Adding Business Logic in Java    Part 5 – Moving Business Logic to OpenRules