Kiểm thử dễ dàng hơn với EasyMock

Bắt chước các giao diện, các lớp và các ngoại lệ bằng khung công tác đối tượng giả (mock-object) mã nguồn mở

Hãy cùng Elliotte Rusty Harold xem xét một số kiểm thử đơn vị khó thực hiện, được làm cho trở nên dễ dàng hơn thông qua các đối tượng giả - cụ thể hơn là khung công tác EasyMock. Thư viện mã nguồn mở này tiết kiệm thời gian cho bạn và giúp bạn làm cho mã đối tượng giả của bạn súc tích và dễ đọc.

Elliotte Rusty Harold, Giáo sư, Polytechnic University

Elliotte Rusty Harold xuất thân từ bang New Orleans nơi mà ông vẫn thỉnh thoảng về thăm những lúc thảnh thơi. Tuy nhiên, ông đang cư trú gần Trung tâm University Town Center, Irvine cùng với vợ ông là Beth và những chú mèo Charm (được đặt tên theo hạt "charm quark" trong vật lý) và Marjorie (đặt tên theo tên của mẹ vợ ông). Trang Web Cafe au Lait của ông đã trở thành một trong những trang Java độc lập nổi tiếng nhất trên Internet, và trang Web phụ của ông, Cafe con Leche, đã trở thành một trong những trang XML phổ biến nhất. Cuốn sách của ông gần đây nhất là Refactoring HTML



28 05 2010

Phát triển hướng theo kiểm thử là một thành phần quan trọng của việc phát triển phần mềm. Nếu mã lệnh không được kiểm thử, thì nó sẽ đổ vỡ. Tất cả mã lệnh phải được kiểm thử, và điều lý tưởng là các phép kiểm thử phải được viết trước khi viết mã của mô hình. Nhưng có một số thứ dễ dàng được kiểm thử hơn những thứ khác. Nếu bạn đang viết một lớp đơn giản để biểu diễn tiền tệ, thì rất dễ dàng để kiểm thử rằng bạn có thể cộng $ 1.23 với 2.28 € và nhận được kết quả là $ 4.03 và không phải là $ 3.03 hoặc $ 4,029999998. Ta cũng không gặp nhiều khó khăn hơn để kiểm thử rằng ta không thể tạo ra một số tiền là $ 7.465. Nhưng làm thế nào để bạn kiểm thử phương thức quy đổi $ 7.50 thành 5.88 € - nhất là khi tỷ giá hối đoái được áp dụng bằng cách kết nối đến một cơ sở dữ liệu sống, với các thông tin được cập nhật liên tục mỗi giây? Kết quả đúng của phương thức amount.toEuros() có thể thay đổi mỗi khi bạn chạy chương trình.

Câu trả lời là các đối tượng giả (mock objects). Thay vì kết nối với một máy chủ thực cung cấp thông tin về tỷ giá được cập nhật mỗi phút, phép kiểm thử kết nối tới một máy chủ giả luôn luôn trả về cùng tỷ giá hối đoái. Thế thì bạn có một kết quả dự đoán trước được mà bạn có thể kiểm thử. Sau cùng, mục tiêu là kiểm thử lôgic hoạt động trong phương thức toEuros() chứ không phải kiểm tra máy chủ có gửi đi các giá trị chính xác hay không. (Hãy để cho các nhà phát triển đã xây dựng máy chủ lo việc này). Loại đối tượng giả này đôi khi được gọi là đồ giả (fake).

Các đối tượng giả cũng có thể hữu ích cho việc kiểm thử các điều kiện lỗi. Ví dụ: Điều gì sẽ xảy ra nếu phương thức toEuros() cố gắng lấy ra tỷ giá mới nhất, khi mạng bị hỏng? Bạn có thể tháo cáp mạng Ethernet khỏi máy tính của mình và sau đó chạy phép kiểm thử của bạn, nhưng ta sẽ không cần nhiều công sức để viết một đối tượng giả, mô phỏng một lỗi mạng.

Các đối tượng giả cũng có thể được dùng để do thám hành vi của một lớp. Bằng cách đặt các xác nhận bên trong mã đối tượng giả, bạn có thể xác minh được rằng mã đang được kiểm thử chuyển các đối số đúng đắn cho các đối tượng cộng tác với nó vào đúng thời điểm. Đối tượng giả có thể cho phép bạn xem và kiểm thử các phần riêng tư (private) của một lớp mà không để lộ chúng ra qua các phương thức công cộng không cần thiết khác.

