Filed under: Java, TDD, — Tags: Database integration, Database integration test, HSQL, Test driven development — Thomas Sundberg — 2011-08-07
Development guided by test is perhaps the best way to make sure that your solution works and is possible to verify. I will show how you can develop a database layer guided by tests. It will be divided into two parts. The first part will be an in memory solution and the second part will be implemented using Java Persistence API, JPA, and Hypersonic. The second implementation is supposed to replace the first one guided by the tests developed for the in memory solution.
The reason for developing in two steps is that it is fast to develop and change the in memory solution. You will be able to get a working solution that supports the demands of your application fast. It takes longer time to develop a complete database support layer and it is harder to change when the demands on the application changes.
The in memory solution should be replaced by a database supported solution when all demands are understood. The transition is supported by the test cases used to create the in memory solution. They define the demands on the backend. You are done with the transition when all tests passes.
All code needed will be presented at the end of each part. You should be able to cut and paste all files and get an example up and running.
The first step is to create a working solution in memory.
The motivation is that during development we want to be able to verify the logic in the system we are building without locking ourselves to a database design. The database support is of course very important, but it is just a support function so we don't need to start with it. It is better to have a working solution and add persistence support later. That is working from the outside in, instead of inside out and hope that we creates something that the users want.
The example we will be building is about storing and retrieving a set of cars.
Let us start with an interface defining a Car. I will use the simplest possible design here and a Car only has three methods as defined below:
package se.sigma.educational.dto; public interface Car { String getRegistrationNumber(); String getBrand(); String getColor(); }
Why do I to create an interface to define something so trivial as the car above? I'm planning ahead somewhat and want to be able to avoid returning null when a car can't be located. This is preparation for implementing a null car.
An implementation of the car interface could look like this:
package se.sigma.educational.dto; public class CarImpl implements Car { private String registrationNumber; private String brand; private String color; public CarImpl(String registrationNumber, String brand, String color) { this.registrationNumber = registrationNumber; this.brand = brand; this.color = color; } @Override public String getRegistrationNumber() { return registrationNumber; } @Override public String getBrand() { return brand; } @Override public String getColor() { return color; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null) { return false; } if (!(o instanceof Car)) { return false; } Car that = (Car) o; if (registrationNumber != null) { if (!registrationNumber.equals(that.getRegistrationNumber())) return false; } else { if (that.getRegistrationNumber() != null) return false; } return true; } @Override public int hashCode() { if (registrationNumber != null) return registrationNumber.hashCode(); else return 0; } @Override public String toString() { return "Car{" + "registrationNumber='" + registrationNumber + '\'' + ", brand='" + brand + '\'' + ", color='" + color + '\'' + '}'; } }
The definitions of equals() and hashCode() tells us that the registration number will be used as the primary key. Two cars are equal when their registration numbers are equal. We don't care about color or brand when we compare two cars.
I will test drive the implementation of the data store. This is a simple implementation and it could be argued that test driving it is an overkill. I don't think so, test driving it will make sure that I get all details correct even in a simple solution.
I will start by creating a Car. The test will use a created car, call the create method in the dao that will return the created car and finally assert that the returned car is the same car as the car that was given as an argument.
@Test public void createCar() { Car actualCar = carDao.create(expectedCar); assertThat(actualCar, is(expectedCar)); }
The resulting production code is:
public Car create(Car car) { String registrationNumber = car.getRegistrationNumber(); cars.put(registrationNumber, car); return car; }
The car that was received is stored somewhere and then returned. Simple and easy to change if the business demands changes and fast to test during development.
Next thing to add is find a car. The tests looks like this:
@Test public void findCar() { carDao.create(expectedCar); String registrationNumber = expectedCar.getRegistrationNumber(); Car actualCar = carDao.find(registrationNumber); assertThat(actualCar, is(expectedCar)); }
and the unhappy path:
@Test public void findCarIncorrectRegistrationNumber() { Car expectedCar = new CarNullImpl(); String registrationNumber = "foo"; Car actualCar = carDao.find(registrationNumber); assertThat(actualCar, is(expectedCar)); }
The corresponding production code is:
public Car find(String registrationNumber) { Car result = cars.get(registrationNumber); if (result == null) { return new CarNullImpl(); } return result; }
The car is retrieved from some sort of collection where the registration number is used as key.
Java collections return null when a value isn't found. Returning null means that you have to check for null later in
your code. This is a NullPointerException
waiting to happen. Instead of returning null I choose to return a car that isn't dangerous to use in the code. I
return a null car implementation that looks like a car, acts like a car and is easy to recognise as a null car. You
may check for a null car if you need to take any special action when a car isn't found. Checking if the result is a
null car is better than checking for null.
An implementation of the null car may look like this:
package se.sigma.educational.dto; public class CarNullImpl implements Car { private String registrationNumber = "Null car registration number"; @Override public String getRegistrationNumber() { return registrationNumber; } @Override public String getBrand() { return "Null car brand"; } @Override public String getColor() { return "Null car color"; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null) { return false; } if (!(o instanceof Car)) { return false; } Car that = (Car) o; if (registrationNumber != null) { if (!registrationNumber.equals(that.getRegistrationNumber())) return false; } else { if (that.getRegistrationNumber() != null) return false; } return true; } @Override public int hashCode() { return 17; } @Override public String toString() { return "CarNullImpl{" + "registrationNumber='" + registrationNumber + '\'' + '}'; } }
The null car implementation is not dangerous to use, it will never return null or act badly in any such sense. It is just meaningless and easy to spot.
Searching for all cars with a specific property, say a specific color, may be implemented as below.
First two tests, happy and unhappy path:
@Test public void findAllBlackCars() { carDao.create(expectedCar); String color = expectedCar.getColor(); int firstCar = 0; List<Car> actualCars = carDao.findAllCarsByColor(color); assertThat(actualCars.get(firstCar), is(expectedCar)); }
and
@Test public void findAllRedCars() { carDao.create(expectedCar); String color = "Red"; int noCarsFound = 0; List<Car> actualCars = carDao.findAllCarsByColor(color); assertThat(actualCars.size(), is(noCarsFound)); }
The production code:
public List<Car> findAllCarsByColor(String color) { List<Car> carsWithCorrectColor = new LinkedList<Car>(); for (Car car : cars.values()) { if (car.getColor().equals(color)) { carsWithCorrectColor.add(car); } } return carsWithCorrectColor; }
This implementation will iterate through all cars in the data store and select only those who has the correct property.
The thing to notice here is that I don't use the null implementation when no car is found, I don't do anything. When no cars are found, all I do is return the collection that is empty. An empty collection is the equivalent to a null object, it is not dangerous to use and in a sense fairly meaningless.
Deleting a car is just a matter of removing if from the data store.
First a test:
@Test public void deleteCar() { Car car = carDao.create(expectedCar); String registrationNumber = car.getRegistrationNumber(); Car locatedCar = carDao.find(registrationNumber); assertThat(locatedCar, is(expectedCar)); carDao.delete(expectedCar); Car noCarFound = new CarNullImpl(); Car actualCar = carDao.find(registrationNumber); assertThat(actualCar, is(noCarFound)); }
The test needs to create a car, verify it's existence, delete it and finally verify that it doesn't exist anymore. A fairly complex test scenario for a simple operation.
The production code:
public void delete(Car car) { cars.remove(car.getRegistrationNumber()); }
Delete is a quiet operation, if the car is in the data store it is deleted. If it isn't, then it isn't and the delete operation doesn't change anything. It could be argued that you should tell the caller how many records that were deleted but that may be overkill in my opinion. The car is gone after the operation or it didn't event exist before the operation, no big difference to the world.
The final operation to test and implement is update. Update can be expressed as a two step operation, first delete the original value and then store the new value.
The test code could be implemented as below:
@Test public void updateCar() { Car car = carDao.create(expectedCar); String newColor = "White"; Car updatedCar = new CarImpl(car.getRegistrationNumber(), car.getBrand(), newColor); Car actualCar = carDao.update(updatedCar); assertThat(actualCar, is(updatedCar)); Car locatedCar = carDao.find(expectedCar.getRegistrationNumber()); assertThat(locatedCar, is(updatedCar)); assertThat(locatedCar.getColor(), is(newColor)); }
The verification consists of creating a car, changing it's color, verify that the updated car was returned and verify that the updated color was set.
The production code:
public Car update(Car car) { delete(car); return create(car); }
The implementation consists of deleting a car and create it again.
Implementing an in memory database backend is not a lot of work. It is fast to implement. It is fast to execute all tests during the development. It is easy to change when the business demands changes. It is less error prone.
Just for completeness, all files needed to build this example are included below.
The Maven pom:
<?xml version="1.0" encoding="UTF-8"?> <project> <modelVersion>4.0.0</modelVersion> <groupId>se.sigma.educational</groupId> <artifactId>in-memory-example</artifactId> <version>1.0</version> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.8.2</version> <scope>test</scope> </dependency> </dependencies> </project>
The complete test suite:
package se.sigma.educational.dao; import org.junit.Before; import org.junit.Test; import se.sigma.educational.dto.Car; import se.sigma.educational.dto.CarImpl; import se.sigma.educational.dto.CarNullImpl; import java.util.List; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; public class CarDaoTest { private CarDao carDao; private Car expectedCar; @Before public void setUp() { carDao = new CarDaoImpl(); String registrationNumber = "XOE546"; String brand = "Chrysler"; String color = "Black"; expectedCar = new CarImpl(registrationNumber, brand, color); } @Test public void createCar() { Car actualCar = carDao.create(expectedCar); assertThat(actualCar, is(expectedCar)); } @Test public void findCar() { carDao.create(expectedCar); String registrationNumber = expectedCar.getRegistrationNumber(); Car actualCar = carDao.find(registrationNumber); assertThat(actualCar, is(expectedCar)); } @Test public void findCarIncorrectRegistrationNumber() { Car expectedCar = new CarNullImpl(); String registrationNumber = "foo"; Car actualCar = carDao.find(registrationNumber); assertThat(actualCar, is(expectedCar)); } @Test public void findAllBlackCars() { carDao.create(expectedCar); String color = expectedCar.getColor(); int firstCar = 0; List<Car> actualCars = carDao.findAllCarsByColor(color); assertThat(actualCars.get(firstCar), is(expectedCar)); } @Test public void findAllRedCars() { carDao.create(expectedCar); String color = "Red"; int noCarsFound = 0; List<Car> actualCars = carDao.findAllCarsByColor(color); assertThat(actualCars.size(), is(noCarsFound)); } @Test public void deleteCar() { Car car = carDao.create(expectedCar); String registrationNumber = car.getRegistrationNumber(); Car locatedCar = carDao.find(registrationNumber); assertThat(locatedCar, is(expectedCar)); carDao.delete(expectedCar); Car noCarFound = new CarNullImpl(); Car actualCar = carDao.find(registrationNumber); assertThat(actualCar, is(noCarFound)); } @Test public void updateCar() { Car car = carDao.create(expectedCar); String newColor = "White"; Car updatedCar = new CarImpl(car.getRegistrationNumber(), car.getBrand(), newColor); Car actualCar = carDao.update(updatedCar); assertThat(actualCar, is(updatedCar)); Car locatedCar = carDao.find(expectedCar.getRegistrationNumber()); assertThat(locatedCar, is(updatedCar)); assertThat(locatedCar.getColor(), is(newColor)); } }
The car interface:
package se.sigma.educational.dto; public interface Car { String getRegistrationNumber(); String getBrand(); String getColor(); }
The car data transfer object implementation:
package se.sigma.educational.dto; public class CarImpl implements Car { private String registrationNumber; private String brand; private String color; public CarImpl(String registrationNumber, String brand, String color) { this.registrationNumber = registrationNumber; this.brand = brand; this.color = color; } @Override public String getRegistrationNumber() { return registrationNumber; } @Override public String getBrand() { return brand; } @Override public String getColor() { return color; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null) { return false; } if (!(o instanceof Car)) { return false; } Car that = (Car) o; if (registrationNumber != null) { if (!registrationNumber.equals(that.getRegistrationNumber())) return false; } else { if (that.getRegistrationNumber() != null) return false; } return true; } @Override public int hashCode() { if (registrationNumber != null) return registrationNumber.hashCode(); else return 0; } @Override public String toString() { return "Car{" + "registrationNumber='" + registrationNumber + '\'' + ", brand='" + brand + '\'' + ", color='" + color + '\'' + '}'; } }
The null car implementation:
package se.sigma.educational.dto; public class CarNullImpl implements Car { private String registrationNumber = "Null car registration number"; @Override public String getRegistrationNumber() { return registrationNumber; } @Override public String getBrand() { return "Null car brand"; } @Override public String getColor() { return "Null car color"; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null) { return false; } if (!(o instanceof Car)) { return false; } Car that = (Car) o; if (registrationNumber != null) { if (!registrationNumber.equals(that.getRegistrationNumber())) return false; } else { if (that.getRegistrationNumber() != null) return false; } return true; } @Override public int hashCode() { return 17; } @Override public String toString() { return "CarNullImpl{" + "registrationNumber='" + registrationNumber + '\'' + '}'; } }
The car data access object implementation:
package se.sigma.educational.dao; import se.sigma.educational.dto.Car; import java.util.List; public interface CarDao { Car create(Car car); Car find(String registrationNumber); List<Car> findAllCarsByColor(String color); void delete(Car car); Car update(Car car); }
Finally the main implementation class, the car data access object implementation:
package se.sigma.educational.dao; import se.sigma.educational.dto.Car; import se.sigma.educational.dto.CarNullImpl; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; public class CarDaoImpl implements CarDao { private Map<String, Car> cars = new HashMap<String, Car>(); @Override public Car create(Car car) { String registrationNumber = car.getRegistrationNumber(); cars.put(registrationNumber, car); return car; } @Override public Car find(String registrationNumber) { Car result = cars.get(registrationNumber); if (result == null) { return new CarNullImpl(); } return result; } @Override public List<Car> findAllCarsByColor(String color) { List<Car> carsWithCorrectColor = new LinkedList<Car>(); for (Car car : cars.values()) { if (car.getColor().equals(color)) { carsWithCorrectColor.add(car); } } return carsWithCorrectColor; } @Override public void delete(Car car) { cars.remove(car.getRegistrationNumber()); } @Override public Car update(Car car) { delete(car); return create(car); } }
The second step is to replace the working, in memory implementation, with a database implementation.
All tests created in the first step are reused as support during this transition. The tests defines what the application expects should work and how it should work.
I will use the Java Persistence API, JPA, and support it with a Hypersonic, HSQL, database. A large advantage with
using Hypersonic that it is written entirely in Java and can be started without an installation. By asking an EntityManagerFactory
for a data store with the same name as a persistence unit defined in persistence.xml
in the class path
we will get access to a running Hypersonic database. This sounds easy and it is.
Lets go through all the changes needed for the transition.
The first change I will do is to turn CarImpl
into an entity recognizable for JPA. This is done with
one annotation that will mark this class as an entity, @Entity
. The entity class also need to have a
primary key. I will mark the registration number field with @Id
to indicate that this field is to be
used as primary key.
This are the two changes that has to be done to mark CarImpl
as being an entity. The annotated class
will now look like this:
package se.sigma.educational.dto; import javax.persistence.Entity; import javax.persistence.Id; @Entity(name = "cars") public class CarImpl implements Car { private String registrationNumber; private String brand; private String color; public CarImpl() { } public CarImpl(String registrationNumber, String brand, String color) { this.registrationNumber = registrationNumber; this.brand = brand; this.color = color; } public void setRegistrationNumber(String registrationNumber) { this.registrationNumber = registrationNumber; } @Id @Override public String getRegistrationNumber() { return registrationNumber; } public void setBrand(String brand) { this.brand = brand; } @Override public String getBrand() { return brand; } public void setColor(String color) { this.color = color; } @Override public String getColor() { return color; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null) { return false; } if (!(o instanceof Car)) { return false; } Car that = (Car) o; if (registrationNumber != null) { if (!registrationNumber.equals(that.getRegistrationNumber())) return false; } else { if (that.getRegistrationNumber() != null) return false; } return true; } @Override public int hashCode() { if (registrationNumber != null) return registrationNumber.hashCode(); else return 0; } @Override public String toString() { return "Car{" + "registrationNumber='" + registrationNumber + '\'' + ", brand='" + brand + '\'' + ", color='" + color + '\'' + '}'; } }
I want the cars to be stored in a table called cars. This is done by setting the name of the entity in the
annotation. @Entity(name = "cars")
. This is not really necessary, but may be useful when the
application is deployed on another, more persistent database.
This implementation is still just being a test and only in memory.
Next step is to change the setup of the dao in the test.
@Before public void setUp() { EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("dao-integration-test"); EntityManager entityManager = entityManagerFactory.createEntityManager(); carDao = new CarDaoImpl(); ((CarDaoImpl) carDao).setEntityManager(entityManager); String registrationNumber = "XOE546"; String brand = "Chrysler"; String color = "Black"; expectedCar = new CarImpl(registrationNumber, brand, color); }
The new setup method creates the CarDao
and injects a EntityManager
into it. This is the
only difference in the test class. Everything else is unchanged.
The real changes are done in the production code. It will now use the EntityManager
and connect to a
database.
The first operation that will be updated to use the database saving a new car. It can be implemented as below:
@Override public Car create(Car car) { EntityTransaction entityTransaction = beginTransaction(); entityManager.persist(car); commitTransaction(entityTransaction); return car; }
The job is done in the statement entityManager.persist(car);
. It will save the car to the database.
Unfortunately, an entity that is persisted that way will not be accessible for other operations before the save has
been committed. Therefore I need to run the operation in a transaction. All updating operations need to be executed
in a transaction so I have created two helper methods that will handle the boiler plate code that starting and
ending transactions are.
Except from the transactions needed, saving an entity is really easy.
Locating stored cars may be done as below:
public Car find(String registrationNumber) { Car result; result = entityManager.find(CarImpl.class, registrationNumber); if (result == null) { result = new CarNullImpl(); } return result; }
When we have an entity that has been stored, it is just a matter of entityManager.find()
with the
entity class and the primary key to retrieve it. If no entity is found, entityManager.find()
will
return null. I check for null and return a null car instead of null to avoid a future
NullPointerException
.
Locating an entity based on a property is a little more complicated. You need to write some kind of query and execute it. An example may be:
public List<Car> findAllCarsByColor(String color) { String ql = "from cars " + "where color = :color"; Query query = entityManager.createQuery(ql); query.setParameter("color", color); return addResultSet(query); }
The result from this query is untyped objects. To avoid warnings during compile time and use Javas type checking mechanism, I add the result to a typed list and return that typed list.
private List<Car> addResultSet(Query query) { List<Car> result = new LinkedList<Car>(); for (Object o : query.getResultList()) { result.add((Car) o); } return result; }
Deleting a stored car is done as below:
public void delete(Car car) { EntityTransaction entityTransaction = beginTransaction(); entityManager.remove(car); commitTransaction(entityTransaction); }
Delete is updating so you have to execute it within a transaction. It is also a quiet operation so nothing is returned.
Update a stored car could be implemented in terms of delete and create, but it is done using merge.
public Car update(Car car) { EntityTransaction entityTransaction = beginTransaction(); entityManager.merge(car); commitTransaction(entityTransaction); return car; }
The updated car is returned to the caller if they need it.
Using the tests developed in the first step, building a in memory solution, will force you to create a database solution that satisfies the requirements from the application. Adding database support may be done at a later stage. And as always, working in small steps will ensure that you know what you are doing and enable you to build the right thing and build it right.
Just for completeness, all files needed to build this example are included below.
The Maven pom:
<?xml version="1.0" encoding="UTF-8"?> <project> <modelVersion>4.0.0</modelVersion> <groupId>se.sigma.educational</groupId> <artifactId>database-example</artifactId> <version>1.0</version> <packaging>jar</packaging> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <dependencies> <dependency> <groupId>javax.persistence</groupId> <artifactId>persistence-api</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.8.2</version> <scope>test</scope> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>3.4.0.GA</version> <scope>test</scope> </dependency> <dependency> <groupId>hsqldb</groupId> <artifactId>hsqldb</artifactId> <version>1.8.0.7</version> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.4.2</version> <scope>test</scope> </dependency> </dependencies> </project>
The complete test suite:
package se.sigma.educational.dao; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import se.sigma.educational.dto.Car; import se.sigma.educational.dto.CarImpl; import se.sigma.educational.dto.CarNullImpl; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.Persistence; import java.util.List; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; public class CarDaoTest { private CarDao carDao; private Car expectedCar; @Before public void setUp() { EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("dao-integration-test"); EntityManager entityManager = entityManagerFactory.createEntityManager(); carDao = new CarDaoImpl(); ((CarDaoImpl) carDao).setEntityManager(entityManager); String registrationNumber = "XOE546"; String brand = "Chrysler"; String color = "Black"; expectedCar = new CarImpl(registrationNumber, brand, color); } @Test public void createCar() { Car actualCar = carDao.create(expectedCar); assertThat(actualCar, is(expectedCar)); } @Test public void findCar() { carDao.create(expectedCar); String registrationNumber = expectedCar.getRegistrationNumber(); Car actualCar = carDao.find(registrationNumber); assertThat(actualCar, is(expectedCar)); } @Test public void findCarIncorrectRegistrationNumber() { Car expectedCar = new CarNullImpl(); String registrationNumber = "foo"; Car actualCar = carDao.find(registrationNumber); assertThat(actualCar, is(expectedCar)); } @Test public void findAllBlackCars() { carDao.create(expectedCar); String color = expectedCar.getColor(); int firstCar = 0; List<Car> actualCars = carDao.findAllCarsByColor(color); assertThat(actualCars.get(firstCar), is(expectedCar)); } @Test public void findAllRedCars() { carDao.create(expectedCar); String color = "Red"; int noCarsFound = 0; List<Car> actualCars = carDao.findAllCarsByColor(color); assertThat(actualCars.size(), is(noCarsFound)); } @Test public void deleteCar() { Car car = carDao.create(expectedCar); String registrationNumber = car.getRegistrationNumber(); Car locatedCar = carDao.find(registrationNumber); assertThat(locatedCar, is(expectedCar)); carDao.delete(expectedCar); Car noCarFound = new CarNullImpl(); Car actualCar = carDao.find(registrationNumber); assertThat(actualCar, is(noCarFound)); } @Test public void updateCar() { Car car = carDao.create(expectedCar); String newColor = "White"; Car updatedCar = new CarImpl(car.getRegistrationNumber(), car.getBrand(), newColor); Car actualCar = carDao.update(updatedCar); assertThat(actualCar, is(updatedCar)); Car locatedCar = carDao.find(expectedCar.getRegistrationNumber()); assertThat(locatedCar, is(updatedCar)); assertThat(locatedCar.getColor(), is(newColor)); } }
The glue between tha Hypersonic database and the tests, persistence.xml:
<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0"> <persistence-unit name="dao-integration-test" transaction-type="RESOURCE_LOCAL"> <class>se.sigma.educational.dto.CarImpl</class> <properties> <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect"/> <property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver"/> <property name="hibernate.connection.username" value="sa"/> <property name="hibernate.connection.password" value=""/> <property name="hibernate.connection.url" value="jdbc:hsqldb:mem:sabine-unit-test"/> <property name="hibernate.hbm2ddl.auto" value="create-drop"/> </properties> </persistence-unit> </persistence>
It should be located in ./src/test/resources/META-INF/persistence.xml
. The important glue here is the
string dao-integration-test
It is the locator used when creating the EntityManager that will tie the
Hypersonic database to the test.
The car interface:
package se.sigma.educational.dto; public interface Car { String getRegistrationNumber(); String getBrand(); String getColor(); }
The car data transfer object implementation:
package se.sigma.educational.dto; import javax.persistence.Entity; import javax.persistence.Id; @Entity(name = "cars") public class CarImpl implements Car { private String registrationNumber; private String brand; private String color; public CarImpl() { } public CarImpl(String registrationNumber, String brand, String color) { this.registrationNumber = registrationNumber; this.brand = brand; this.color = color; } public void setRegistrationNumber(String registrationNumber) { this.registrationNumber = registrationNumber; } @Id @Override public String getRegistrationNumber() { return registrationNumber; } public void setBrand(String brand) { this.brand = brand; } @Override public String getBrand() { return brand; } public void setColor(String color) { this.color = color; } @Override public String getColor() { return color; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null) { return false; } if (!(o instanceof Car)) { return false; } Car that = (Car) o; if (registrationNumber != null) { if (!registrationNumber.equals(that.getRegistrationNumber())) return false; } else { if (that.getRegistrationNumber() != null) return false; } return true; } @Override public int hashCode() { if (registrationNumber != null) return registrationNumber.hashCode(); else return 0; } @Override public String toString() { return "Car{" + "registrationNumber='" + registrationNumber + '\'' + ", brand='" + brand + '\'' + ", color='" + color + '\'' + '}'; } }
The null car implementation:
package se.sigma.educational.dto; public class CarNullImpl implements Car { private String registrationNumber = "Null car registration number"; @Override public String getRegistrationNumber() { return registrationNumber; } @Override public String getBrand() { return "Null car brand"; } @Override public String getColor() { return "Null car color"; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null) { return false; } if (!(o instanceof Car)) { return false; } Car that = (Car) o; if (registrationNumber != null) { if (!registrationNumber.equals(that.getRegistrationNumber())) return false; } else { if (that.getRegistrationNumber() != null) return false; } return true; } @Override public int hashCode() { return 17; } @Override public String toString() { return "CarNullImpl{" + "registrationNumber='" + registrationNumber + '\'' + '}'; } }
The car data access object implementation:
package se.sigma.educational.dao; import se.sigma.educational.dto.Car; import java.util.List; public interface CarDao { Car create(Car car); Car find(String registrationNumber); List<Car> findAllCarsByColor(String color); void delete(Car car); Car update(Car car); }
Finally the main implementation class, the car data access object implementation:
package se.sigma.educational.dao; import se.sigma.educational.dto.Car; import se.sigma.educational.dto.CarImpl; import se.sigma.educational.dto.CarNullImpl; import javax.persistence.EntityManager; import javax.persistence.EntityTransaction; import javax.persistence.Query; import javax.persistence.RollbackException; import java.util.LinkedList; import java.util.List; public class CarDaoImpl implements CarDao { private EntityManager entityManager; @Override public Car create(Car car) { EntityTransaction entityTransaction = beginTransaction(); entityManager.persist(car); commitTransaction(entityTransaction); return car; } @Override public Car update(Car car) { EntityTransaction entityTransaction = beginTransaction(); entityManager.merge(car); commitTransaction(entityTransaction); return car; } @Override public Car find(String registrationNumber) { Car result; result = entityManager.find(CarImpl.class, registrationNumber); if (result == null) { result = new CarNullImpl(); } return result; } @Override public List<Car> findAllCarsByColor(String color) { String ql = "from cars " + "where color = :color"; Query query = entityManager.createQuery(ql); query.setParameter("color", color); return addResultSet(query); } private List<Car> addResultSet(Query query) { List<Car> result = new LinkedList<Car>(); for (Object o : query.getResultList()) { result.add((Car) o); } return result; } @Override public void delete(Car car) { EntityTransaction entityTransaction = beginTransaction(); entityManager.remove(car); commitTransaction(entityTransaction); } public void setEntityManager(EntityManager entityManager) { this.entityManager = entityManager; } private EntityTransaction beginTransaction() { EntityTransaction entityTransaction = entityManager.getTransaction(); entityTransaction.begin(); return entityTransaction; } private void commitTransaction(EntityTransaction entityTransaction) { try { entityTransaction.commit(); } catch (RollbackException e) { e.printStackTrace(); entityTransaction.rollback(); } } }
The difference between the two test classes is in the @Before
method. This difference could be factored
out and accessed either by subclasses or composition. If the subclass approach is used, make sure that the test
inherits a common class that creates the data access object. If composition is used, create a utility class that
creates the dao.
I would prefer the composition approach. It would allow me to make the transition one dao at the time. I could update the test classes, ignore all tests and then implement the dao step by step and enable the test test by test. This transition would be done in many small steps, where every step is simple and easy to understand and verify.
My personal opinion is that many small steps better than of few large steps. Experience has showed me that it is easier for me to reach a goal step by step instead few large steps. It is easier to stumble when you take large steps.
Using almost any small step approach would be an instance of Martin Fowlers Strangler pattern, also known as Strangler Application.
Michael Brodie and Michael Stonebraker is using the term cold turkey or Chicken little in 'DARWIN: On the Incremental Migration of Legacy Information Systems' to describe a transition strategy done in many small steps instead of one large big bang.
There are many examples but they all go back to the same basic idea, minimize the transition risk and maximize feed back. In other words, always shorten the feed back loop as much as possible.