Tuesday, March 16, 2010

Effective Unit Testing with TestNG

Writing good unit tests is essential to maintain product quality. JUnit is a popular testing framework for Java. Starting with JUnit 4, it has a radical change on the framework in support of annotation based configuration. With annotations, it's no longer required to extend TestCase for all your test cases. However, the lack of good supporting dependency testing makes TestNG, another test automation framework, a formidable alternative to JUnit.

TestNG natively supports dependency testing with dependsOnMethods and dependsOnGroups. It has a concept of group that helps organize test cases on the level of category. Another important feature is parametric testing by using Data Provider. A variety of data values can be injected into same test method to verify different scenarios.

Ideally, every project should have as complete test cases covered as possible so that future enhancements could be assured not breaking existing codes. But, let's face the real world. Developers are driven by tight deadlines. Sluggish testing process would make them reluctant to run the tests.

With the help of in-memory database, you could run your unit testing comfortably fast. Here is an example of how Hibernate defines in-memory database using H2, a SQL database written in Java and proven very fast.

test-hibernate.cfg.xml
<!-- Basic -->
<property name="hibernate.connection.driver_class">org.h2.Driver</property>
<property name="hibernate.dialect">org.hibernate.dialect.HSQLDialect</property>
<property name="hibernate.connection.url">jdbc:h2:mem:test</property>
<property name="hibernate.connection.username">username</property>
<property name="hibernate.connection.password">password</property>
<property name="hibernate.connection.default_schema">test</property>
<property name="hbm2ddl.auto">create</property>

<!-- Mappings -->
<mapping class="org.sheng.db.User"/>

The property hbm2ddl.auto is set to create to make the testing process idempotent, meaning every execution of same test cases generates same result. You also don't need to clean testing data created from testing phases. Please note I intentionally use HSQLDialect here due to H2Dialect in some version of Hibernate is buggy. Followings are more details of the example:

testng.xml
<suite name="Test Suite">
  <test name="Test Cases">
    <classes>
      <class name="org.sheng.test.MyTest"/>
    </classes>
  </test>
</suite>

MyTest.java
public class MyTest {

  protected Session session;

  @BeforeSuite
  public void init() throws Exception {
    DaoFactory.configureSessionFactory("test-hibernate.cfg.xml");
  }

  @BeforeClass
  public void setUp() throws Exception {
    session = DaoFactory.newSession();
  }

  @DataProvider(name = "users") 
  public Object[][] getUsers() { 
    return new Object[][] { 
      new Object[] { "admin", "password" },
      new Object[] { "manager", "password" },
      new Object[] { "user1", "password" },
      new Object[] { "user2", "password" }
    };
  }

  @Test(groups = {"database"}, dataProvider = "users")
  public void createUser(String username, String password) throws Exception {
    User user = new User();
    user.setUsername(username);
    uesr.setPassword(password);
    DaoFactory.getUserDao().createUser(session, user);
  }

  @Test(groups = {"database"}, dependsOnMethods = {"createUser"})
  public void verifyUsers() throws Exception {
    //Verify users
  }

}

MyTest.java
public class DaoFactory {

  private static AnnotationConfiguration configuration;
  private static SessionFactory sessionFactory;

  public synchronized static void configureSessionFactory(String resource) {
    URL url = getResource(resource);
    configuration = new AnnotationConfiguration();
    sessionFactory = configuration.configure(url).buildSessionFactory();
  }

  public static Session newSession() {
    return sessionFactory.openSession();
  }

  public static URL getResource(String resource) {
    ClassLoader classLoader = null;
    URL url = null;

    try {
      Method method = Thread.class.getMethod("getContextClassLoader", (Class[])null);
      classLoader = (ClassLoader)method.invoke(Thread.currentThread(), (Object[])null);
      url = classLoader.getResource(resource);      

      if(url != null) {
        return url;
      }
    }
    catch(Exception e) {
      e.printStackTrace()
    }

    return ClassLoader.getSystemResource(resource);
  }

}

Effective testing process is important to the success of a project. It's difficult to keep track of every change especially when you are collaborating with other teammates. Well-written test cases also serves as documents to help a developer who is new to the project to make contributions in a timely manner.