Cuối cùng, các đối tượng giả giúp loại bỏ khỏi phép kiểm thử các phụ thuộc cồng kềnh. Chúng làm cho các phép kiểm thử trở nên một đơn vị trọn vẹn hơn. Thất bại trong phép kiểm thử dùng một đối tượng giả sẽ nhiều khả năng là thất bại trong phương thức đang được kiểm thử hơn là ở trong một phụ thuộc của nó. Điều này giúp cô lập vấn đề và làm cho việc gỡ lỗi trở nên đơn giản hơn.

EasyMock là một thư viện đối tượng giả mã nguồn mở cho ngôn ngữ lập trình Java, giúp bạn nhanh chóng và dễ dàng tạo ra các đối tượng giả cho tất cả các mục đích đó. Nhờ sự kỳ diệu của phép ủy nhiệm (proxy) động, EasyMock cho phép bạn tạo ra một triển khai thực hiện cơ sở của bất kỳ giao diện nào chỉ với một dòng mã lệnh. Bằng cách thêm phần mở rộng về lớp của EasyMock, bạn cũng có thể tạo ra các cái làm giả (mocks) các lớp. Các giả lớp này có thể được cấu hình cho bất kỳ mục đích nào, từ các đối số giả đơn giản để điền vào một chữ ký phương thức đến các trình do thám đa triệu gọi (multi-invocation), nhằm xác minh một dãy dài các cuộc gọi phương thức.

Giới thiệu về EasyMock

Tôi sẽ bắt đầu bằng một ví dụ cụ thể để chứng tỏ EasyMock làm việc như thế nào. Liệt kê 1 là giao diện ExchangeRate giả định. Giống như bất kỳ giao diện nào, nó chỉ đơn giản nói lên những gì mà một cá thể làm chứ không chỉ rõ cá thể làm điều đó như thế nào. Ví dụ: Nó không nói các dữ liệu về tỷ giá hối đoái đến từ trang Yahoo finance, từ chính phủ, hay từ nơi nào khác.

Liệt kê 1. Giao diện ExchangeRate
import java.io.IOException;

public interface ExchangeRate {

    double getRate(String inputCurrency, String outputCurrency) throws IOException;

}

Liệt kê 2 là bộ xương sườn của lớp Currency (tiền tệ) giả định. Nó thực sự khá phức tạp, và nó cũng có thể có lỗi. (Tôi tránh cho bạn khỏi phải hồi hộp: nó có lỗi – thực sự cũng kha khá lỗi)

Liệt kê 2. Lớp Currency
import java.io.IOException;

public class Currency {

    private String units;
    private long amount;
    private int cents;


    public Currency(double amount, String code) {
        this.units = code;
        setAmount(amount);
    }

    private void setAmount(double amount) {
        this.amount = new Double(amount).longValue();
        this.cents = (int) ((amount * 100.0) % 100);
    }

    public Currency toEuros(ExchangeRate converter) {
        if ("EUR".equals(units)) return this;
        else {
            double input = amount + cents/100.0;
            double rate;
            try {
                rate = converter.getRate(units, "EUR");
                double output = input * rate;
                return new Currency(output, "EUR");
            } catch (IOException ex) {
                return null;
            }
        }
    }

    public boolean equals(Object o) {
        if (o instanceof Currency) {
            Currency other = (Currency) o;
            return this.units.equals(other.units)
                    && this.amount == other.amount
                    && this.cents == other.cents;
        }
        return false;
    }

    public String toString() {
        return amount + "." + Math.abs(cents) + " " + units;
    }

}

Nhìn thoáng qua, có thể chưa thấy rõ ràng ngay một điều quan trọng về thiết kế của lớp Currency. Tỷ giá hối đoái được chuyển từ bên ngoài lớp vào. Nó không được xây dựng ở bên trong lớp. Điều này là rất trọng yếu, cho phép tôi làm giả tỷ giá hối đoái để các phép kiểm thử có thể chạy mà không cần nói chuyện với máy chủ tỷ giá hối đoái thực. Nó cũng cho phép các ứng dụng phía khách cung cấp các nguồn khác nhau của dữ liệu tỷ giá hối đoái.

Liệt kê 3 trình bày một phép kiểm thử JUnit để xác minh rằng $ 2.50 đã được quy đổi thành 3.75 € khi tỷ giá hối đoái là 1.5. EasyMock được sử dụng để tạo ra đối tượng ExchangeRate luôn cung cấp giá trị 1.5.

Liệt kê 3. Lớp CurrencyTest
import junit.framework.TestCase;
import org.easymock.EasyMock;
import java.io.IOException;

public class CurrencyTest extends TestCase {

    public void testToEuros() throws IOException {
        Currency testObject = new Currency(2.50, "USD");
        Currency expected = new Currency(3.75, "EUR");
        ExchangeRate mock = EasyMock.createMock(ExchangeRate.class);
        EasyMock.expect(mock.getRate("USD", "EUR")).andReturn(1.5);
        EasyMock.replay(mock);
        Currency actual = testObject.toEuros(mock);
        assertEquals(expected, actual);
    }

}

Nói đúng ra là Liệt kê 3 thất bại khi lần đầu tiên tôi chạy nó, vì các phép kiểm thử thường như vậy. Tuy nhiên, tôi đã sửa lỗi đó trước khi đưa ra ví dụ này. Đây là lý do tại sao chúng ta thực hiện việc phát triển hướng kiểm thử (TDD).

Bạn hãy chạy phép thử này và nó thoát thành công. Điều gì đã xảy ra? Ta hãy nhìn vào từng dòng lệnh một của phép kiểm thử. Trước tiên đối tượng kiểm thử và kết quả dự kiến được xây dựng:

Currency testObject = new Currency(2.50, "USD");
Currency expected = new Currency(3.75, "EUR");

Chưa có gì mới.

Tiếp theo tôi tạo một phiên bản giả của giao diện ExchangeRate bằng cách gửi đối tượng Class cho giao diện này tới cho phương thức tĩnh EasyMock.createMock():

ExchangeRate mock = EasyMock.createMock(ExchangeRate.class);

Đây là một phần huyền bí nhất. Hãy lưu ý rằng tôi chưa hề viết một lớp thực hiện giao diện ExchangeRate nào. Hơn nữa, tuyệt đối không có cách nào mà phương thức EasyMock.createMock() có thể được được định kiểu để trả về một cá thể ExchangeRate, một kiểu mà nó chưa bao giờ biết đến và tôi đã tạo ra chỉ vì bài viết này. Và ngay cả khi nếu bởi một phép mầu nào đó, nó trả về một ExchangeRate, thì điều gì xảy ra khi tôi cần làm giả một cá thể của một giao diện khác?

Lần đầu tiên tôi thấy điều này tôi gần như ngã ngửa người vì ngạc nhiên. Tôi không tin rằng mã này có thể biên dịch được, và nó đã làm được điều đó. Có một ma thuật bí ẩn ở đây, nó là kết quả của sự kết hợp giữa đặc điểm chung của Java 5 với phép ủy nhiệm động đã được đưa vào kể từ Java phiên bản 1.3 (xem phần Tài nguyên). May mắn thay, để sử dụng nó (và thán phục sự thông minh của các lập trình viên đã phát minh ra các thủ thuật này), bạn không cần phải hiểu cách nó hoạt động như thế nào.

Bước tiếp theo thực sự đáng ngạc nhiên. Để báo cho đối tượng giả những gì mà mình mong đợi, tôi gọi phương thức như một đối số của phương thức EasyMock.expect(). Sau đó, tôi gọi phương thức andReturn() để chỉ rõ những gì sẽ là kết quả của việc gọi phương thức này:

EasyMock.expect(mock.getRate("USD", "EUR")).andReturn(1.5);

EasyMock ghi lại hành động triệu gọi này và nó biết sau này cần làm những gì.

Nếu bạn quên gọi phương thức EasyMock.replay() trước khi sử dụng đối tượng giả, thì bạn sẽ nhận được một ngoại lệ IllegalStateException với một thông báo lỗi không hữu ích lắm: thiếu định nghĩa hành vi cho cuộc gọi phương thức trước đó.

Tiếp theo tôi làm cho đối tượng giả sẵn sàng phát lại dữ liệu đã được ghi lại của nó bằng cách gọi phương thức EasyMock.replay():

EasyMock.replay(mock);

Đây là một phần của thiết kế mà tôi nhận thấy có một chút rối rắm. Phương thức EasyMock.replay() không thực sự phát lại đối tượng giả. Thay vào đó, nó thiết đặt lại đối tượng giả để lần sau khi các phương thức của nó được gọi, nó sẽ bắt đầu việc phát lại.

