Saturday, July 22, 2006

EasyMock

ผมผัดผ่อนกับเรื่อง Unit Testing แบบจริงๆจังมานานแล้ว
ส่วนใหญ่จะทำนิดทำหน่อย ตรงจุดที่ไม่แน่ใจใน algorithm
มา project ตัวปัจจุบันนี้ก็เลยตั้งใจว่า
จะบังคับตัวเองให้เขียน testcase ดีๆให้ได้

ตัว tool ที่เลือกใช้ ก็คือ TestNG
ซึ่งหลังจากเขียนไปได้จำนวนหนึ่ง ก็รู้สึกไม่ค่อยมี feedback มากระตุ้นตัวเองมากพอ
ก็เลยเอา JCoverage มาใช้อีกตัว

ใช้ไปได้สักพัก ก็ยังรู้สึกว่ามีแรงจูงใจยังไม่พออยู่ดี
เนื่องจากเรารู้ตัวเองว่า เรายังหลีกเลี่ยงไม่แตะ source code ส่วนที่สำคัญจำนวนหนึ่งอยู่
เนื่องจากมันเป็น code ส่วนที่ไกล้ชีิดกับ Database
ซึ่งเมื่อทำเสร็จมันต้อง rollback ข้อมูลที่ insert, update ลงไป
เพื่อให้ database อยู่ในสถานะก่อนหน้าที่จะ test
(ซึ่งเดิมที่เคยทำ ผมใช้ DBUnit ช่วยทำในส่วนนี้)

ปัญหาหลักๆก็คงอยู่ตรงความขี้เกียจนี่แหล่ะ ขี้เกียจเตรียมข้อมูล

EasyMock เข้้ามาเป็นพระเอกในจุดนี้
EasyMock ช่วยให้ผมสามารถ test business logic โดยไม่ต้องยุ่งกับ database ได้

เริ่มแรก สมมติเรามี logic การ Login อยู่
public class AuthenService {

IUserDao userDao;

public boolean authenticate(String login, String password)
throws DisableAccountException{

User user = userDao.getByLoginId(login);
if (user.isEnable()) {
if (user.getPassword().equals(password)) {
return true;
}
} else {
throw new DisableAccountException();
}
if (userDao.increaseFailCount() > 3) {
userDao.disableLogin(login);
}
return false;
}

}

โดยเราจะ test logic "ในกรณีที่ login ไม่สำเร็จ 3 ครั้งให้ disable account ของ user"
คัว class ที่เราจะสร้าง Mock ก็คือ IUserDao
public interface IUserDao {
/**
*
@param login
*
@return
*/

public User getByLoginId(String login) ;

/**
*
@return number of fail count
*/

public int increaseFailCount();

/**
*
@param login
*/

public void disableLogin(String login);

}


เริ่มด้วยการเขียน testcase
public class TestLogin {

private User createUser(boolean flag) {
User user = new User();
user.setLogin("pok");
user.setPassword("xxx");
user.setEnable(flag);
return user;
}

@Test
public void testDisableAccount() {

IUserDao dao = createStrictMock(IUserDao.class);

expect(dao.getByLoginId("pok")).andReturn(createUser(true));
expect(dao.increaseFailCount()).andReturn(4);
dao.disableLogin("pok");
expect(dao.getByLoginId("pok")).andReturn(createUser(false));

replay(dao);

AuthenService service = new AuthenService();
service.setUserDao(dao);
try {
service.authenticate("pok", "yyy");
} catch (DisableAccountException de) {
fail("early throw exception");
}
try {
service.authenticate("pok", "yyy");
fail("should not be here.");
} catch (DisableAccountException de) {

}
verify(dao);
}

}

ใน code เราเริ่มด้วยการ สร้าง Mock ของ​ IUserDao
IUserDao dao = createStrictMock(IUserDao.class);

จากนั้นก็ train Mock ว่า เดี๋ยวในตอน test จะมีการเรียกใช้แบบนี้เกิดขึ้นนะ
expect(dao.getByLoginId("pok")).andReturn(createUser(true));
expect(dao.increaseFailCount()).andReturn(4);
dao.disableLogin("pok");
expect(dao.getByLoginId("pok")).andReturn(createUser(false));

จบการ train ด้วยคำสั่ง replay
replay(dao);

ที่เหลือก็เป็นการจำลองการเรียกใช้งาน AuthenService

สุดท้ายจบ testcase ด้วยคำสั่ง
verify(dao);

ซึ่งเป็นการตรวจว่าถ้า dao เราถูกเรียกไม่เรียงลำดับ หรือไม่ครบตามที่เรากำหนดไว้
testcase นั้นก็จะ error

ลองดูผลลัพท์จาก JCoverage หลังจาก test แล้ว



สถานะปัจจุบัน ตอนนี้ผมได้ feel ของการเขียน test กลับมาแล้ว
คือรู้สึกว่าติดและสนุกกับการเขียน Test

Related link from Roti

No comments: