Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 19 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
19
Dung lượng
209,98 KB
Nội dung
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ở Elliotte Rusty Harold, Giáo sư, Polytechnic University Tóm tắt: 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. 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) { [...]... 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) để... đố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ả Mục lục Giới thiệu về EasyMock Kiểm tra các ngoại lệ Thiết đặt các kết quả mong đợi Các đối tượng giả nghiêm ngặt và kiểm tra trình tự Các đối tượng giả... 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();... 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... 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... 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... 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... 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... 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ế... 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 . 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. để 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. tích và dễ đọc. 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ử,