Bây giờ đối tượng giả đã được chuẩn bị xong, tôi chuyển nó như một đối số tới cho phương thức đang được kiểm thử:

Làm giả các lớp

Việc làm giả các lớp là khó khăn hơn, nhìn từ phối cảnh triển khai thực hiện. Bạn không thể tạo một ủy nhiệm (proxy) động cho một lớp. Khung công tác EasyMock tiêu chuẩn không hỗ trợ việc làm giả các lớp. Tuy nhiên, phần mở rộng về lớp của EasyMock sử dụng thao tác mã bytecode để tạo ra tác dụng tương tự. Các mẫu trong mã của bạn gần như giống hệt nhau. Bạn chỉ cần nhập khẩu lớp org.easymock.classextension.EasyMock thay vì lớp org.easymock.EasyMock. Việc làm giả lớp cũng cho bạn tùy chọn để thay thế một số phương thức trong một lớp bằng cái giả và để nguyên các phương thức khác.

Currency actual = testObject.toEuros(mock);

Cuối cùng, tôi kiểm tra xem câu trả lời có đúng như mong đợi không:

assertEquals(expected, actual);

Và tất cả chỉ có thế. Bất cứ lúc nào bạn có một giao diện cần phải trả những về kết quả nhất định dành cho mục đích của kiểm thử, thì bạn có thể chỉ cần tạo nhanh ra một đối tượng giả. Điều này thực sự dễ dàng. Giao diện ExchangeRate đủ nhỏ và đơn giản để tôi có thể viết thủ công một cách dễ dàng một lớp giả. Tuy nhiên, giao diện càng lớn và càng phức tạp, thì càng mất nhiều công để viết các đối tượng giả riêng lẻ cho mỗi kiểm thử đơn vị. EasyMock cho phép bạn thực hiện các giao diện lớn như java.sql.ResultSet hoặc org.xml.sax.ContentHandler bằng một dòng mã, và sau đó cung cấp cho chúng đủ hành vi để chạy phép kiểm thử của bạn.


Kiểm tra các ngoại lệ

Một trong những sử dụng phổ biến hơn của đối tượng giả là để kiểm thử các điều kiện ngoại lệ. Ví dụ: Bạn không thể dễ dàng tạo ra một vụ hỏng mạng theo yêu cầu của mình, nhưng bạn có thể tạo ra vụ hỏng giả, phỏng theo vụ hỏng thực.

Lớp Currency được giả định là trả về kết quả null khi phương thức getRate() đưa ra một ngoại lệ IOException. Liệt kê 4 kiểm thử điều này:

Liệt kê 4. Kiểm thử xem một phương thức đưa ra ngoại lệ đúng
public void testExchangeRateServerUnavailable() throws IOException {
   Currency testObject = new Currency(2.50, "USD");
   ExchangeRate mock = EasyMock.createMock(ExchangeRate.class);
   EasyMock.expect(mock.getRate("USD", "EUR")).andThrow(new IOException());
   EasyMock.replay(mock);
   Currency actual = testObject.toEuros(mock);
   assertNull(actual);
}

Đoạn mã mới ở đây là phương thức andThrow(). Như bạn có thể đã đoán được, đoạn mã này chỉ ra lệnh cho phương thức getRate () đưa ra một ngoại lệ đã định khi được gọi.

Bạn có thể trả về bất kỳ loại ngoại lệ nào mà bạn thích – ngoại lệ đã kiểm tra (checked), ngoại lệ thời gian chạy (runtime) hoặc lỗi (error) - chừng nào mà chữ ký phương thức còn hỗ trợ nó. Điều này đặc biệt hữu ích để kiểm thử các điều kiện đặc biệt hiếm khi xảy ra (Ví dụ như lỗi tràn bộ nhớ hoặc không tìm thấy định nghĩa lớp) hoặc các điều kiện chỉ ra lỗi máy ảo (ví dụ như có không có mã ký tự UTF-8).


Thiết đặt các kết quả mong đợi

EasyMock không chỉ cung cấp cho câu trả lời làm sẵn cho các câu hỏi được làm sẵn. Nó cũng có thể kiểm tra xem đầu vào có đúng những gì cần phải có hay không. Ta lấy ví dụ, giả sử rằng phương thức toEuros() có lỗi như trong liệt kê 5, tại đây nó sẽ trả về một kết quả tính bằng đồng euro nhưng lại lấy tỷ giá hối đoái cho đồng đô la Canada. Điều này có thể làm ai đó được hoặc bị mất rất nhiều tiền.

Liệt kê 5. Một phương thức toEuros() bị lỗi
public Currency toEuros(ExchangeRate converter) {
    if ("EUR".equals(units)) return this;
    else {
        double input = amount + cents/100.0;
        double rate;
        try {
            rate = converter.getRate(units, "CAD");
            double output = input * rate;
            return new Currency(output, "EUR");
        } catch (IOException e) {
            return null;
        }
    }
}

Tuy nhiên, tôi không cần phải làm một phép kiểm thử bổ sung cho việc này. Đoạn mã testToEuros trong Liệt kê 4 đã bắt lỗi này rồi. Khi bạn chạy phép thử này với mã bị lỗi trong liệt kê 4, thì phép thử sẽ không thành công với thông báo lỗi sau:

"java.lang.AssertionError:
  Unexpected method call getRate("USD", "CAD"):
    getRate("USD", "EUR"): expected: 1, actual: 0".

Lưu ý rằng đây không phải là một khẳng định mà tôi tạo ra. EasyMock nhận thấy rằng các đối số mà tôi đã dùng không ăn nhập và đánh hỏng ca kiểm thử.

Theo mặc định, EasyMock chỉ cho phép các ca kiểm thử gọi những phương thức mà bạn đã định với các đối số mà bạn đã chỉ rõ. Đôi khi đây là một điều hơi quá khắt khe, do đó có những cách để nới lỏng nó. Ví dụ: Giả sử tôi muốn cho phép bất kỳ một chuỗi ký tự nào được chuyển cho phương thức getRate(), thay vì chỉ là USD và EUR. Thế thì tôi có thể chỉ rõ rằng tôi mong đợi phương thức EasyMock.anyObject () thay vì các chuỗi ký tự tường minh, như liệt kê sau:

EasyMock.expect(mock.getRate(
       (String) EasyMock.anyObject(),
       (String) EasyMock.anyObject())).andReturn(1.5);

Tôi có thể đào sâu thêm một chút và chỉ rõ phương thức EasyMock.notNull () chỉ cho phép các chuỗi không rỗng:

EasyMock.expect(mock.getRate(
        (String) EasyMock.notNull(),
        (String) EasyMock.notNull())).andReturn(1.5);

Việc kiểm tra kiểu tĩnh sẽ ngăn không cho giá trị không phải là chuỗi ký tự được chuyển cho phương thức này. Tuy nhiên, bây giờ tôi cho phép cả các giá trị kiểu String ngoài USD và EUR. Bạn có thể sử dụng các biểu thức chính quy để được thể hiện tường minh hơn với phương thức EasyMock.matches (). Ở đây tôi yêu cầu một chuỗi ký tự ASCII gồm ba ký tự, viết hoa:

EasyMock.expect(mock.getRate(
        (String) EasyMock.matches("[A-Z][A-Z][A-Z]"),
        (String) EasyMock.matches("[A-Z][A-Z][A-Z]"))).andReturn(1.5);

Việc sử dụng phương thức EasyMock.find() thay cho phương thức EasyMock.matches() sẽ chấp nhận bất kỳ String nào có chứa một chuỗi con gồm 3 ký tự được viết hoa.

EasyMock có những phương thức tương tự cho các loại dữ liệu nguyên thuỷ:

  • EasyMock.anyInt()
  • EasyMock.anyShort()
  • EasyMock.anyByte()
  • EasyMock.anyLong()
  • EasyMock.anyFloat()
  • EasyMock.anyDouble()
  • EasyMock.anyBoolean()

Đối với các giá trị kiểu số, bạn cũng có thể sử dụng phương thức EasyMock.lt(x) để chấp nhận bất kỳ giá trị nào nhỏ hơn x, hoặc phương thức EasyMock.gt (x) để chấp nhận bất kỳ giá trị nào lớn hơn x.

Khi kiểm tra một dãy dài các kết quả mong đợi, bạn có thể thu được các kết quả hay đối số của một lời gọi phương thức và so sánh nó với giá trị được chuyển cho lời gọi phương thức khác. Và cuối cùng, bạn có thể định nghĩa các phép so khớp (matchers) tùy chỉnh, kiểm tra hầu như bất kỳ chi tiết nào về các đối số mà bạn có thể tưởng tượng ra, mặc dù quá trình để làm như vậy hơi phức tạp. Tuy nhiên, đối với hầu hết các phép kiểm thử, các phép so khớp như EasyMock.anyInt(), EasyMock.matches() và EasyMock.eq() là đủ.


Các đối tượng giả nghiêm ngặt và kiểm tra trình tự

EasyMock không chỉ kiểm tra rằng các phương thức dự kiến sẽ được gọi với các đối số đúng. Nó cũng có thể xác minh rằng bạn gọi các phương thức đó và chỉ những phương thức đó, theo đúng trình tự. Việc kiểm tra này không được thực hiện theo mặc định. Để bật tính năng này, bạn hãy gọi phương thức EasyMock.verify(mock) tại phần cuối của phương thức kiểm thử của bạn. Ví dụ, Liệt kê 6 bây giờ sẽ thất bại nếu phương thức toEuros() gọi phương thức getRate() nhiều lần:

Liệt kê 6. Kiểm tra rằng phương thức getRate() được gọi chỉ một lần
public void testToEuros() throws IOException {
    Currency expected = new Currency(3.75, "EUR");
    ExchangeRate mock = EasyMock.createMock(ExchangeRate.class);
    EasyMock.expect(mock.getRate("USD", "EUR")).andReturn(1.5);
    EasyMock.replay(mock);
    Currency actual = testObject.toEuros(mock);
    assertEquals(expected, actual);
    EasyMock.verify(mock);
}

Chính xác phương thức EasyMock.verify() thực hiện kiểm tra như thế nào thực sự phụ thuộc vào các chế độ có sẵn mà nó vận hành theo:

  • Chế độ bình thường — EasyMock.createMock(): Tất cả các phương thức dự kiến sẽ phải được gọi với các đối số đã chỉ rõ. Tuy nhiên, trình tự mà trong đó các phương thức này được gọi không quan trọng. Các cuộc gọi đến phương thức không dự kiến làm cho phép thử thất bại.
  • Chế độ nghiêm ngặt — EasyMock.createStrictMock(): Tất cả các phương thức dự kiến sẽ phải được gọi với các đối số dự kiến, theo một trình tự đã chỉ rõ. Các cuộc gọi đến các phương thức không dự kiến làm cho phép thử thất bại.
  • Chế độ dễ tính — EasyMock.createNiceMock(): Tất cả các phương thức dự kiến sẽ phải được gọi với đối số đã chỉ rõ theo trình tự bất kỳ. Các cuộc gọi đến các phương thức không dự kiến không làm cho phép thử thất bại. Các đối tượng giả dễ tính là các mặc định hợp lý cho các phương thức bạn không làm giả một cách tường minh. Các phương thức trả về giá trị số sẽ trả về giá trị 0. Các phương thức trả về giá trị boolean sẽ trả về giá trị false. Phương thức trả về đối tượng sẽ trả về giá trị null.

Việc kiểm tra trình tự và số lần mà một phương thức được gọi thậm chí hữu ích hơn trong các giao diện lớn hơn và các phép kiểm thử lớn hơn. Ví dụ: Ta hãy xem xét giao diện org.xml.sax.ContentHandler. Nếu bạn đã kiểm thử một trình phân tích cú pháp XML, bạn hẳn đã muốn nhập vào các tài liệu và muốn kiểm tra rằng trình phân tích cú pháp đã gọi đúng các phương thức trong ContentHandler theo đúng trình tự. Ví dụ, ta hãy xem xét tài liệu XML đơn giản trong liệt kê 7:

Liệt kê 7. Một tài liệu XML đơn giản
<root>
  Hello World!
</root>

Theo đặc tả kỹ thuật SAX, khi trình phân tích cú pháp phân tích tài liệu này, nó cần phải gọi những phương thức dưới đây theo trình tự sau:

  1. setDocumentLocator()
  2. startDocument()
  3. startElement()
  4. characters()
  5. endElement()
  6. endDocument()

Tuy nhiên, chỉ để làm cho vấn đề thú vị hơn, cuộc gọi đến phương thức setDocumentLocator() là tùy chọn; trình phân tích cú pháp được phép gọi phương thức characters() nhiều lần. Chúng không cần phải chuyển luồng văn bản dài tối đa trong một cuộc gọi duy nhất, và trong thực tế, hầu hết chúng không làm như vậy. Điều này khó kiểm thử bằng các phương thức truyền thống, ngay cả đối với một tài liệu đơn giản như liệt kê 7, nhưng EasyMock làm cho nó trở nên đơn giản hơn, như trong liệt kê 8:

Liệt kê 8. Kiểm thử một trình phân tích cú pháp XML
import java.io.*;
import org.easymock.EasyMock;
import org.xml.sax.*;
import org.xml.sax.helpers.XMLReaderFactory;
import junit.framework.TestCase;

public class XMLParserTest extends TestCase {

    private  XMLReader parser;

    protected void setUp() throws Exception {
        parser = XMLReaderFactory.createXMLReader();
    }

    public void testSimpleDoc() throws IOException, SAXException {
        String doc = "<root>\n  Hello World!\n</root>";
        ContentHandler mock = EasyMock.createStrictMock(ContentHandler.class);

        mock.setDocumentLocator((Locator) EasyMock.anyObject());
        EasyMock.expectLastCall().times(0, 1);
        mock.startDocument();
        mock.startElement(EasyMock.eq(""), EasyMock.eq("root"), EasyMock.eq("root"),
                (Attributes) EasyMock.anyObject());
        mock.characters((char[]) EasyMock.anyObject(),
                EasyMock.anyInt(), EasyMock.anyInt());
        EasyMock.expectLastCall().atLeastOnce();
        mock.endElement(EasyMock.eq(""), EasyMock.eq("root"), EasyMock.eq("root"));
        mock.endDocument();
        EasyMock.replay(mock);

        parser.setContentHandler(mock);
        InputStream in = new ByteArrayInputStream(doc.getBytes("UTF-8"));
        parser.parse(new InputSource(in));

        EasyMock.verify(mock);
    }
}

Phép kiểm thử này cho thấy một số thủ thuật mới. Trước tiên, nó sử dụng chế độ giả nghiêm ngặt, vì vậy cần phải có một trình tự. Ví dụ, bạn không muốn trình phần tích cú pháp gọi phương thức endDocument() trước khi gọi phương thức startDocument().

Tiếp theo, các phương thức tôi đang kiểm thử đều trả về giá trị rỗng (void). Điều đó có nghĩa là tôi không thể chuyển chúng dưới dạng các đối số của phương thức EasyMock.expect(), như tôi đã làm với phương thức getRate(). (EasyMock đánh lừa trình biên dịch về rất nhiều điều, nhưng nó không đủ thông minh để đánh lừa trình biên dịch tin rằng giá trị rỗng là một kiểu đối số hợp pháp). Thay vào đó, tôi chỉ cần gọi phương thức rỗng trên đối tượng giả và EasyMock thu kết quả. Nếu tôi cần thay đổi một số chi tiết của các kết quả mong đợi, thì sau đó tôi gọi phương thức EasyMock.expectLastCall() ngay sau khi gọi phương thức làm giả. Bạn cũng phải lưu ý rằng bạn không thể chuyển bất kỳ các giá trị kiểu chuỗi và các giá trị nguyên và các mảng như là đối số mong đợi. Trước tiên tất cả các giá trị đó phải được bọc lại với phương thức EasyMock.eq() để các giá trị của chúng có thể được thu giữ như mong đợi.

Liệt kê 8 sử dụng phương thức EasyMock.expectLastCall() để điều chỉnh số lần mà các phương thức có thể được gọi. Theo mặc định, mỗi phương thức chỉ được gọi một lần. Tuy nhiên, tôi làm cho phương thức setDocumentLocator() thành tùy chọn bằng cách đặt số lần gọi .times(0, 1). Điều này có nghĩa là phương thức đó sẽ được gọi từ 0 đến 1 lần. Tất nhiên, bạn có thể thay đổi các đối số để cho các phương thức được gọi từ 1-10 lần, 3-30 lần, hoặc bất kỳ vùng giá trị nào mà bạn muốn. Đối với phương thức characters(), tôi thực sự không biết bao nhiêu lần nó sẽ được gọi, ngoại trừ việc nó phải được gọi ít nhất một lần, vì vậy tôi đặt là .atLeastOnce(). Nếu đây là một phương thức không trả về rỗng, thì tôi đã có thể áp dụng times(0, 1) và atLeastOnce() để đặt giá trị mong đợi một cách trực tiếp. Tuy nhiên, do các phương thức được làm giả trả về giá trị rỗng, nên tôi phải nhằm đến chúng với phương thức EasyMock.expectLastCall() để thay thế.

Cuối cùng, bạn hãy lưu ý việc sử dụng các phương thức EasyMock.anyObject() và EasyMock.anyInt() cho các đối số của phương thức characters(). Việc này tính toán đến nhiều cách khác nhau mà trình phân tích cú pháp được phép làm để chuyển văn bản vào phương thức ContentHandler.


Các đối tượng giả và thực tế

EasyMock có đáng giá không? Nó không làm được bất cứ cái gì mà khi viết các lớp giả một cách thủ công không thể thực hiện được, và trong trường hợp các lớp được viết theo cách thủ công thì dự án của bạn sẽ bớt đi một hay hai phần phụ thuộc. Ví dụ: liệt kê 3 là một trường hợp, ở đây đối tượng giả được viết một cách thủ công bằng cách sử dụng một lớp vô danh bên trong hầu như cũng nhỏ gọn như thế và có thể là dễ đọc hơn đối với các nhà phát triển phần mềm chưa quen với EasyMock. Tuy nhiên, đó là một ví dụ ngắn mà tôi chọn có chủ ý cho mục đích của một bài viết. Khi ta làm giả các giao diện lớn hơn chẳng hạn như org.w3c.dom.Node (có 25 phương thức), hoặc java.sql.ResultSet (có 139 phương thức và đang tăng lên), thì EasyMock là một công cụ tiết kiệm thời gian rất lớn, nó sản xuất ra những mã ngắn hơn nhiều và dễ đọc hơn với chi phí tối thiểu.

Bây giờ là lời cảnh báo: Các đối tượng giả có thể được sử dụng quá đà. Ta có thể làm giả rất nhiều thứ đến nỗi một phép thử luôn luôn thành công ngay cả khi mã bị hỏng nghiêm trọng. Bạn càng tạo nhiều đối tượng giả, thì bạn kiểm thử càng ít. Nhiều lỗi tồn tại trong các thư viện phụ thuộc và trong sự tương tác giữa một phương thức và các phương thức mà nó gọi đến. Làm giả các phần phụ thuộc có thể che giấu rất nhiều lỗi mà đúng ra bạn phải thực sự tìm ra. Việc tạo đối tượng giả không nên là lựa chọn đầu tiên của bạn trong bất kỳ tình huống nào. Nếu bạn có thể sử dụng một phụ thuộc thực, thì bạn hãy làm như vậy. Một đối tượng giả là một sự thay thế kém hơn cho một lớp thực. Tuy nhiên, nếu bạn không thể kiểm thử một cách đáng tin cậy và tự động với các lớp thực vì bất cứ lý do gì, thì kiểm thử bằng một đối tượng giả tốt hơn nhiều là không kiểm thử gì cả.

Tài nguyên

Học tập

Lấy sản phẩm và công nghệ

  • EasyMock: Cài đặt hệ thống EasyMock.
  • cglib 2.1: Thư viện thao tác bytecode mã nguồn mở mà bạn sẽ cần cài đặt để sử dụng phần mở rộng về lớp của EasyMock.

Thảo luận

Bình luận

developerWorks: Đăng nhập

Các trường được đánh dấu hoa thị là bắt buộc (*).


Bạn cần một ID của IBM?
Bạn quên định danh?


Bạn quên mật khẩu?
Đổi mật khẩu

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Khi bạn đăng ký với trang developerWorks lần đầu tiên, một tiểu sử của của bạn được tạo ra. Chọn các thông tin về tiểu sử của bạn (tên, nước/vùng, và nơi làm việc) đã được hiện lên màn hình, thông tin này sẽ được hiện kèm với nội dung mà bạn đăng tải. Bạn có thể cập nhật thông tin này bất kỳ lúc nào.

Thông tin gửi đi được đảm bảo an toàn.

Chọn tên hiển thị của bạn



Lần đầu tiên bạn đăng nhập vào trang developerWorks, một bản trích ngang được tạo ra cho bạn, bạn cần phải chọn một tên để hiển thị. Tên hiển thị của bạn sẽ đi kèm theo các nội dung mà bạn đăng tải trên developerWorks.

Tên hiển thị cần có từ 3 đến 30 ký tự. Tên xuất hiện của bạn phải là duy nhất trên trang Cộng đồng developerWorks và vì lí do an ninh nó không phải là địa chỉ email của bạn.

Các trường được đánh dấu hoa thị là bắt buộc (*).

(Tên hiển thị cần có từ 3 đến 30 ký tự)

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Thông tin gửi đi được đảm bảo an toàn.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=70
Zone=Công nghệ Java, Nguồn mở
ArticleID=494209
ArticleTitle=Kiểm thử dễ dàng hơn với EasyMock
publish-date=05282010