Mock Objects: Khuyết điểm và hữu dụng(p1)

Tóm tắt
Viết một Unit tests thật ko dễ dàng. Phần lớn thời gian ta dùng là test code kết nối database, với server, hoặc với những modules khác(chưa được viết xong). Với một số lượng lớn điều kiện như vậy thì ko dể gì hiện thực một môi trường test. Việc cài đặt những điều kiện test làm ta mất đi một số lượng lớn thời gian và tiền bạc, và có lẽ những lợi thế của test ko còn như ta mong muốn. Bài viết này dùng Mock Objects, một kỹ thuật test từ cộng đồng XP, khuyến khích cô lập code bằng cách tạo những ràng buộc giả. Như những tool khác, ta cần phải cẩn trọng khi dùng, và ko nên lạm dụng.

Mock Objects Overview
Những năm gần đây, những developer đã làm sống lại lợi thế của việc dùng test, đồng ý rằng việc tìm kiếm và gắn kết các thành phần là khó khăn. Và kết quả là Unit Testing trở thành một thành phần chính của software development, là một cách để tìm lỗi và đồng nhất yêu cầu. Thành phần chính của unit testing là test bằng cách cô lập các thành phần, và thường là class. Test cách ly là rất khó khăn, đặc biệt là khi nó giao tiếp với một phần khó, hoặc tạo nhanh một test. Khó khăn hơn là viết và maintain unit tests, và đó là lý do mà developers chán nản làm test.
Họ đã miêu tả những khó khăn và đưa ra hướng giải quyết:
Tạo những sự phụ thuộc phức tạp(ví dụ như một database)
Kiểm tra những actions cần thiết để run code(ví dụ như một JDBC conection đã đóng sau khi dùng )
Tạo môi trường là rất khó khăn (lỗi thể hiện cảu câu SQL)
Mặc dù hữu dụng, nhưng chắc chắn nó không thể làm được tất cả, việc lạm dụng chỉ làm cho chất lượng của project giảm xuống.
Nhược điểm của Mocks
Hãy nhìn vào những điều cần biết khi dùng.
Mocks ẩn chứa nhiều vấn đề

Figure 1. Lưu thông tin của một employee vào database
Class EmployeeBO cung cấp những business services cho Employees và dùng EmployeeDAO để lưu dữ liệu vào CSDL quan hệ dùng JDBC. Test EmployeeBO cần có bước cài đặt database và cung cấp nó cho việc test.
Đề nghị của mock objects là bạn có thể lưu bằng cách giả lặp một EmployeeDAO, và không cần tạo một database. Mocks giúp cho việc tạo và run nhanh hơn, nhưng chúng ko cho ta sự tin cậy vào hệ thống, như một cách làm phù hợp. Mock testing có thể có bugs hay sai sót trong các thành phần được giả định. Để tìm một sai xót, thì chúng ta cần phải có một bài test tổng thể. Trong ví dụ trên, hệ thống cần test dùng database để lưu thông tin employee. Mock testing hạn chế khi kiểm tra sự chính sác của tương tác giữa EmployeeBO và EmployeeDAO—vì vậy, EmployeeBO chỉ gọi một vài phương thức từ EmployeeDAO trong cùng thời gian. Chỉ có test tổng thể mới có thể giúp ta tìm vấn đề, nhưng lỗi trong JDBC driver hay trong chính bản thân database, cái này chỉ xuất hiện khi application trở thành sản phẩm.
Mocks add clutter and duplication to test code
Đoạn code sau dùng EasyMock để test EmployeeBO dùng EmployeeDAO để lưu thông tin một employees mới và update thông tin một employee đã tồn tại.
@Before public void setUp() {
mockEmployeeDAO = createMock(EmployeeDAO.class);
employeeBO = new EmployeeBO(mockEmployeeDAO);
employee = new Employee("Alex", "CA", "US");
}

@Test public void shouldAddNewEmployee() {
mockEmployeeDAO.insert(employee);
replay(mockEmployeeDAO);
employeeBO.addNewEmployee(employee);
verify(mockEmployeeDAO);
}

