Có lẽ đa số chúng ta đều quen với việc hoàn tất chương trình, chạy thử, thấy sai, tìm và sửa lỗi, chạy thử... Điều này không có gì là sai trái, tuy nhiên nếu đợi đến khi hoàn tất chương trình rồi mới chạy thử, thì việc tìm và sửa lỗi sẽ tương đối vất vả. Quá trình này sẽ đơn giản hơn, nếu bạn có thể chia chương trình thành từng phần nhỏ, và test chúng một cách riêng rẽ. Đó là ý nghĩa của unit test.
Tuy nhiên, để unit test hiệu quả, chúng ta cần có các công cụ để tiến hành nó một cách tự động. Các công cụ này gọi là unit test framework. Với Java, framework nổi tiếng nhất là JUnit. JUnit là một framework đơn giản dùng cho việc tạo các unit testing tự động, và chạy các test có thể lặp đi lặp lại. Nó chỉ là một phần của họ kiến trúc xUnit cho việc tạo các unit testing. JUnit là một chuẩn trên thực tế cho unit testing trong Java.
Lợi ích của JUnit.
JUnit tránh cho người lập trình phải làm đi làm lại những việc kiểm thử nhàm chán bằng cách tách biệt mã kiểm thử ra khỏi mã chương trình, đồng thời tự động hóa việc tổ chức và thi hành các bộ số liệu kiểm thử.
Thoạt tiên, khi sử dụng JUnit, ta có thể có cảm giác là JUnit chỉ làm mất thêm thời gian cho việc kiểm thử: Thay vì phải viết thêm các lớp và phương thức mới phục vụ cho công tác kiểm thử, ta có thể soạn nhanh một bộ số liệu rồi viết ngay vào trong phương thức main() và quan sát ngay kết quả kiểm thử. Vì quá trình soạn số liệu và quá trình kiểm thử diễn ra đồng thời, nên ta sẽ dễ dàng nhận biết được ngay chương trình đã chạy đúng trên bộ số liệu kiểm thử hay không, mà không cần nhìn vào tín hiệu "xanh" mà JUnit có thể hỗ trợ.
Giới thiệu về JUnit và Mockito
JUnit là một framework dùng cho việc tạo các Unit testing để kiểm thử các method của các đối tượng trong chương trình Java. Trong nhiều trường hợp method được kiểm thử gọi đến những phương thức của interface nào đó, mà interface này chưa được cài đặt (implementing) bởi bất cứ lớp đối tượng nào. Khi đó, trong mã lệnh unit test cần giả lập (mocking) các hàm của interface được sử dụng. Để giả lập chức năng của các hàm trong interface, người ta thường sử dụng một số thư viện hỗ trợ giả lập kiểm thử như Mockito, JMock, … để tạo giả một đối tượng cài đặt interface, giả lập lời gọi hàm đến đối tượng giả này và chỉ định kết quả trả lại cho lời gọi hàm giả lập đó. Trong bài viết này, chúng ta quan tâm đến thư viện Mockito.
Các hàm thường sử dụng trong Mockito
– Mockito.when(T methodCall): dùng để giả lập một lời gọi hàm nào đó được sử dụng bên trong method đang được kiểm thử.
– Hàm Mockito.when() thường đi kèm với .thenReturn(), .thenAnswer(), .thenThrow() để chỉ định kết quả trả lại.
Ví dụ:
//Khi method_A được gọi thì trả về kết quả là “demoValue”: Mockito.when(method_A()).thenReturn("demoValue"); //Khi method_B được gọi thì sẽ ném ra một Exception với message “demoError”: Mockito.when(method_B()).thenThrow(new Exception("demoError")); //Khi method_C được gọi thì sẽ thực hiện xử lý các lệnh định nghĩa // bên trong hàm answer() (hàm này giả lập xử lý của method_C) và // trả về kết quả: Mockito.when(method_C()).thenAnswer(new Answer<String>(){ public String answer(InvocationOnMock invocation){ String str = “demoNewAnswer”; return str; } }); |
Ví dụ:
//Giả lập phương thức method_D với bất kỳ tham số có kiểu “int” thì trả về // kết quả là “1”: Mockito.when(method_D(anyInt())).thenReturn(1); //Giả lập phương thức method_E với bất kỳ tham số có kiểu “String” thì trả // về kết quả là "demoValue": Mockito.when(method_E(anyString())).thenReturn("demoValue"); //Giả lập phương thức method_F với bất kỳ tham số thuộc kiểu class XYZ thì // trả về kết quả là "null": Mockito.when(method_F(any(XYZ.class))).thenReturn(null); |
Minh họa Unit test với JUnit và Mockito
Bài viết này được demo bằng một project được thiết kế như sau, gồm có 3 thành phần chính:
- RestAPI: cung cấp các giao diện lập trình cho phía Client sử dụng, thành phần này chịu trách nhiệm nhận HTTP Resquest và trả kết quả dưới dạng HTTP Response về cho Client.
- Manager: chịu trách nhiệm xử lý các vấn đề liên quan đến nghiệp vụ (business).
- DAO: chịu trách nhiệm connect DB và thực hiện truy vấn dữ liệu.
Như trong hình trên thì có 3 interface và 3 implementation tương ứng.
Như trong hình trên thì có thể thấy thành phần cao nhất là RestAPI gọi Manager, Manager gọi DAO, DAO là thành phần cuối cùng thực hiện kết nối đến Database.
- Khi Unit test ResrAPI ta sẽ thực hiện mock (giả lập) các đối tượng Manager được gọi.
- Khi Unit test Manager ta sẽ thực hiện mock (giả lập) các đối tượng DAO được gọi.
- Khi Unit test DAO ta sẽ không mock (giả lập) gì cả, vì DAO là đơn thể cuối cùng nên không tham chiếu đến thành phần nào cả, nó chỉ làm việc trực tiếp với Database.
Tạo Project Java Về JUnit và Mockito
Các bước xây dựng project:
B1: Tạo project maven có tên là Demo_Mockito_JUnit có cấu trúc như sau.
B2: Tạo các Interface và class.
- Tạo 2 class model đơn giản
Class demo.mockitoJUnit.model.Category
public class Category { public Category(){} public Category(String _id, String _name){ this.id = _id; this.name = _name; } private String id; private String name; public String getId() { return id; } public void setId(String _id) { this.id = _id; } public String getName() { return name; } public void setName(String _name) { this.name = _name; } } |
Class demo.mockitoJUnit.model.Product
public class Product { public Product(){} public Product(String _id, String _name, String _cat){ this.id = _id; this.name = _name; this.catID = _cat; } private String id; private String name; private String catID; public String getId() { return id; } public void setId(String _id) { this.id = _id; } public String getName() { return name; } public void setName(String _name) { this.name = _name; } public String getCatID() { return catID; } public void setCatID(String _catID) { this.catID = _catID; } } |
- Tạo interface CategoryDao.
Tạo interface demo.mockitoJUnit.dao.CategoryDao với 2 function:
- getList(): Lấy danh sách các danh mục.
- getProductsByCatID(id): Lấy danh sách các sản phẩm theo danh mục.
public interface CategoryDao { List<Category> getList(); List<Product> getProductsByCatID(String id); } |
- Tạo interface CategoryManager.
Tạo interface CategoryManager với 2 function:
- getCategories(): Lấy danh sách các danh mục.
- getProducts(id): Lấy danh sách các sản phẩm theo danh mục.
public interface CategoryManager { List<Category> getCategories(); List<Product> getProducts(String id); } |
- Tạo class CategoryManagerImpl
Tạo class CategoryManagerImpl implements interface CategoryManager.
Bên trong lớp này, khai báo thuộc tính có kiểu CategoryDao và nạp đè 2 function getCategories, getProducts. Kiểu dữ liệu của thuộc tính categoryDao là interface chứ không phải là class cài đặt đã được phát triển đầy đủ.
- Trong method getCategories() sẽ gọi phương thức getList() của đối tượng do categoryDao trỏ đến.
- Trong method getProducts(id) sẽ gọi phương thức getProductsByCatID(id) của đối tượng do categoryDao trỏ đến.
public class CategoryManagerImpl implements CategoryManager{ private CategoryDao categoryDao; public CategoryDao getCategoryDao() { return categoryDao; } public void setCategoryDao(CategoryDao _categoryDao) { this.categoryDao = _categoryDao; } public List<Category> getCategories() { return categoryDao.getList(); } public List<Product> getProducts(String _id) { return categoryDao.getProductsByCatID(_id); } } |
- Tạo class CategoryManagerTest.
Tạo folder src/test/java và file CategoryManagerTest.java định nghĩa class demo.mockitoJUnit.manager.CategoryManagerTest
@RunWith(MockitoJUnitRunner.class) public class CategoryManagerTest { //Khai báo đối tượng thực hiện Unit test //@InjectMocks: yêu cầu MockitoJUnitRunner tạo đối tượng cho biến, // gán các đối tượng mock cho các thuộc tính bên trong đối tượng này. @InjectMocks private CategoryManagerImpl categoryManager; @Mock //Khai báo đối tượng sẽ được mock. private CategoryDao categoryDao; private Map<String, Category> categoryMap = new HashMap<String, Category>(); private Map<String, Category> createCategoryMap(int length) { Map<String, Category> quesMap = new HashMap<String, Category>(); for (int i = 0; i < length; i++) { Category question = createCategory("Category_" + i, i); quesMap.put("Category_" + i, question); } return quesMap; } private Category createCategory(String id, int i) { Category category = new Category(id, "Category_" + i); return category; } @Before public void setUp() { MockitoAnnotations.initMocks(this); //Tạo đối tượng Map dùng để giả lập data trong database. categoryMap = createCategoryMap(10); //Thực hiện mock method .getCategories() và giả lập xử lý lấy //dữ liệu từ Database thông qua categoryMap. when(categoryDao.getCategories()).thenAnswer(new Answer<List<Category>>() { public List<Category> answer(InvocationOnMock invocation) throws Throwable { List<Category> newList = new ArrayList<Category>(); for (int i = 0; i < categoryMap.size(); i++) { newList.add(categoryMap.get("Category_" + i)); } return newList; } }); } @Test public void getCategoriesTest() { List<Category> list = categoryManager.getCategories(); Assert.assertTrue(list.size() == 10); } @Test public void getProductsTest() { //Thực hiện mock method .getProducts() và giả lập xử lý trả về kết //quả khi lấy dữ liệu từ Database. Kết quả trả về là list<Product>. when(categoryManager.getProducts(Mockito.anyString())) .thenAnswer(new Answer<List<Product>>() { public List<Product> answer(InvocationOnMock invocation) throws Throwable { List<Product> listProduct = new ArrayList<Product>(); for (int i = 0; i < 5; i++) { Product item = new Product("Product_" + i, "Product_" + i, (String)invocation.getArguments()[0]); listProduct.add(item); } return listProduct; } }); //Thực hiện test method categoryManager.getProducts(idTest) String idTest = "Category_1"; List<Product> listProduct = categoryManager.getProducts(idTest); //Test kết quả sau khi chạy method cần test. //assertTrue là method dùng để kiểm tra thông tin sau khi chạy //testcase có đúng hay không. Assert.assertTrue(listProduct.size() == 5); } @Test public void getProductsTest_NotFound() { //Thực hiện mock method .getProducts() và giả lập xử lý trả về một //Exception. when(categoryManager.getProducts(Mockito.anyString())) .thenAnswer(new Answer<List<Product>>() { public List<Product> answer(InvocationOnMock invocation) throws Throwable { throw new Exception("NotFound"); } }); try{ //Chạy testcase String idTest = "Category_999"; List<Product> listProduct = categoryManager.getProducts(idTest); Assert.assertNull(listProduct); }catch(Exception e){ //Kiểm tra xem thông điệp lỗi có đúng không. Assert.assertEquals("NotFound",e.getMessage()); } } } |
Kết quả thực hiện Unit test:
One comment
i want to test a project by Junit test and Mock framework.
but i have some method must connect to database.
I don't know how to test it.
If you know, you could help me.
thanks you. i hope you can help me.
Leave a Comment