Development

Testing Persistent Objects Without Spring

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
  • 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

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);
    }
}

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.

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;
        }
    }
}

Unit Of Work

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.

Screenshot of IntelliJ Project

Contact me - using the contact me page if you would like access to the svn repo. All the code is here though.

comments powered by Disqus