Test-Driven Development
with Spring and Hibernate
Matt Raible
[email protected]appfuse.dev.java.net © 2005, Virtuas, LLC
Introductions
Your experience with webapps?
Your experience with J2EE?
What do you want to get from this tutorial?
Open Source experience with Ant, XDoclet,
Hibernate, Spring, Eclipse/IDEA?
appfuse.dev.java.net © 2005, Virtuas, LLC
Who is Matt Raible?
appfuse.dev.java.net © 2005, Virtuas, LLC
Agenda ~ Part I
Overview of J2EE, Spring and Hibernate
AppFuse: History, Overview and Demo
Development Environment: JDK, Ant, AppFuse,
Eclipse, MySQL and Tomcat
appfuse.dev.java.net © 2005, Virtuas, LLC
Agenda ~ Part II
[Lab] Creating database tables using Ant and
Hibernate
JUnit and Test-Driven Development
Review of interface vs. implementation classes
[Lab] Writing a WeblogDAOTest JUnit test
Overview of Spring’s DAO Support classes for
Hibernate
[Lab] Write WeblogDAO interface and
WeblogDAOHibernate implementation
appfuse.dev.java.net © 2005, Virtuas, LLC
Agenda ~ Part III
Business Facade: What is it and why should I use
one?
[Lab] Writing a WeblogManagerTest with jMock
[Lab] Writing a WeblogManager interface and
implementation
[Lab] Defining the “weblogManager” bean and per-
method transaction attributes
Alternative I: Generic DAO and Manager
Alternative II: Code Generation
appfuse.dev.java.net © 2005, Virtuas, LLC
Agenda ~ Optional
Pick your favorite Java web framework:
JSF, Struts, Spring MVC, Tapestry or WebWork
Testing Controllers (or page backing objects) for
selected framework
Testing the UI with Canoo WebTest and Ant
appfuse.dev.java.net © 2005, Virtuas, LLC
What is Spring?
www.springlive.com © 2005, Virtuas, LLC
Spring Mission Statement
www.springlive.com © 2005, Virtuas, LLC
Typical Java Application
www.springlive.com © 2005, Virtuas, LLC
Spring Modules
www.springlive.com © 2005, Virtuas, LLC
Sample Architecture
www.springlive.com © 2005, Virtuas, LLC
The BeanFactory
www.springlive.com © 2005, Virtuas, LLC
Get that Context!
ClassPathXmlApplicationContext - load files from
classpath
FileSystemXmlApplicationContext - load from
filesystem (relative or absolute paths)
StaticXmlApplicationContext - allows
programmatic registration of beans
XmlWebApplicationContext - used by
ContextLoaderListener to read paths from web.xml
www.springlive.com © 2005, Virtuas, LLC
ClassPathXmlApplicationContext
ApplicationContext ctx = new ClassPathXmlApplicationContext("/WEB-INF/applicationContext.xml");
String paths[] = new String[] {"/WEB-INF/applicationContext-hibernate.xml",
"/WEB-INF/applicationContext.xml"};
ApplicationContext ctx = new ClassPathXmlApplicationContext(paths);
ApplicationContext ctx = new ClassPathXmlApplicationContext("/WEB-INF/applicationContext*.xml");
www.springlive.com © 2005, Virtuas, LLC
Dependency Injection
www.springlive.com © 2005, Virtuas, LLC
Example: Controller and DAO
Controller has a setter or a constructor argument for
the DAO
DAO is configured as a dependency of the
Controller
<bean id="userController" class="org.appfuse.web.UserController">
<property name="userDAO" ref="userDAO"/>
</bean>
<bean id="userDAO" class="org.appfuse.dao.UserDAOHibernate">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
Makes it easy to swap out implementations
Tests can verify operations succeed on the interface
www.springlive.com © 2005, Virtuas, LLC
Code before IoC
public class UserController implements Controller {
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response)
throws Exception {
Connection conn = DatabaseUtils.getConnection();
UserDAO dao = DAOFactory.createUserDAO("hibernate", conn);
List users = dao.getUsers();
DatabaseUtils.closeConnection(conn);
return new ModelAndView("userList", "users", users);
}
}
www.springlive.com © 2005, Virtuas, LLC
Code after IoC
public class UserController implements Controller {
private UserDAO dao = null;
public void setUserDAO(UserDAO userDAO) {
this.dao = userDAO;
}
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response)
throws Exception {
List users = dao.getUsers();
return new ModelAndView("userList", "users", users);
}
}
www.springlive.com © 2005, Virtuas, LLC
Constructor vs. Setter
Injection
www.springlive.com © 2005, Virtuas, LLC
Spring vs. J2EE
www.springlive.com © 2005, Virtuas, LLC
Data Access Support
Meaningful Exceptions
No more try/catch/finally
DaoSupport and Template classes for many
frameworks
Hibernate, iBATIS, JDO, OJB and TopLink
www.springlive.com © 2005, Virtuas, LLC
Rich Exception Hierarchy
www.springlive.com © 2005, Virtuas, LLC
Hibernate
www.springlive.com © 2005, Virtuas, LLC
Mapping a POJO
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="org.appfuse.model.User" table="app_user">
<id name="id" column="id" unsaved-value="null">
<generator class="native"/>
</id>
<property name="firstName" column="first_name" not-null="true"/>
<property name="lastName" column="last_name" not-null="true"/>
</class>
</hibernate-mapping>
www.springlive.com © 2005, Virtuas, LLC
Configuring Hibernate
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mappingResources">
<list>
<value>org/appfuse/model/User.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
<prop key="hibernate.hbm2ddl.auto">update</prop>
</props>
</property>
</bean>
<bean id="transactionManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
www.springlive.com © 2005, Virtuas, LLC
Using Hibernate
public class UserDAOHibernate extends HibernateDaoSupport implements UserDAO {
public List getUsers() {
return getHibernateTemplate().find("from User");
}
public User getUser(Long id) {
return (User) getHibernateTemplate().get(User.class, id);
}
public void saveUser(User user) {
getHibernateTemplate().saveOrUpdate(user);
}
public void removeUser(Long id) {
getHibernateTemplate().delete(getUser(id));
}
}
www.springlive.com © 2005, Virtuas, LLC
Callbacks
public List getUsers(final User user) {
if (user == null) {
return getHibernateTemplate().find("from User");
} else {
// filter on properties set in the user object
HibernateCallback callback = new HibernateCallback() {
public Object doInHibernate(Session session) throws HibernateException {
Example ex = Example.create(user).ignoreCase()
.enableLike(MatchMode.ANYWHERE);
return session.createCriteria(User.class).add(ex).list();
}
};
return (List) getHibernateTemplate().execute(callback);
}
}
www.springlive.com © 2005, Virtuas, LLC
Lazy-Loading Support
OpenSessionInViewFilter and Interceptor
Domain objects with lazy-loaded collections need
to keep Session/PersistenceManager opened in unit
tests
www.springlive.com © 2005, Virtuas, LLC
Configuring Filters
/WEB-INF/web.xml
<filter>
<filter-name>hibernateFilter</filter-name>
<filter-class>org.springframework.orm.hibernate3.support.OpenSessionInViewFilter</filter-class>
</filter>
<!-- The 'hibernateFilter', a.k.a. Spring's OpenSessionInViewFilter
guarantees one session per request. Performance seems to be the
same if it's enabled or disabled. -->
<filter-mapping>
<filter-name>hibernateFilter</filter-name>
<url-pattern>*.html</url-pattern>
</filter-mapping>
www.springlive.com © 2005, Virtuas, LLC
Testing Hibernate
public void testGetUsers() {
// add a record to the database so we have something to work with
User user = new User("Easter", "Bunny");
dao.saveUser(user);
List users = dao.getUsers();
assertTrue(users.size() >= 1);
assertTrue(users.contains(user));
}
www.springlive.com © 2005, Virtuas, LLC
Testing with Lazy-Loading
protected void setUp() throws Exception {
// the following is necessary for lazy loading
sf = (SessionFactory) ctx.getBean("sessionFactory");
// open and bind the session for this test thread.
Session s = sf.openSession();
TransactionSynchronizationManager
.bindResource(sf, new SessionHolder(s));
// setup code here
}
protected void tearDown() throws Exception {
// unbind and close the session.
SessionHolder holder = (SessionHolder)
TransactionSynchronizationManager.getResource(sf);
Session s = holder.getSession();
s.flush();
TransactionSynchronizationManager.unbindResource(sf);
SessionFactoryUtils.closeSessionIfNecessary(s, sf);
// teardown code here
}
www.springlive.com © 2005, Virtuas, LLC
Spring MVC
www.springlive.com © 2005, Virtuas, LLC
Spring Supports many
Java MVC Frameworks
ContextLoaderListener loads beans into
ServletContext - can retrieve with:
WebApplicationContext ctx =
WebApplicationContextUtils.getWebApplicationContext(servletContext);
Struts has ContextLoaderPlugin and ActionSupport
classes
WebWork has SpringObjectFactory
Tapestry - override BaseEngine and stuff the context
into the Global
JSF - DelegatingVariableResolver
www.springlive.com © 2005, Virtuas, LLC
Transactions
Much easier to use than UserTransaction in J2EE
CMT-like capabilities with XML and transparent
AOP
Supports Commons Attributes and JDK 5
Annotations
Pluggable Transaction Managers, including JTA
www.springlive.com © 2005, Virtuas, LLC
AOP
AOP - Reduces duplication among classes
Interceptors make it easy to add before, around and
after method advice
Useful for caching, logging and security
EHCache, Performance/Tracing interceptors,
Acegi for Security
www.springlive.com © 2005, Virtuas, LLC
Testing Spring Applications
www.springlive.com © 2005, Virtuas, LLC
Test-First Development
www.springlive.com © 2005, Virtuas, LLC
Unit vs. Integration Testing
www.springlive.com © 2005, Virtuas, LLC
Tools for Testing
JUnit, TestNG, JTiger
Comparison: http://www.theserverside.com/news/thread.tss?
thread_id=35248
EasyMock and jMock
Spring Mocks
DbUnit
Cactus
jWebUnit, Canoo WebTest and Selenium
Cargo
Anthill, CruiseControl, Continuum
www.springlive.com © 2005, Virtuas, LLC
JUnit
package org.appfuse;
import junit.framework.TestCase;
import org.appfuse.model.User;
public class SimpleTest extends TestCase {
public void testGetFullName() {
User user = new User();
user.setFirstName("Jack");
user.setLastName("Raible");
assertEquals("Jack Raible", user.getLastName());
}
}
www.springlive.com © 2005, Virtuas, LLC
EasyMock and jMock
EasyMock: provides mocks for interfaces in JUnit
tests by generating them on-the-fly using Java's
proxy mechanism.
jMock is similar, but requires you to extend
MockObjectTestCase.
www.springlive.com © 2005, Virtuas, LLC
EasyMock Example
private UserManager mgr = new UserManagerImpl();
private MockControl control;
private UserDAO mockDAO;
protected void setUp() throws Exception {
// Create a MockControl
control = MockControl.createControl(UserDAO.class);
// Get the mock
mockDAO = (UserDAO) control.getMock();
mgr.setUserDAO(mockDAO);
}
protected void testGetUser() {
// Set expected behavior
mockDAO.removeUser(new Long(1));
control.setVoidCallable();
// Active the mock
control.replay();
// Execute method to test
mgr.removeUser("1");
// Verify methods called
control.verify();
}
www.springlive.com © 2005, Virtuas, LLC
jMock Example
private UserManager mgr = new UserManagerImpl();
private Mock mockDAO;
protected void setUp() throws Exception {
// Create a Mock
mockDAO = new Mock(UserDAO.class);
// Set dependencies
mgr.setUserDAO((UserDAO) mockDAO.proxy());
}
protected void testGetUser() {
// Set expected behavior
mockDAO.expects(once()).method("getUser")
.with( eq(new Long(1)));
// Execute method to test
mgr.removeUser("1");
// Verify expectations
mockDAO.verify();
}
www.springlive.com © 2005, Virtuas, LLC
Spring Mocks
Stubs for Servlet and JNDI API
Spring’s org.springframework.test package has a
number of base classes to simplify testing
AbstractDependencyInjectionSpringContextTests - can
do both setter or field-based dependency injection,
cached context files
AbstractTransactionalDataSourceSpringContextTests -
allows you to easily clear data from tables and rolls back
any data entered into the database
www.springlive.com © 2005, Virtuas, LLC
IoC in Tests
public class UserManagerIntegrationTest extends AbstractTransactionalDataSourceSpringContextTests {
private UserManager userManager;
public void setUserManager(UserManager userManager) {
this.userManager = userManager;
}
protected String[] getConfigLocations() {
return new String[] {"classpath*:/WEB-INF/applicationContext*.xml"};
}
protected void onSetUpInTransaction() throws Exception {
deleteFromTables(new String[] {"users"});
}
public void testGetUsers() {
User kid1 = new User("Abbie", "Raible");
User kid2 = new User("Jack", "Raible");
userManager.saveUser(kid1);
userManager.saveUser(kid2);
assertEquals(2, userManager.getUsers().size());
}
}
www.springlive.com © 2005, Virtuas, LLC
JNDI in Tests
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName" value="java:comp/env/jdbc/appfuse"/>
</bean>
import org.springframework.mock.jndi.SimpleNamingContextBuilder;
try {
SimpleNamingContextBuilder builder =
SimpleNamingContextBuilder.emptyActivatedContextBuilder();
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName("com.mysql.jdbc.Driver");
ds.setUrl("jdbc:mysql://localhost/appfuse");
ds.setUsername("root");
ds.setPassword("");
builder.bind("java:comp/env/jdbc/appfuse", ds);
} catch (NamingException ne) {
// do nothing, test will fail on its own
}
www.springlive.com © 2005, Virtuas, LLC
DBUnit
private IDatabaseConnection conn = null;
private IDataSet dataSet = null;
protected void setUp() throws Exception {
DataSource ds = (DataSource) ctx.getBean("dataSource");
conn = new DatabaseConnection(ds.getConnection());
dataSet = new XmlDataSet(new FileInputStream(
"test/data/sample-data.xml"));
// clear table and insert only sample data
DatabaseOperation.CLEAN_INSERT.execute(conn, dataSet);
}
protected void tearDown() throws Exception {
// clear out database
DatabaseOperation.DELETE.execute(conn, dataSet);
conn.close();
conn = null;
}
www.springlive.com © 2005, Virtuas, LLC
Canoo WebTest
<target name="UserTests"
description="Adds a new user profile">
<canoo name="userTests">
&config;
<steps>
<!-- View add screen -->
<invoke url="/editUser.html"/>
<verifytitle text="${userForm.title}"/>
<!-- Enter data and save -->
<setinputfield name="firstName" value="Test"/>
<setinputfield name="lastName" value="Name"/>
<clickbutton label="Save"/>
<!-- View user list -->
<verifytitle text="${userList.title}"/>
<!-- Verify PDF contains new user -->
<clicklink label="PDF"/>
<verifyPdfText text="Test Name"/>
<!-- Delete first user in table -->
<invoke url="/users.html"/>
<clicklink href="editUser.html"/>
<verifytitle text="${userForm.title}"/>
<clickbutton label="Delete"/>
<verifytitle text="${userList.title}"/>
</steps>
</canoo>
</target>
www.springlive.com © 2005, Virtuas, LLC
jWebUnit
public class UserWebTest extends WebTestCase {
public UserWebTest(String name) {
super(name);
getTestContext().setBaseUrl("http://localhost:8080/appfuse");
}
public void testWelcomePage() {
beginAt("/");
assertTitleEquals("AppFuse ~ Welcome");
}
public void testAddUser() {
beginAt("/editUser.html");
assertTitleEquals("AppFuse ~ User Details");
setFormElement("firstName", "Jack");
setFormElement("lastName", "Raible");
submit("save");
assertTextPresent("saved successfully");
}
}
www.springlive.com © 2005, Virtuas, LLC
Cargo
Container container =
new Resin3xContainer(new Resin3xStandaloneConfiguration("target/resin3x"));
container.setHomeDir("c:/apps/resin-3.0.8");
container.start();
<cargo-resin3x homeDir="c:/apps/resin-3.0.8" action="start"/>
www.springlive.com © 2005, Virtuas, LLC
AppFuse - what is it?
A directory structure, build file and project classes
to get your project started quickly
The hard part is getting started and configuring
dependencies
Uses popular open-source tools: Ant, XDoclet,
Spring, Hibernate, Struts (or JSF, Spring MVC,
WebWork or Tapestry)
Top 5 java.net project in hits, accesses and mail
traffic
appfuse.dev.java.net © 2005, Virtuas, LLC
History
Started as a sample app for Pro JSP
Became a toolkit for starting new projects
Lots of community feedback makes it a “best
practices” webapp for J2EE
Documentation and Tutorials (November 2003)
AppGen - CRUD made easy (November 2004)
New Committers: Nathan, Ben and Sanjiv (2005)
appfuse.dev.java.net © 2005, Virtuas, LLC
Dependencies
Optional Installs
appfuse.dev.java.net © 2005, Virtuas, LLC
Directory Structure
appfuse.dev.java.net © 2005, Virtuas, LLC
Demo of Features
Acegi Security - makes security more portable
between containers
Remember Me and Self Registration
GZip Compression Built-in
Testing environment ready to go, many tutorials,
CruiseControl files included
http://demo.appfuse.org/appfuse
appfuse.dev.java.net © 2005, Virtuas, LLC
Development Environment
Download and install:
Ant 1.6.2+
MySQL 4.1.x (or 5.0.x)
Tomcat 5.0.29
Eclipse 3.1 (or IDEA 4.5.x)
AppFuse 1.8.1
http://raibledesigns.com/wiki/Wiki.jsp?
page=DevelopmentEnvironment
appfuse.dev.java.net © 2005, Virtuas, LLC
Testing DAOs
appfuse.dev.java.net © 2005, Virtuas, LLC
Create database & table
Create new project and database with Ant
Create Weblog.java POJO and generate Hibernate
mapping file with XDoclet
Configure Spring to be aware of Weblog object
Create “weblog” table from POJO using Ant
appfuse.dev.java.net © 2005, Virtuas, LLC
JUnit and Interfaces
TDD makes you think before you code
JUnit is easiest to test with because of plethora of
tools and examples
Writing interfaces allows de-coupling of layers and
implementation
Write code to test interface (integration test) or
implementation (unit test)
appfuse.dev.java.net © 2005, Virtuas, LLC
Let’s write some code!
What is a DAO?
Create WeblogDAOTest - Test First!
Create WeblogDAO Interface
Create WeblogDAOHibernate implementation
Create “weblogDAO” bean definition in Spring
context file
Run JUnit Test
appfuse.dev.java.net © 2005, Virtuas, LLC
Spring simplifies testing
Spring’s org.springframework.test package has a
number of base classes to simplify testing
AbstractDependencyInjectionSpringContextTests - can
do both setter or field-based dependency injection,
cached context files
AbstractTransactionalDataSourceSpringContextTests -
exposes a JdbcTemplate for easy querying and rolls back
any data entered into the database
appfuse.dev.java.net © 2005, Virtuas, LLC
Testing with Mocks
appfuse.dev.java.net © 2005, Virtuas, LLC
Business Facades
Encapsulates business logic for multiple clients
Similar to Business Delegate pattern for EJBs
Provide client-friendly, transactional access to data
layer
Facilitates calling multiple DAOs within the same
transaction
Can be exposed as web services
appfuse.dev.java.net © 2005, Virtuas, LLC
Yee haw - more code!
Create WeblogManagerTest - a true unit test that
uses jMock for mocking dependencies
Create WeblogManager Interface
Create WeblogManagerImpl implementation
Run JUnit Test
Create “weblogManager” bean definition
appfuse.dev.java.net © 2005, Virtuas, LLC
So this is better, huh?
Created 7 classes, modified 2 files, generated 1
Faster than traditional EJB/JDBC, but still painful
Faster options:
BaseDAO/Manager classes in AppFuse for
Generic CRUD on any POJO
Generate/modify files with AppGen or AppFuse
Generator
appfuse.dev.java.net © 2005, Virtuas, LLC
AppGen
To create CRUD for a table, it required you create
11 files and modify 5
AppGen requires you create 1 and modify 1
Uses BaseHibernateDAO and BaseManager for
generic CRUD methods
Still requires you to “pretty up” the UI
Demonstration ➭
appfuse.dev.java.net © 2005, Virtuas, LLC
Web Development
appfuse.dev.java.net © 2005, Virtuas, LLC
Creating the UI
WeblogControllerTest and WeblogFormControllerTest
WeblogController and WeblogFormController
weblogList.jsp and weblogForm.jsp
Validation Rules with XDoclet and Commons
Validator
Testing the UI with Canoo WebTest and Ant
appfuse.dev.java.net © 2005, Virtuas, LLC
End of Coding
Any code tweaks you’d like to see?
Deploying to production
Setup MySQL with create-tables.sql
Setup Tomcat with JDBC Drivers
Deploy using Tomcat’s Manager application
Hosting: kgbinternet.com, contegix.com
appfuse.dev.java.net © 2005, Virtuas, LLC
AppFuse Roadmap
Continue to try and make IDE integration easier
Support/Documentation for more app servers
iBATIS Tutorials, refactor build/test process
Possibly an AppFuse Plugin for Eclipse
AppFuse 2.0: Annotations, JSP 2.0, JDK 5
Other things you’d like to see?
appfuse.dev.java.net © 2005, Virtuas, LLC
Equinox
AppFuse Light - designed for quick apps with few
requirements (i.e. prototypes)
No build-time dependencies (i.e. XDoclet), no out-
of-the-box security
Includes 5 MVC implementations: JSF, Spring MVC,
Struts, Tapestry and WebWork
Includes 5 Persistence frameworks: Hibernate,
iBATIS, JDO, OJB, Spring JDBC
Located at http://equinox.dev.java.net
appfuse.dev.java.net © 2005, Virtuas, LLC
Questions?
AppFuse Official Homepage:
http://appfuse.dev.java.net
Demos and Videos:
http://demo.appfuse.org/demos.html
Tutorials:
http://appfuse.org/tutorials
This Presentation:
http://appfuse.dev.java.net/
TDDWithAppFuse.pdf
appfuse.dev.java.net © 2005, Virtuas, LLC