Topquan's Blog

分享价值----成就你我----我的博客----你的家

Developing a Spring Framework MVC application (三)

This is Part 3 of a step-by-step account of how to develop a web application from scratch using the Spring Framework. In Part 1 (Steps 1 – 19) we configured the environment and set up a basic application that we will build upon. Part 2 (Steps 13-19) improved the application in several ways. We are now going to add some unit tests to the application.


Step 20 – Add unit test for the SpringappController

Before we create any unit tests, we want 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. I used 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. I also added 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 I add a new sub-directory in the src directory that I name tests. 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 SpringappController 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.

springapp/src/tests/TestSpringappController.java

package tests;

import java.util.Map;
import java.util.List;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletException;
import junit.framework.TestCase;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
import org.springframework.web.servlet.ModelAndView;
import web.SpringappController;
import bus.ProductManager;
import bus.Product;

public class TestSpringappController extends TestCase {

private ApplicationContext ac;

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

public void testHandleRequest() throws ServletException, IOException {
SpringappController sc = (SpringappController) ac.getBean("springappController");
ModelAndView mav = sc.handleRequest((HttpServletRequest) null, (HttpServletResponse) null);
Map m = mav.getModel();
List pl = (List) ((Map) m.get("model")).get("products");
Product p1 = (Product) pl.get(0);
assertEquals("Lamp", p1.getDescription());
Product p2 = (Product) pl.get(1);
assertEquals("Table", p2.getDescription());
Product p3 = (Product) pl.get(2);
assertEquals("Chair", p3.getDescription());
}

}

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

springapp/src/tests/WEB-INF/springapp-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="springappController" class="web.SpringappController"><property name="productManager"> <ref bean="prodMan"/> </property> </bean> <bean id="prodMan" class="bus.ProductManager"> <property name="products"> <list> <ref bean="product1"/> <ref bean="product2"/> <ref bean="product3"/> </list> </property> </bean> <bean id="product1" class="bus.Product"> <property name="description"><value>Lamp</value></property> <property name="price"><value>5.75</value></property> </bean> <bean id="product2" class="bus.Product"> <property name="description"><value>Table</value></property> <property name="price"><value>75.25</value></property> </bean> <bean id="product3" class="bus.Product"> <property name="description"><value>Chair</value></property> <property name="price"><value>22.79</value></property> </bean> </beans>

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


Step 21 – Add unit test and new functionality for ProductManager

Next I add a test case for the ProductManager, and I also add a test for a new method to increase the prices that I am planning on adding to the ProductManager.

springapp/src/tests/TestProductManager .java

package tests;

import java.util.List;
import java.util.ArrayList;
import junit.framework.TestCase;
import bus.ProductManager;
import bus.Product;

public class TestProductManager extends TestCase {

private ProductManager pm;

public void setUp() {
pm = new ProductManager();
Product p = new Product();
p.setDescription("Chair");
p.setPrice(new Double("20.50"));
ArrayList al = new ArrayList();
al.add(p);
p = new Product();
p.setDescription("Table");
p.setPrice(new Double("150.10"));
al.add(p);
pm.setProducts(al);
}

public void testGetProducs() {
List l = pm.getProducts();
Product p1 = (Product) l.get(0);
assertEquals("Chair", p1.getDescription());
Product p2 = (Product) l.get(1);
assertEquals("Table", p2.getDescription());
}

public void testIncreasePrice() {
pm.increasePrice(10);
List l = pm.getProducts();
Product p = (Product) l.get(0);
assertEquals(new Double("22.55"), p.getPrice());
p = (Product) l.get(1);
assertEquals(new Double("165.11"), p.getPrice());
}

}

For this test, there is no need to create an application context. I just create a couple of products in the setUp method and add them to the product manager. I add tests for both getProducts and increasePrice. The increasePrice method is a cross the board increase based on the percentage passed in to the method. I modify the ProductManager class to implement this new method.

springapp/src/bus/ProductManager.java

package bus;

