UNIT TEST WITH JMOCKIT IN JAVA
1. Mocking trong Unit test là gì? Unit test là việc kiểm tra tính đúng của một method. Input giá trị A thì Output phải là giá trị B. Với những method đơn giản, nhận dữ liệu đầu vào, tính toán và trả kết quả đầu ra thì việc unit test là rất dễ dàng. Với những method phức tạp hơn, khi tính toán có ...
1. Mocking trong Unit test là gì?
Unit test là việc kiểm tra tính đúng của một method. Input giá trị A thì Output phải là giá trị B. Với những method đơn giản, nhận dữ liệu đầu vào, tính toán và trả kết quả đầu ra thì việc unit test là rất dễ dàng. Với những method phức tạp hơn, khi tính toán có gọi và sử dụng kết quả trả về từ một method của object khác thì việc viết unit test trở nên phức tạp hơn, và phức tạp hơn nữa khi phải unit test các private method. Giải pháp là ta phải làm giả (mocking) được việc trả về kết quả mong muốn giống như khi gọi method của object kia.
Một cách đơn giản, mocking là việc tạo các object mà nó mô phỏng các hành vi của object thực.
Ta xét ví dụ sau.
public class View { DBManager dbManager = new DBManager() public String processName(){ String name = dbManager.getName(); return name.toLowerCase(); } }
Thông thường để unit test method processName()của View thì ta phải có DB thỏa mãn để method getName()của object dbManager thực thi lấy dữ liệu từ BD và trả về name (String). Việc này là rắc rối, vậy ta sẽ làm giả (mocking) method getName()của object dbManager.
public class ViewTest { @Test public void testProcessName(){ //* Code to mock dbManager.getName //* to return "Anoop" goes here View view= new View(); String name = view.processName(); // here internally dbmanager returns //"Anoop" because its mocked assertEquals("The returned name should be anoop","anoop",name); } }
2. Làm sao JMockit hỗ trợ mocking?
*** 2.1 JMockit Expectations***
Đây là class được sử dụng nhiều nhất để trả về kết quả mong muốn của mỗi method được mocked. Ở lớp này, các kết quả trả về mong mốn được cài đặt để trả về khi method thực sự được gọi.
Để mock method của object/class thì object/class ấy phải có annotation "@Mocked" phía trước. Cụ thể.
public class ViewTest { @Mocked DBManager dbManager; //NOT using new to create an //instance @Test public void testProcessName(){ View view = new View(); new Expectations(){ //locally defined objects are automatically considered mocked view.getName(); //Setting the expected behaviour here. returns(“Nandan”);//Will return Nandan } }; String name = view.processName(); assertEquals(“Name Should be Nandan”,”Nandan”,name); }
Method testProcessName() trên ta đã làm giả method getName() của view. Nó sẽ trả về Nandan khi được gọi. Việc thực hiện test sẽ thất bại nếu:
-
Khối Expectations không đươc thực hiện lại.
-
Method processName() được gọi hơn một lần.
-
Method khác getName() được gọi.
*** 2.2 JMockit Verifications***
Giống như tên gọi thì class này được dùng để xác thực một cái gì đó sau khi việc gọi một hàm test được thực hiện hay chưa. Class Verifications còn có thể được dùng để kiểm tra một cách rõ ràng một cuộc gọi hàm được tạo ra và gọi bao nhiêu lần.
Chúng ta xét ví dụ sau.
public class ViewTest { @Mocked DBManager dbManager; //NOT using new to create an //instance @Test public void testProcessName(){ View view = new View(); new Expectations(){ //locally defined objects are automatically considered mocked { dbManager.getName();//Setting the expected behaviour here. returns(“Nandan”);//Will return Nandan }}; String name = view.processName(); assertEquals(“Name Should be Nandan”,”Nandan”,name); } @Test public void testGetNameCalls(){ View view = new View(); String name = new.getPersonName(); new Verifications(){ { dbManager.getName();//verify that this method is called when //view.getPersonName() is invoked }}; } } *** 2.3 JMockit Inline Mockups*** Là tính năng mạnh nhất của JMockit để định nghĩa lại methods/static blocks/constructors. Chúng ta xét ví dụ sau. public class ViewTest { @Test public void testGetPersonName(){ View view = new View(); new MockUp<department>(){ { //A directive to JMockit to redefine the method.Remember!! @Mock public String getPersonName(){ return null; } }}; String name = view.getPersonName();//this call returns a null because of redefinition assertNull(name); } }
3. Làm sao chạy test case với JMockit?
Nếu bạn dùng Maven project thì thêm dependances vào file pom.xml khai báo sau.
<dependency> <groupId>com.googlecode.jmockit</groupId> <artifactId>jmockit</artifactId> <version>1.7</version> <scope>test</scope> </dependency>
Nếu bạn dùng Ant project thì chỉ việc add thư viện .jar là file jmockit.jar vào JRE library system.
4. Làm sao mock public methods với JMockit?
Có hai cách thực hiện.
4.1 Sử dụng lớp Expectations
Chúng ta sẽ sử dụng lớp inner Expectations để định nghĩa kết quả mong muốn khi method được gọi với tham số đầu vào (có hoặc không).
Ta xét ví dụ sau.
public class DBManager { public String retrieveAccountHolderName(int accountId ){ String accountHolderName = null; //connect to db //retrieve the Account Holder Name return accountHolderName; } } public class Bank { DBManager dbManager =new DBManager(); public String processAccount(int accountID){ //Some other code goes here String accountHolderName = dbManager.retrieveAccountHolderName(accountID); //some more processing code return accountHolderName; } } /*************** BankTest.java ****************/ public class BankTest { @Test public void testRetrieveAccountHolderName() { Bank bank = new Bank(); // Define the Expectations block here new Expectations() { DBManager dbManager; // variables declared here are mocked by default { dbManager.retrieveAccountHolderName(10); returns(“Abhi”); }}; String name = bank.processAccount(10); assertEquals(“Account holder Name for A/C id 10 is ‘Abhi’ “,”Abhi”,name); } }
Lưu ý: Lớp DBManager được khai báo (không khởi tạo) trong khối lệnh khởi tạo lớp Expectations thì mặc định được làm giả (mocked). Nếu ta khai báo ngoài khối lệnh lớp Expectations thì bắt buộc phải thêm annotation @Mocked phía trước: @Mocked DBManager dbManager;
Điều gì sẽ xảy ra khi ta khai báo Bank bank = newBank()sau khối lệnh Expectations?
Khi đó sẽ có exception UnexpectedInvocation xảy ra. Vì Expectations là strict checking.
Trong khối lệnh khởi tạo Expectations() thì:
-
Chỉ method retrieveAccountHolderName() được gọi từ dbManager.
-
Tham số bắt buộc phải là 10. Nếu khác 10 thì sẽ có exception UnexpectedInvocation xảy ra.
-
Method retrieveAccountHolderName() chỉ được gọi duy nhất 1 lần.
Theo cách trên thì trường hợp test của ta có thể là bị cứng nhắc. JMockit cung cấp cho ta trường anyInt.
Khi đó có thể truyền tham số cho hàm được gọi như sau:
dbManager.retrieveAccountHolderName(anyInt);
4.2 Sử dụng lớp MockUp
Sau đây chúng ta sẽ sử dụng lớp MockUp để định nghĩa lại hàm method retrieveAccountHolderName().
public class BankTest { @Test public void testRetrieveAccountHolderName() { new MockUp<DBManager>() { @SuppressWarnings(“unused”) @Mock public String retrieveAccountHolderName( int accountId ){ return “Abhi”; }}; Bank bank = new Bank(); String name = bank.processAccount(10); assertEquals(“Account holder Name for A/C id 10 is ‘Abhi’ “,”Abhi”,name); } }
Method retrieveAccountHolderName() được định nghĩa lại và luôn luôn trả về chuỗi “Abhi” với kể đầu vào là số int nào.
Lưu ý: Lớp Bank được khai báo sau khối lệnh khởi tạo class MockUp.
So sánh ta thấy sử dụng class MockUp linh hoạt hơn sử dụng class Expectations.
5.Làm sao mock private methods với JMockit?
Về nguyên tắc thì chúng ta không thể gọi private method từ bên ngoài, cũng như trong lớp Test. JMockit cũng cấp cho chúng ta tiện ích sự phản chiếu (Reflextions Untility) gọi là Gỡ bỏ tính đóng goi của lớp (Deencapsulation class).
Tương tự như mock public methods, ta cũng có 2 cách để mock private methods.
5.1 Sử dụng lớp Expectations
Ta xét ví dụ sau:
/****************** Simple.java ***********/ public class Simple { private String iAmPrivate(){ return “Private Method”; } public String publicCallsPrivate(){ return iAmPrivate(); } /**************** SimpleTest.java *********/ public class SimpleTest { @Test public void testPublicInvokesPrivate(){ //Make simple final to be used in the //Expectations inner class final Simple simple = new Simple(); //pass simple as argument to make it a Mocked type //in the Expectations class new Expectations(simple){ { Deencapsulation.invoke(simple, “iAmPrivate”); returns(“I got INVOKED”); }}; String str = simple.publicCallsPrivate(); assertEquals(“The returned string is – I got INVOKED”,”I got INVOKED”,str); } }
Lưu ý: Biến object simple được khái báo final. Và cũng tuân thủ 3 nguyên tắc như mock public method sử dụng class Expectations.
5.2 Sử dụng lớp MockUp
Xét ví dụ sau.
/**************** SimpleTest.java *********/ public class SimpleTest { @Test public void testPublicInvokesPrivateMockUp(){ new MockUp<Simple>(){ //Override the private method //Dont provide any ACCESSS MODIFIER! @Mock String iAmPrivate(){ return “MockUp Invoke”; } }; Simple simple = new Simple(); String str = simple.publicCallsPrivate(); assertEquals(“String returned – MockUp Invoke”,”MockUp Invoke”,str); } }
Lưu ý: Tương tự mock public method sử dụng class MockUp, biến simple khai váo và khởi tạo sau khối khởi tạo lớp MockUp.