Testing Persistent Objects Without Spring
The Spring fallacy
Spring proponents will have you believe that "Unit Testing" of persistent classes is very hard without spring. This is not the case, and can be accomplished in only a few classes.
This is not spring-bashing - I'm sure that Spring is very useful for some. This is simply an alternative approach to one that was presented at InfoQ
Requirements
To use these examples you will need the following libraries - all open source
- Hibernate
- Using Oracle Jars http://www.oracle.com - then sign up
- Hamcrest
- J2EE Api - whereever you can get it.
Persistance
We will be using a simple annotated class just to show the example.
package net.time4tea.infoq.domain; import javax.persistence.*; import java.math.BigDecimal; @Entity public class Loan { public enum Status { IN_REVIEW, SUB_PRIME, DEFAULTED } @Id @GeneratedValue private long id; private BigDecimal amount; private String currency; @Enumerated(EnumType.STRING) private Status status; private BigDecimal purchasePrice; public Loan() { // only for JPA } public Loan(BigDecimal amount, String currency, Status status, BigDecimal purchasePrice) { this.amount = amount; this.currency = currency; this.status = status; this.purchasePrice = purchasePrice; } public long getId() { return id; } public BigDecimal getAmount() { return amount; } public String getCurrency() { return currency; } public Status getStatus() { return status; } public BigDecimal getPurchasePrice() { return purchasePrice; } }
The Persistent Stuff
This is where you might have used the ejbFindBy methods back in the day... its just an interface.. so you can conveniently have mocks and stubs for them.
package net.time4tea.infoq; import net.time4tea.infoq.domain.Loan; import javax.persistence.PersistenceException; public interface Loans { Loan findById(long id) throws PersistenceException; void add(Loan loan) throws PersistenceException; }
And an implementation for the interface
Using an EntityManager. This means that we will create them in the transactional context in which they are used, rather than having them around for ever - as we might have used with an EntityManagerFactory. Short lived objects are very efficent these days.
package net.time4tea.infoq; import net.time4tea.infoq.domain.Loan; import javax.persistence.PersistenceException; import javax.persistence.EntityManager; import javax.persistence.Query; public class PersistentLoans implements Loans { private EntityManager entityManager; public PersistentLoans(EntityManager entityManager) { this.entityManager = entityManager; } public Loan findById(long id) throws PersistenceException { Query query = entityManager.createQuery("select loan from Loan loan where loan.id = :id"); query.setParameter("id", id); return (Loan) query.getSingleResult(); } public void add(Loan loan) throws PersistenceException { entityManager.persist(loan); } }
Transactional Context
If we are to have any hope of using the entity manager outside of a J2EE container, then we need to do so within a transaction. Managing this as part of a testcase is a bit hit-and-miss - better abstract it.
package net.time4tea.infoq.testsupport; import javax.persistence.EntityManager; import javax.persistence.EntityTransaction; import javax.persistence.PersistenceException; public class Transactor { private final EntityManager entityManager; public Transactor(EntityManager entityManager) { this.entityManager = entityManager; } public void transact(UnitOfWork work) throws Exception { EntityTransaction transaction = entityManager.getTransaction(); transaction.begin(); try { work.work(); transaction.commit(); } catch (PersistenceException e ) { throw e; } catch (Exception e) { transaction.rollback(); throw e; } } }
package net.time4tea.infoq.testsupport; public interface UnitOfWork { void work() throws Exception; }
Creating a test Configuration
All systems have some way of getting their configuration. In real code the implementation here would be a little more intelligent.
package net.time4tea.infoq.conf; public class SystemConfiguration { private String databaseUrl; private String databaseUser; private String databasePassword; SystemConfiguration(String databaseUrl, String databaseUser, String databasePassword) { this.databaseUrl = databaseUrl; this.databaseUser = databaseUser; this.databasePassword = databasePassword; } public String getDatabaseUrl() { return databaseUrl; } public String getDatabaseUser() { return databaseUser; } public String getDatabasePassword() { return databasePassword; } public static SystemConfiguration load() { return new SystemConfiguration("jdbc:oracle:thin:@localhost:1521:XE", "james", "james"); } }
Creating a test Entity Manager
This example uses Hibernate and Oracle, but you could do this with lots of other implementations. Note that the actual implementation doesn't escape - as far as the rest of the code is concerned, we are still only talking about EntityManagers.
To keep the amount of code in this page short - I used the autoDDL feature of hibernate. I would suggest never do this, and make sure to have a good database rebuilding script as part of the build process. Dropping and recreating databases takes only seconds.
package net.time4tea.infoq.testsupport; import net.time4tea.infoq.domain.Loan; import net.time4tea.infoq.conf.SystemConfiguration; import org.hibernate.ejb.HibernatePersistence; import static org.hibernate.ejb.HibernatePersistence.LOADED_CLASSES; import javax.persistence.EntityManagerFactory; import java.util.ArrayList; import static java.util.Collections.unmodifiableList; import java.util.List; import java.util.Properties; public class TestConfiguration { public static final List<Class<?>> PERSISTENT_CLASSES = unmodifiableList(new ArrayList<Class<?>>() {{ add(Loan.class); }}); public static EntityManagerFactory createEntityManagerFactory(SystemConfiguration systemConfiguration) { Properties hibernateConfiguration = new Properties(); hibernateConfiguration.put("hibernate.dialect", "org.hibernate.dialect.OracleDialect"); hibernateConfiguration.put("hibernate.connection.driver_class", "oracle.jdbc.driver.OracleDriver"); hibernateConfiguration.put("hibernate.connection.url", systemConfiguration.getDatabaseUrl()); hibernateConfiguration.put("hibernate.connection.username", systemConfiguration.getDatabaseUser()); hibernateConfiguration.put("hibernate.connection.password", systemConfiguration.getDatabasePassword()); hibernateConfiguration.put("hibernate.show_sql", "true"); hibernateConfiguration.put("hibernate.hbm2ddl.auto", "update"); //only here for this example - use a script! hibernateConfiguration.put(LOADED_CLASSES, PERSISTENT_CLASSES); HibernatePersistence p = new HibernatePersistence(); return p.createEntityManagerFactory(hibernateConfiguration); } }
Builders
We like to use the Builder pattern - it makes code much cleaner!
package net.time4tea.infoq.domain; import java.math.BigDecimal; public class LoanBuilder { private BigDecimal amount = new BigDecimal(1000); private String currency = "USD"; private Loan.Status status = Loan.Status.SUB_PRIME; private BigDecimal purchasePrice = new BigDecimal(10000000); public Loan build() { return new Loan(amount, currency, status, purchasePrice); } public LoanBuilder withAmount(BigDecimal amount) { this.amount = amount; return this; } public LoanBuilder withCurrency(String currency) { this.currency = currency; return this; } public LoanBuilder withStatus(Loan.Status status) { this.status = status; return this; } public LoanBuilder withPurchasePrice(BigDecimal purchasePrice) { this.purchasePrice = purchasePrice; return this; } }
The actual persistence test
package net.time4tea.infoq.persistence; import junit.framework.TestCase; import net.time4tea.infoq.Loans; import net.time4tea.infoq.PersistentLoans; import net.time4tea.infoq.conf.SystemConfiguration; import net.time4tea.infoq.domain.Loan; import net.time4tea.infoq.domain.LoanBuilder; import net.time4tea.infoq.testsupport.TestConfiguration; import net.time4tea.infoq.testsupport.Transactor; import net.time4tea.infoq.testsupport.UnitOfWork; import org.hamcrest.*; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; public class PersistentLoansTest extends TestCase { private Loans persistentLoans; private Transactor transactor; @Override protected void setUp() throws Exception { SystemConfiguration configuration = SystemConfiguration.load(); EntityManagerFactory entityManagerFactory = TestConfiguration.createEntityManagerFactory(configuration); EntityManager entityManager = entityManagerFactory.createEntityManager(); persistentLoans = new PersistentLoans(entityManager); transactor = new Transactor(entityManager); } public void testCanSaveALoanAndFindItByItsId() throws Exception { final Loan loan = new LoanBuilder().build(); transactor.transact(new UnitOfWork() { public void work() throws Exception { persistentLoans.add(loan); } }); transactor.transact(new UnitOfWork() { public void work() throws Exception { Loan loaded = persistentLoans.findById(loan.getId()); MatcherAssert.assertThat(loaded, hasSameNonTransientFieldsAs(loan)); } }); } // you would genericalise this with a simple bit of reflection to be generally useful private Matcher<Loan> hasSameNonTransientFieldsAs(final Loan given) { return new TypeSafeMatcher<Loan>() { public boolean matchesSafely(Loan loan) { return given.getAmount().equals(loan.getAmount()) && given.getId() == loan.getId() && given.getCurrency().equals(loan.getCurrency()) && given.getPurchasePrice().equals(loan.getPurchasePrice()) && given.getStatus().equals(loan.getStatus()); } public void describeTo(Description description) { description.appendText("has same fields as " ); description.appendValue(given); } }; } }
How it all looks
Here's a screenshot from IntelliJ after we have put all the code together.
Contact me - using the contact me page if you would like access to the svn repo. All the code is here though.
Screenshot

Thanks
Nat & Nick
Complaints
To me...
Comments ( Hide | Add Comment )
Anonymous says:Nov 21, 2007 09:25 ( Permalink | ) |
Anonymous says:The matcher should be hasSameTransientFieldsAs, not hasSameNonTransientFieldsAs, |
|
|
James Richardson says:No it shouldn't. Transient fields are not persisted. We are trying to test that the persisted fields are loaded back in correctly. |
This is helpful, thanks for the post. Perhaps your own article on InfoQ next time?
JAW
http://jawspeak.com