import java.io.Serializable;
import java.util.ListIterator; import java.util.List; public class ProductManager implements Serializable { private List products; public void setProducts(List p) { products = p; } public List getProducts() { return products; } public void increasePrice(int pct) { ListIterator li = products.listIterator(); while (li.hasNext()) { Product p = (Product) li.next(); double newPrice = p.getPrice().doubleValue() * (100 + pct)/100; p.setPrice(new Double(newPrice)); } } }

Next I build and run the tests. As you can see, this test is just like any regular test – the business classes don't depend on any of the servlet classes so these classes are very easy to test.


Step 22 – Adding a form

To provide an interface in the web application, I add a form that will allow the user to enter a percentage value. This form uses 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 springapp/war/WEB-INF directory. Now we must also add a <taglib> entry to web.xml.

springapp/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>springapp</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springapp</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.

springapp/war/WEB-INF/jsp/priceincrease.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="priceincrease.heading"/></h1>
<form method="post">
<table width="95%" bgcolor="f8f8ff" border="0" cellspacing="0" cellpadding="5">
<tr>
<td alignment="right" width="20%">Increase (%):</td>
<spring:bind path="priceIncrease.percentage">
<td width="20%">
<input type="text" name="percentage" value="<c:out value="${status.value}"/>">
</td>
<td width="60%">
<font color="red"><c:out value="${status.errorMessage}"/></font>
</td>
</spring:bind>
</tr>
</table>
<br>
<spring:hasBindErrors name="priceIncrease">
<b>Please fix all errors!</b>
</spring:hasBindErrors>
<br><br>
<input type="submit" alignment="center" value="Execute">
</form>
<a href="<c:url value="hello.htm"/>">Home</a>
</body>
</html>

The <spring:bind> tag is used to bind an <input> form element to a command object PriceIncrease.java, that is used together with the form. This command 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.

springapp/src/bus/PriceIncrease.java

package bus;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class PriceIncrease {

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

private int percentage;

public void setPercentage(int i) {
percentage = i;
logger.info("Percentage set to " + i);
}

public int getPercentage() {
return percentage;
}

}

This is a very simple JavaBean class, and in our case there is a single property with a getter and setter. 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.

springapp/src/bus/PriceIncreaseValidator.java

package bus;

import java.io.Serializable;
import org.springframework.validation.Validator;
import org.springframework.validation.Errors;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class PriceIncreaseValidator implements Validator {
private int DEFAULT_MIN_PERCENTAGE = 0;
private int DEFAULT_MAX_PERCENTAGE = 50;
private int minPercentage = DEFAULT_MIN_PERCENTAGE;
private int maxPercentage = DEFAULT_MAX_PERCENTAGE;

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

public boolean supports(Class clazz) {
return clazz.equals(PriceIncrease.class);
}

public void validate(Object obj, Errors errors) {
PriceIncrease pi = (PriceIncrease) obj;
if (pi == null) {
errors.rejectValue("percentage", "error.not-specified", null, "Value required.");
}
else {
logger.info("Validating with " + pi + ": " + pi.getPercentage());
if (pi.getPercentage() > maxPercentage) {
errors.rejectValue("percentage", "error.too-high",
new Object[] {new Integer(maxPercentage)}, "Value too high.");
}
if (pi.getPercentage() <= minPercentage) {
errors.rejectValue("percentage", "error.too-low",
new Object[] {new Integer(minPercentage)}, "Value too low.");
}
}
}

public void setMinPercentage(int i) {
minPercentage = i;
}

public int getMinPercentage() {
return minPercentage;
}

public void setMaxPercentage(int i) {
maxPercentage = i;
}

public int getMaxPercentage() {
return maxPercentage;
}

}

Now we need to add an entry in the springapp-servlet.xml file to define the new form and controller. We define properties for command object and 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 can be of two types. It can be a regular view reference that is forwarded to one of our JSP pages. One disadvantage with this approach is, that if the user refreshes the page, the form data is submitted again, and you would end up with a double priceincrease. An alternative way is to use a redirect, where a response is sent back to the users browser instructing it to redirect to a new url. The url we use in this case can't be one of our JSP pages, since they are hidden from direct access. It has to be a url that is externally reachable. I have choosen to use 'hello.htm' as my redirect url. This url maps to the 'hello.jsp' page, so this should work nicely.

