Our application has grown in size and complexity since the last time we created our unit test for the shopping service class. This is what I would like to do:
- Integrate our test case with Spring – this is pretty cool, instead of creating and initializing our objects manually or performing any JNDI looking ups ourselves (that is if we did need to do that) we will let Spring take care of this for us using our existing Spring bean configuration files we created in the previous tutorial.
- Mock objects – In some cases it is very difficult to test a service. In our case it isn’t because all we are doing is creating an instance of the service ourselves but what if the instance of the service existed on a remote server and you needed to use a remote call to lookup that instance? It adds additional overhead to your test case. For one it relies on this remote service to be available at the time the test is run and two it requires additional resources in order to perform this lookup. An alternative to testing the service is to create a mock of the service and predict the outcome of the test. So you don’t actually test the service implementation but your very own mock that is based on the service interface. This sounds like a lot of work but it isn’t actually thanks to a third party library we are going to use called EasyMock.
- Database integration testing – We touch on an integration test for testing transactions with the database. We will be using spring as well as a third party library called DBUnit.
What you need before we get started
- ShoppingCartAdvancedTestCasesPart9 sample application. - This is an Eclipse archive file that contains one Eclipse project. Please import this project into your Eclipse environment.
- You will need the previous third party libraries that were mentioned and used in earlier parts to this tutorial
- The following libraries need to be added to your existing Libs project:
What we will cover
- Spring integration
- Mock object tests
- Database integration tests
Level
- Intermediary
Spring Integration
We are going to use Spring for two purposes, the first is for our Shopping service class. We will let Spring manage the creation and initialization of this object for us so. We will also make use of our existing spring bean configuration file to initialize our service class. Secondly we are going to use Spring to create test data for us so that we can use it in our test case. To do this we will create a Spring bean configuration file specifically for our tests. I have put it under our test folder and called it shoppingcart-test.xml.
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd"> <bean id="shoppingService" class="com.mydomain.shoppingcart.service.impl.ShoppingManager"> <property name="itemDao"> <ref bean="itemDao" /> </property> <property name="basketDao"> <ref bean="basketDao" /> </property> </bean> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="shoppingCartDataSource" /> </bean> <import resource="classpath:shoppingcart-database.xml" /> </beans>
There are two different beans defined here, namely:
- shoppingService – our shopping service class
- jdbcTemplate – spring class used to simplify the use of JDBC
Next we will make changes to our ShoppingServiceTest class.
package com.mydomain.shoppingcart.service.test; import static org.easymock.EasyMock.createMock; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.replay; import static org.easymock.EasyMock.verify; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import java.util.LinkedList; import java.util.List; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import com.mydomain.shoppingcart.bo.Basket; import com.mydomain.shoppingcart.bo.BasketItem; import com.mydomain.shoppingcart.bo.Item; import com.mydomain.shoppingcart.dao.BasketDao; import com.mydomain.shoppingcart.service.ShoppingService; /** * @author Ross */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = { "classpath:/shoppingcart-test.xml" }) public class ShoppingServiceTest { private Basket basket; private BasketDao basketDaoMock; @Autowired private ShoppingService shoppingManager; private ShoppingService shoppingManagerMock; private Item testItem; /** * Tests adding an item to the basket. */ @Test @DirtiesContext public void addItem() { int itemCount = basket.getItemCount(); basket.addItem(testItem); assertEquals(itemCount + 1, basket.getItemCount()); } /** * Tests emptying the basket. */ @Test @DirtiesContext public void empty() { basket.empty(); assertEquals(0, basket.getItemCount()); } /** * Tests finding items. */ @Test public void findItems() { try { int itemCount = basket.getItemCount(); List<Item> mockResult = new LinkedList<Item>(); for (int i = 0; i < itemCount; i++) { mockResult.add(new Item()); } expect(shoppingManagerMock.findItems()).andReturn(mockResult); replay(shoppingManagerMock); List<Item> allItems = new LinkedList<Item>(shoppingManagerMock.findItems()); assertEquals(itemCount, allItems.size()); verify(shoppingManagerMock); } catch (Exception e) { e.printStackTrace(); fail("Error in Shopping Manager"); } } /** * Tests removing an item from the cart. */ @Test @DirtiesContext public void removeItem() { int itemCount = basket.getItemCount(); for (BasketItem basketItem : basket.getBasketItems()) { basket.removeItem(basketItem.getItem()); break; } assertEquals(itemCount - 1, basket.getItemCount()); } /** * Tests saving a basket. */ @Test public void saveBasket() { try { shoppingManager.setBasketDao(basketDaoMock); basketDaoMock.saveOrUpdateBasket(basket); replay(basketDaoMock); shoppingManager.updateBasket(basket); verify(basketDaoMock); } catch (Exception e) { e.printStackTrace(); fail("Error in Shopping Manager"); } } /** * Sets up the test fixture. * * Called before every test case method. */ @Before public void setUp() { try { shoppingManagerMock = createMock(ShoppingService.class); basketDaoMock = createMock(BasketDao.class); testItem = new Item(1l, "Candy Cotton", "Candy coated milky tarts", 8.50d); basket = new Basket(); basket.addItem(new Item(2l, "Jelly Beans", "Jelly icecream waffle cream", 18.99d)); basket.addItem(new Item(3l, "Jam Doughnut", "Strawberry jam and Christmas pudding", 23.00d)); } catch (Exception e) { e.printStackTrace(); fail("Error setting up test case"); } } }
Ok there are a couple of things I would like to point out here and step through them each:
- @RunWith attribute is a JUnit annotation and is used when you want to override the default JUnit runner. Here we want to use Springs own implementation in order to use Spring in our tests.
- @ContextConfiguration is a Spring annotation and sets the location of the Spring bean configuration files needed for this test.
- @Autowired is a Spring annotation that tells the compiler to automatically "wire" the variable (in this case shoppingManager) to the matching bean configured in the Spring bean configuration file.
- Lastly we have our test method. Nothing really fancy about the body of the method. You notice we didn't have to create any objects or initialize anything as Spring did that for us. There is one more additional Spring annotation, @DirtiesContext. This annotation ensures that this method gets a clean context before it executes. Another test method may have altered the state of the beans defined in the Spring configuration files this annotation ensures the state of those beans will be the same as its original state before it executes.
Mock object test
Mock objects are simulated objects that mimic the behavior of real objects in controlled ways. A mock object is created to test the behavior of some other object. Mock objects can simulate the behavior of complex, real (non-mock) objects and are therefore useful when a real object is impractical or impossible to incorporate into a unit test. If an object has any of the following characteristics, it may be useful to use a mock object in its place:
- Supplies non-deterministic results (e.g. the current time or the current temperature)
- Has states that are difficult to create or reproduce (e.g. a network error)
- Is slow (e.g. a complete database, which would have to be initialized before the test)
- Does not yet exist or may change behavior
- Would have to include information and methods exclusively for testing purposes (and not for its actual task)
- Mock objects have the same interface as the real objects they mimic, allowing a client object to remain unaware of whether it is using a real object or a mock object.
EasyMock
EasyMock is a third party library that provides Mock Objects for interfaces in JUnit tests by generating them on the fly.
Shopping Cart Application
In the shopping cart application I created two different mock objects to show you how and where you can use them:
- ShoppingManagerMock – A mock for the shopping service interface. You would most likely want to create a mock object for a service that you are looking up remotely and have no control over it. In the shopping cart application this is not the case but this example demonstrates what you can do.
- BasketDaoMock – A mock for the basket dao. You may not want to test your service class but not the actual persistence of data to the database. Simply because there is a performance overhead setting up connections and persisting data to the database as well as managing the test data that you save and delete from the database. You don't want to corrupt your database with your tests. The last part of this tutorial shows you an example of testing your DAO's.
In our setup method we create the mock objects using EasyMocks createMock static method. This call creates a mock object that implements the given interface.
The first thing we do is define what method calls we expect should be made on our mock object. We can also define what result we expect back from a method call:
expect(shoppingManagerMock.findItems()).andReturn(mockResult);
Once we have set up all our expectations (in our case only one) we change the mode of our mock test to replay. What this means is that every other method call made on our mock object from this point on will be checked to see if it is what we expect to be called.
replay(shoppingManagerMock); List<Item> allItems = new LinkedList<Item>(shoppingManagerMock.findItems()); assertEquals(itemCount, allItems.size());
Finally at the end of our test we call the EasyMock verify method that verifies the expected behaviour and throws an exception if it does not match.
verify(shoppingManagerMock);
I created a new test method to test the BasketDao mock object. As you can see it is very similar to the above test. You will notice that I am not using the shopping manager mock object.
@Test public void saveBasket() { try { shoppingManager.setBasketDao(basketDaoMock); basketDaoMock.saveOrUpdateBasket(basket); replay(basketDaoMock); shoppingManager.updateBasket(basket); verify(basketDaoMock); } catch (Exception e) { e.printStackTrace(); fail("Error in Shopping Manager"); } }
Database Integration Tests
I would call this a type of integration test because we are testing the integration of our application with the database. I created a new test called BasketDaoTest to test the BasketDao data access class. These are the following steps I took when putting together this test:
- Create test data that we will use to insert into our database and then remove at the end of the test
- Create new test class to test the BasketDao class
1. Test data
We are going to create test data to run our tests against. You don’t want to run your test against data already in the database. For one you don’t know what data is there and two you don’t want to mess with someone else’s data. It is therefore essential to create our own data.
I created an xml file (shoppingcart-dbunit.xml) for my test data that will be used by DBUnit to insert into the database and at the end of the test remove it from the database. An example of the xml I used looks like the following:
<?xml version='1.0' encoding='UTF-8'?> <dataset> <basket ID="100001" /> <item ID="100001" DESCRIPTION="Candy coated milky tarts" NAME="Candy Cotton" PRICE="8.5"/> <item ID="100002" DESCRIPTION="Jelly icecream waffle cream" NAME="Jelly Beans" PRICE="18.99"/> <basket_item ID="100001" BASKET_ID="100001" ITEMS_ID="100001" QUANTITY="2" PRICE="17"/> <basket_item ID="100002" BASKET_ID="100001" ITEMS_ID="100002" QUANTITY="1" PRICE="18.99"/> </dataset>
DbUnit is a JUnit extension targeted at database-driven projects that puts your database into a known state between test runs. The easiest way to create this test data is to export the data from the database. There are specific DBUnit classes you can use to export data in the format required either by writing a java application that does it or by using an ant task made available by DBUnit to do this.
2. Test class
Now that we the data that our test is going to use we can move right along to our test class.
Once again we will leverage off Spring to make our lives a whole lot easier. If you take a look at our class declaration you will notice a couple of interesting points.@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = { "classpath:/shoppingcart-test.xml" }) @TransactionConfiguration @Transactional public class BasketDaoTest extends AbstractTransactionalJUnit4SpringContextTests {
This class is an abstract transactional extension of AbstractJUnit4SpringContextTests () which adds convenience functionality for JDBC access. It expects a DataSource bean and a PlatformTransactionManager bean to be defined in the Spring application context.
This class exposes a SimpleJdbcTemplate and provides an easy way to count the number of rows in a table, delete from the database , and execute SQL scripts within a transaction.
The @TransactionConfiguration and @Transactional annotations configure transactions for the tests.
I have broken our test class up into the following steps:
- Record current state of data in the database
- Insert test data into the database
- Run test on data in database
- Delete test data from database
- Validate data stored in the database to match original state
1. Record current state of data in the database
The first step I decided to do is to determine how many rows there are for the basket table. I did this so that at the end of the test I can compare the number of rows in the basket table to this value and check to see whether or not they are equal.
@BeforeTransaction public void beforeTransaction() { basketTableRowCount = countRowsInTable(BASKET_TABLE_NAME); }
The method with the annotation @BeforeTransaction is run before the transaction starts.
2. Insert test data into the database
I take the test data we created and I use DBUnit classes to insert the data into the database.
@Before public void setUpTestDataWithinTransaction() { try { IDatabaseConnection connection = new DatabaseConnection(jdbcTemplate.getDataSource().getConnection()); DatabaseOperation.INSERT.execute(connection, new FlatXmlDataSet(new FileInputStream(TEST_DATA_FILE))); jdbcTemplate.getDataSource().getConnection().close(); } catch (Exception e) { e.printStackTrace(); } }
As we saw in the earlier tutorial on test cases the method with a @Before annotation gets called before any test methods.
3. Run test on data in database
All our JUnit test methods can now be run
@Test @Rollback(true) public void saveOrUpdateBasket() { assertNotNull("Basket DAO is null.", basketDao); try { Collection<Basket> baskets = basketDao.loadById(1); assertNotNull("Basket list is null.", baskets); assertEquals("Number of baskets should be " + basketTableRowCount + 1 + ".", basketTableRowCount + 1, baskets.size()); for (Basket basket : baskets) { assertNotNull("Basket is null.", basket); } } catch (Exception e) { fail("Error in saveOrUpdateBasket"); } }
The @Test annotation identifies this method as our test method to run. The @Rollback annotation is a Spring test annotation used to indicate whether or not the transaction for the annotated test method should be rolled back after the test method has completed. If true, the transaction will be rolled back; otherwise, the transaction will be committed.
4. Delete test data from database
At the end of all our tests we use DBUnit again to delete our test data from the database so that the state of the database returns to what it was before the start of our test case.
@After public void tearDownWithinTransaction() { try { IDatabaseConnection connection = new DatabaseConnection(jdbcTemplate.getDataSource().getConnection()); DatabaseOperation.DELETE.execute(connection, new FlatXmlDataSet(new FileInputStream(TEST_DATA_FILE))); jdbcTemplate.getDataSource().getConnection().close(); } catch (Exception e) { fail("Error in tearDownWithinTransaction"); } }
As we saw in the earlier tutorial on test cases the method with a @After annotation gets called after all the test methods have been called.
5. Validate data stored in the database to match original state
Here we validate the number of rows in our basket table to the number we originally retrieved at the start of our test.
@AfterTransaction public void afterTransaction() { assertEquals(basketTableRowCount, countRowsInTable(BASKET_TABLE_NAME)); }
The method with the annotation @AfterTransaction is run after the transaction ends.
4 comments:
shopping-cart-core-advanced-test-cases-part9
where is this class
AbstractTransactionalJUnit4SpringContextTests which is extended by BasketDaoTest i tryed to find it but without any luck
It is part of the org.springframework.test 3.0.0.M1 library.
I included this library in an earlier tutorial on Spring
You rock man ;) I knew all the shown, but I have never seen a whole example on a real world project structure. This one has it all: Spring + Hibernate + JUnit + EasyMock integration tests + DAO + DBUnit .
thanks, I am glad you like it
Post a Comment