Saturday, August 18, 2007

Unit Testing สำหรับ DAO

ปกติเวลาเขียน unit testing
ผมมักจะมีปัญหากับ test ในส่วนของ dao, service layer อยู่มาก
เนื่องจากมันต้องยุ่งเกี่ยวกับ Database
ซึ่งหมายถึงต้องมี Test Data เตรียมไว้ให้เรียบร้อย
ที่แย่ก็คือ พอ testing ที่ไปยุ่งกับ update, delete
มันก็จะทำให้ state ของ data ใน database เราเปลี่ยนไป

ดังนี้ปกติที่ผมทำ ก็คือ
เวลา test ใน service layer ก็ใช้พวก mock กับ stub มาช่วย
จะได้ไม่ต้องยุ่งกับ database

ส่วนใน dao layer ก็เอาพวก DBUnit มาใช้
ซึ่งก็ช่วยได้พอสมควร
แต่ก็รู้สึกว่าการใช้ DBUnit ก็ดูเป็นการทรมาน database ไปหน่อย
(delete, insert ทั้งหมด ทุกๆ test method)
หลังๆ เวลา test ก็เปลี่ยนมาใช้พวก in memory database แทน
ซึ่งผลพลอยได้ ก็คือประหยัดเวลา test ลงไปด้วย

แต่พอเป็นโปรเจคใหญ่ๆที่มี table สัก 200 table
มี data test เยอะพอสมควร
การใช้ DBUnit ก็ดูจะไม่ไหว

สุดท้าย ก็ไปลงเอยที่ AbstractTransactionalDataSourceSpringContextTests
ที่ spring เตรียมไว้ให้
โดยหลักการ ก็คือ spring มันจะเตรียม transaction ไว้ให้เราก่อนเรียก method test
และพอจบ method, spring ก็จะสั่ง rollback ให้
ซึ่งเป็นแนวคิดที่เรียบง่าย แต่ได้ผลดีมาก

แต่ก็มีปัญหาอยู่บ้าง ในกรณีที่เราใช้ Hibernate
hibernate มันจะ cache sql ไว้ (รอให้จบ transaction ก่อนจึงจะทำงาน)
ทำให้เกิดปัญหาว่า assert statement ของเรา
ทำงานไม่ถูกต้อง
ทางแก้ก็คือ เราต้องสั่งให้ hibernate flush sql statement
ก่อนที่จะทำการ assert

ปัญหาอีกอย่างที่เจอ ก็คือ ผมใช้ TestNG อยู่
แต่เจ้าตัว AbstractTransactionalDataSourceSpringContextTests
มัน base อยู่บน JUnit
ก็เลยต้องมีการดัดแปลงนิดๆหน่อยๆ
แต่ผลที่ได้ก็เจ๋งไปอีกแบบคือเลือก run ได้ทั้ง Junit และ TestNG
(พึ่งรู้ว่า เราใช้คำสั่ง assert* ของ Junit มา run ใน TestNG ได้ด้วย)

หน้าตาของ base class ของ testcase ผมก็จะเป็นแบบนี้
public class AbstractDaoTest extends
AbstractTransactionalDataSourceSpringContextTests {

protected SessionFactory sessionFactory;
protected HibernateTemplate hibernateTemplate;

public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
hibernateTemplate = new HibernateTemplate(this.sessionFactory);
}

@Override
protected String[] getConfigLocations() {
return new String[] { "classpath:test-dao.xml" };
}

@Configuration(beforeTestMethod = true, alwaysRun = true)
public void tNGsetUp() throws Exception {
super.setUp();
}

@Configuration(afterTestMethod = true, alwaysRun = true)
public void tNGtearDown() throws Exception {
super.tearDown();
}

protected void flush() {
hibernateTemplate.flush();
}
}


ตัวอย่าง test case
public class TestUserDao extends AbstractDaoTest {

UserDao dao;

@Test
public void testCreateRole() {
assertFalse(dao.getFetchRoles("u190240").isUserInRole("testing"));
dao.addRole("u190240", "testing");

flush();

assertTrue(dao.getFetchRoles("u190240").isUserInRole("testing"));
}

@Test
public void testFindAll() {
List<User> list = dao.findAll();
int cnt = jdbcTemplate.queryForInt("select count(*) from b_user_name");
assertEquals(cnt, list.size());
}

public void setDao(UserDao dao) {
this.dao = dao;
}

}

ข้อดีอีกอย่างของ AbstractTransactionalDataSourceSpringContextTests
ก็คือมัน inject dao เข้ามาให้เราโดยอัตโนมัติ
ไม่ต้องมา getBean เอง

Related link from Roti

2 comments:

Anonymous said...

ผมเคยใช้ทั้งคู่ครับอย่างที่พี่ pok บอก คิดว่าต่างวิธีก็มีข้อดีข้อเสียของมัน

แต่ผมชอบของ Spring มากกว่าจริงๆ แหละ เพราะถ้าไปทำ DBUnit ปรากฎว่ามัน maintain ลำบากมากเลย

เออพี่ อย่างนี้เรียกว่า integrate test จะตรงกว่า unit test ใช่ปะครับ?

PPhetra said...

อืมม์น่าคิดเนอะ ว่ามันควรเรียก unit หรือ integrate test ดี
ในแง่ integrate ก็ถูก ก็คือมันต้องเชื่อมกับ db จึงจะ test ได้
แต่ในแง่ unit ก็คือ dao มันเป็นชั้นที่ไม่ค่อยมี logic ในนั้น
ส่วนใหญ่ delegate ต่อให้ ORM หรือ jdbc
โดยตัวมันเอง การ test มันโดยไม่ต่อ db ดูไม่ค่อยมีความหมายนัก (significant)

เป็นเรืองการตีความหมายของคำ

ดีนท้วงมาก็ดีแล้ว พี่จะได้ไปนั่งอ่านนิยามเพิ่ม
จะได้ใช้ศัทพ์ได้ตรงกับความเข้าใจของคนส่วนใหญ่
(ไม่ใช่ตีความ ด้วยตัวเอง เพื่อตัวเอง)