@Test public void shouldUpdateEmployee() {
mockEmployeeDAO.update(employee);
replay(mockEmployeeDAO);
employeeBO.updateEmployee(employee);
verify(mockEmployeeDAO);
}
Phương thức shouldAddNewEmployee kiểm tra sự ảnh hưởng giữa một đối tượng đại diện cho (employeeBO) và một đối tượng đại diện cho (mockEmployeeDAO). Mong chờ employeeBO gọi phương thức insert trong mockEmployeeDAO, qua một thể hiện của Employee nhận được. Qua một vì dụ đơn giản, phương thức shouldAddNewEmployee đã không như ta mong muốn, thêm lôn sộn cho bài test:
A gọi replay để báo cho EasyMock rằng tất cả yêu cầu đã thi hành
A gọi verify để báo cho EasyMock đã kiểm tra
Cách dùng mocks thường theo pattern:
1.Cài mock(s) và dự tính
2.Chạy test
3.Xác nhận dự tính đã thi hành
Như một đã nói duplication in test code, đã hiển thị rỏ trong phương thức shouldAddNewEmployee() và shouldUpdateEmployee(). Class sau , EasyMockTemplate, có thể giúp giảm bớt code clutter and duplication:
/**
* Understands a template for usage of EasyMock mocks.
* @author Alex Ruiz
*/
public abstract class EasyMockTemplate {

/** Mock objects managed by this template */
private final List mocks = new ArrayList();

/**
* Constructor.
* @param mocks the mock objects this template will manage.
* @throws IllegalArgumentException if the list of mock objects is null or empty.
* @throws IllegalArgumentException if the list of mock objects contains a null value.
*/
public EasyMockTemplate(Object... mocks) {
if (mocks == null) throw new IllegalArgumentException("The list of mock objects should not be null");
if (mocks.length == 0) throw new IllegalArgumentException("The list of mock objects should not be empty");
for (Object mock : mocks) {
if (mock == null) throw new IllegalArgumentException("The list of mocks should not include null values");
this.mocks.add(mock);
}
}

/**
* Encapsulates the common pattern followed when using EasyMock.
*

    *
  1. Set up expectations on the mock objects

  2. *
  3. Set the state of the mock controls to "replay"

  4. *
  5. Execute the code to test

  6. *
  7. Verify that the expectations were met

  8. *

* Steps 2 and 4 are considered invariant behavior while steps 1 and 3 should be implemented by subclasses of this template.
*/
public final void run() {
setUp();
expectations();
for (Object mock : mocks) replay(mock);
codeToTest();
for (Object mock : mocks) verify(mock);
}

/** Sets the expectations on the mock objects. */
protected abstract void expectations();

/** Executes the code that is under test. */
protected abstract void codeToTest();

/** Sets up the test fixture if necessary. */
protected void setUp() {}
}
Nào, hãy chỉnh sửa test và dùng EasyMockTemplate:
@Test public void shouldAddNewEmployee() {
EasyMockTemplate t = new EasyMockTemplate(mockEmployeeDao) {
@Override protected void expectations() {
mockEmployeeDAO.insert(employee);
}

@Override protected void codeToTest() {
employeeBO.addNewEmployee(employee);
}
};
t.run();
}

@Test public void shouldUpdateEmployee() {
EasyMockTemplate t = new EasyMockTemplate(mockEmployeeDao) {
@Override protected void expectations() {
mockEmployeeDAO.update(employee);
}

@Override protected void codeToTest() {
employeeBO.updateEmployee(employee);
}
};
t.run();
}
Điểm lợi khi dung EasyMockTemplate :
1.Từ phương thức expectations và codeToTest là abstract, EasyMockTemplate làm cho developers khai báo cả expectations và code để test, hạn chế lỗi.
2.Phân cách rỏ ràng expectations và code test, làm cho việc test dễ hiểu và maintain.
3.Loại trừ code duplication từ việc chúng ta không gọi replay và verify nhiều.
Bạn có thể download EasyMockTemplate ở đây.
Tests dùng mocks có thể hỏng
Mock testing là glass box testing, yêu cầu phải hiểu rỏ bản chất. Đây là một mặt ấn tượng của việc tác động giữa các mocks. Hiện thực của một phương thức có thể bẻ một test dùng mocks, mặc dù nếu kết quả chạy giống như một phương thức.
Trong ví dụ trên, EmployeeBO tác động với EmployeeDAO để lưu thông tin employee vào database dùng JDBC. Hãy thay đổi cách lưu xuống database – từ jdbc sang jpa chẳng hạn- bằng cách thay EmployeeDAO với EmployeeJPA, lưu cùng một thông tin xuống cùng một database. Chúng tôi mong rằng sẽ dùng được cái test được viết sẵn, và dĩ nhiên là kết quả ko thay đổi. Không may, test của chúng ta dùng morks sẽ ko đơn giản vì tương tác giữa EmployeeBO và EmployeeDAO ko tồn tại: EmployeeBO dùng EmployeeJPA để lưu xuống database.