springapp/war/WEB-INF/springapp-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>

<!-- Controller for the initial "Hello" page --> <bean id="springappController" class="web.SpringappController"> <property name="productManager"> <ref bean="prodMan"/> </property> </bean> <!-- Validator and Form Controller for the "Price Increase" page --> <bean id="priceIncreaseValidator" class="bus.PriceIncreaseValidator"/> <bean id="priceIncreaseForm" class="web.PriceIncreaseFormController"> <property name="sessionForm"><value>true</value></property> <property name="commandName"><value>priceIncrease</value></property> <property name="commandClass"><value>bus.PriceIncrease</value></property> <property name="validator"><ref bean="priceIncreaseValidator"/></property> <property name="formView"><value>priceincrease</value></property> <property name="successView"><value>hello.htm</value></property> <property name="productManager"> <ref bean="prodMan"/> </property> </bean> <bean id="prodMan" class="bus.ProductManager"> <property name="products"> <list> <ref bean="product1"/> <ref bean="product2"/> <ref bean="product3"/> </list> </property> </bean> <bean id="product1" class="bus.Product"> <property name="description"><value>Lamp</value></property> <property name="price"><value>5.75</value></property> </bean> <bean id="product2" class="bus.Product"> <property name="description"><value>Table</value></property> <property name="price"><value>75.25</value></property> </bean> <bean id="product3" class="bus.Product"> <property name="description"><value>Chair</value></property> <property name="price"><value>22.79</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="/priceincrease.htm">priceIncreaseForm</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 the increasePrice method on the ProductManager object. It then returns a ModelAndView passing in a new instance of a RedirectView created using the url for the successView.

springapp/src/web/PriceIncreaseFormController.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 bus.Product;
import bus.ProductManager;
import bus.PriceIncrease;

public class PriceIncreaseFormController extends SimpleFormController {

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

private ProductManager prodMan;

public ModelAndView onSubmit(Object command)
throws ServletException {

int increase = ((PriceIncrease) command).getPercentage();
logger.info("Increasing prices by " + increase + "%.");

prodMan.increasePrice(increase);

String now = (new java.util.Date()).toString();
logger.info("returning from PriceIncreaseForm view to " + getSuccessView();

return new ModelAndView(new RedirectView(getSuccessView()));
}

protected Object formBackingObject(HttpServletRequest request) throws ServletException {

PriceIncrease priceIncrease = new PriceIncrease();
priceIncrease.setPercentage(20);

return priceIncrease;

}

public void setProductManager(ProductManager pm) {
prodMan = pm;
}

public ProductManager getProductManager() {
return prodMan;
}

}

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

springapp/war/WEB-INF/classes/messages.properties

title=SpringApp
heading=Hello :: SpringApp
greeting=Greetings, it is now
priceincrease.heading=Price Increase :: SpringApperror.not-specified=Percentage not specified!!!error.too-low=You have to specify a percentage higher than {0}!error.too-high=Don't be greedy - you can't raise prices by more than {0}%!required=Entry required.typeMismatch=Invalid data.typeMismatch.percentage=That is not a number!!!

Finally, we have to provide a link to the priceincrease 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>
<p><fmt:message key="greeting"/> <c:out value="${model.now}"/>
</p>
<h3>Products</h3>
<c:forEach items="${model.products}" var="prod">
<c:out value="${prod.description}"/> <i>$<c:out value="${prod.price}"/></i><br><br>
</c:forEach>
<br><a href="<c:url value="priceincrease.htm"/>">Increase Prices</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.


posted on 2006-08-09 23:23 topquan 阅读(428) 评论(0)  编辑  收藏 所属分类: Spring


只有注册用户登录后才能发表评论。


网站导航: