Thursday, June 9, 2011

Unit Testing Hibernate Data Access Objects using JUnit 4 – Part II

In part I we setup the infrastructure or the framework for unit testing. In this part we will write out domain/entity class, dao interface, dao implementation test and then dao implementation. When we write our test we know it will fail because no such method will exist in the dao implementation. However, we will need to create the implementation class, albeit without any methods. So, let's get straight to it.

@Entity
public class Item {

@Id@GeneratedValue(strategy=GenerationType.AUTO)
private Long id;

@ManyToOne
private Order order;
private String product;
private double price;
private int quantity;
/**
* @return the id
*/
public Long getId() {
return id;
}

/**
* @return the order
*/
public Order getOrder() {
return order;
}
// --- getters and setters follow.
//--- override the ToString()and HashCode()

public class Order{

//Other instance variables ....
@OneToMany(cascade=CascadeType.ALL)
@JoinColumn(name="ORDER_ID")
private Collection items = new LinkedHashSet();

/**
* @return the items
*/
public Collection getItems() {
return items;
}
/**
* @param items the items to set
*/
public void setItems(Collection items) {
this.items = items;
}
}


Few things to note here if you are using hibernate are ITEM has many to one relationship with ORDER- an order has many items whereas an item belongs to an order. In the database you will have ORDER_ID column in the ITEM table. Cascade.ALL means whenever ORDER is deleted, corresponding ITEM is set to null. We will not update the id of ORDER because it is auto-generated. So update to an ORDER does not have any bearing on ITEM. The ids are auto-generated.

Now that we have our domain objects, we will write ItemDao. You will similarly write OrderDao, but  I will leave that to you.

public interface ItemDao {

/**
* Given an item id
* return the Item object.
* @param id of the
* @return Matching Item object.
*/
public Item findById(Long itemId);

/**
* @return All items
*/
public List<Item> findAllItems();
}


We have two simple methods to find an Item by item id and findAllItems. You would indeally expand on this and write methods to delete, update, findItemByOrderId and so on. For now, let's keep things simple.

Now we will implement this dao, except we will return null from the implemented methods.

public class ItemDaoImpl implements ItemDao {

public Item findById(Long itemId){
return null;
}

public List<Item> findAllItems() {
return null;
}
}


We can now write our unit test! If you've followed part I, we setup application-context to inject dao implementation. Now we will read the application-context in order to inject that. Also I mentioned that the test methods will be annotated with @Transactional. This is to ensure that when the method returns (void), the transactions within will be rolledback. This is to ensure that our test db will remain unchanged and we can test again and again with same test data. Of course, this also means that you will need to populate your test data. So let's do that first.

Run this query in MySQL and you will have 2 rows in the ITEM table.

INSERT INTO `test_hibernate`.`item` (
`ID` ,
`PRODUCT` ,
`PRICE` ,
`QUANTITY` ,
`ORDER_ID`

)
VALUES (
NULL , 'Sony Headphones', '99.99', '3', NULL

), (
NULL , 'Logitech XZ Mouse', '15.99', '2', NULL

);


For now we don't set ORDER_ID. Now that you have two rows, we can write our tests.

@ContextConfiguration(locations={"classpath:/applicationContext.xml"})
@RunWith(SpringJUnit4ClassRunner.class)
public class ItemDaoImplTest {

@Autowired
private ItemDaoImpl dao;

@Test
@Transactional
public void testFindById()
{
Item newItem =  dao.findById(1L);
assertTrue(newItem.getProduct().equals("Sony Headphones"));
}

@Test
@Transactional
public void testFindAllItems()
{
List<Item> itemList = dao.findAllItems();
assertTrue(String.valueOf(itemList.size()).equals("2"));
}

If you run this now, your tests will fail. This is because you have not implemented the methods correctly.

So now, we implement those methods from daoImpl.

public Item findById(Long itemId){
DetachedCriteria itemCriteria = DetachedCriteria.forClass(Item.class);
itemCriteria.add(Restrictions.eq(ID_FIELD, itemId));
List<Item> itemList = findByCriteria(itemCriteria);
if(null != itemList && itemList.size() > 0)
return itemList.get(0);
return null;
}

public List<Item> findAllItems() {
DetachedCriteria itemCriteria = DetachedCriteria.forClass(Item.class);
List<Item> itemList = findByCriteria(itemCriteria);
if(null != itemList && itemList.size() > 0)
return itemList;
return null;
}

As I mentioned in part I, I will use DetachedCriteria to query our database. One subtle advantage of doing this is constructing queries is easy (to read and write) and the other major advantage would be since we are going to call this from another module most likely, for example web application's controller via service method in this module, we want the session to be started only when the dao method is called and to be cleaned up as soon as the method returns.

Run the test again, and it will pass. This takes care of testing dao. In a near future I will show you how to write tests for service methods by mocking out the db. I will also point out the advantage of doing this.

4 comments:

  1. Hi, I wanted to know your opinion...

    In the first test case you have....
    assertTrue(newItem.getProduct().equals("Sony Headphones"));
    Would the test case be better if you did...
    assertTrue(newItem.getId().equals(new Long(1L));
    In this instance, you are querying for a row by ID, should we not check the ID field? In other words, check the property that we are accessing the item by?

    for findAllItems() should we be writing a test case that tests what happens when the list is 0? If that is the case, how would we clear the database, prior to our test?

    Thanks, Ed

    ReplyDelete
  2. Yes and yes. You would always want to write test cases that match every possible outcomes. We do not need to however clear the database as the state never changes after each test. And it should not. @Transactional Attribute is used exactly for that. But please make sure your db engine supports rollback feature. For example if you're using MySQL InnoDB supports rollback, MyISAM does not.

    ReplyDelete