Figure 2. Thay cách lưu làm hỏng test dùng morks
Ngược lại, với phương thức test khác (ví dụ như test functional) thì ko phải vậy, kết quả vẫn ko đổi, vì test phụ thuộc vào một thuộc tính trong yêu cầu của System.
Ví dụ, hợp nhất test kiểm tra tính đúng đắn của data được lưu trong database sẽ ko thay đổi khi đổi từ EmployeeDAO sang EmployeeJPA.

Mocking concrete classes can be dangerous
Một vài mock frameworks như EasyMock và JMock cung cấp mở rộng dùng cglib. cglib sinh ra sụ uỷ quyền tại thời điểm cho một class con và đè phương thức interest. Kết quả, lớp mock và phương thức liên quan ko thể final, hạn chế cách giải quyết.
Trong cùng thời gian, một ràng buộc constructor (dùng reflection) có thể cần để hiện thực class-based mock, nối một constructor tới một hay nhiều tests. Do đó, classes dùng mocks khó maintain, và test dễ hỏng, từ đó ko còn quan trọng để refactor code khi tính hữu dụng thể hiện, giống như Java IDEs.
Bình thường mocks, một mocks đều hiện thực của một interface, có những phương thức dự tính, có thể cần cân nhắc nhiều hay ít ởn định. Trái lại, các pt trên intefaces có thể là phương thức protected hay package-protected, tương ứng với hiện thực của class. Như hiện thực có thể thay đổi vào một lúc nào đó, thay sự ảnh hưởng giữa code giữa các mocks, sự thay đổi tăng dần và sẽ phá bỏ logic test.

Cao Trong Hien

,

2 Responses to "Mock Objects: Khuyết điểm và hữu dụng(p1)"

13:30 16 tháng 11, 2007
>Trong cùng thời gian, một ràng buộc constructor (dùng reflection) có thể cần để hiện thực class-based mock, nối một constructor tới một hay nhiều tests. Do đó, classes dùng mocks khó maintain, và test dễ hỏng, từ đó ko còn quan trọng để refactor code khi tính hữu dụng thể hiện, giống như Java IDEs.

Thực tế thì dùng mock dễ maintain vì nó isolate module cần test với các module khác, test hỏng khi và chỉ khi ta thay đổi ràng buộc giữa các module, chuyện này không phải lỗi của mock. Có hay không có mock cũng gặp thôi


>Trong ví dụ trên, EmployeeBO tác động với EmployeeDAO để lưu thông tin employee vào database dùng JDBC. Hãy thay đổi cách lưu xuống database – từ jdbc sang jpa chẳng hạn- bằng cách thay EmployeeDAO với EmployeeJPA, lưu cùng một thông tin xuống cùng một database. Chúng tôi mong rằng sẽ dùng được cái test được viết sẵn, và dĩ nhiên là kết quả ko thay đổi. Không may, test của chúng ta dùng morks sẽ ko đơn giản vì tương tác giữa EmployeeBO và EmployeeDAO ko tồn tại: EmployeeBO dùng EmployeeJPA để lưu xuống database.

Em nên phân biệt là unit test khác với integration hay functional testing. Cho nên case ở trên không phải là yếu điểm của mock object


>Bình thường mocks, một mocks đều hiện thực của một interface, có những phương thức dự tính, có thể cần cân nhắc nhiều hay ít ởn định. Trái lại, các pt trên intefaces có thể là phương thức protected hay package-protected, tương ứng với hiện thực của class. Như hiện thực có thể thay đổi vào một lúc nào đó, thay sự ảnh hưởng giữa code giữa các mocks, sự thay đổi tăng dần và sẽ phá bỏ logic test.

Interface là specification thì khác vói concrete class là implementation. Nếu vì implementation mà làm thay đổi test code thì nên xem lại mình đã design tốt chưa

Hai79
13:03 23 tháng 11, 2007
Cám ơn anh Hải! Đúng là kiến thức của em còn sơ sài quá.

Đăng nhận xét