Làm thế nào để một đơn vị kiểm tra hợp đồng hashCode-equals?


79

Tóm lại, hợp đồng hashCode, theo object.hashCode () của Java:

  1. Mã băm không nên thay đổi trừ khi có điều gì đó ảnh hưởng đến thay đổi bằng ()
  2. bằng () ngụ ý mã băm là ==

Hãy giả sử mối quan tâm chủ yếu đến các đối tượng dữ liệu bất biến - thông tin của chúng không bao giờ thay đổi sau khi chúng được xây dựng, vì vậy số 1 được giả định là giữ nguyên. Điều đó để lại # 2: vấn đề chỉ đơn giản là xác nhận rằng bằng có nghĩa là mã băm ==.

Rõ ràng, chúng tôi không thể kiểm tra mọi đối tượng dữ liệu có thể tưởng tượng được trừ khi tập hợp đó quá nhỏ. Vì vậy, cách tốt nhất để viết một bài kiểm tra đơn vị có khả năng nắm bắt các trường hợp phổ biến là gì?

Vì các thể hiện của lớp này là bất biến, nên có những cách hạn chế để xây dựng một đối tượng như vậy; kiểm tra đơn vị này nên bao gồm tất cả chúng nếu có thể. Ngoài ra, các điểm đầu vào là các hàm tạo, deserialization và các hàm tạo của các lớp con (điều này có thể được rút gọn đối với vấn đề gọi hàm tạo).

[Tôi sẽ cố gắng trả lời câu hỏi của chính mình thông qua nghiên cứu. Đầu vào từ các StackOverflowers khác là một cơ chế an toàn được hoan nghênh cho quá trình này.]

[Điều này có thể áp dụng cho các ngôn ngữ OO khác, vì vậy tôi đang thêm thẻ đó.]


1
Tôi thường thấy rằng hợp đồng đã bị phá vỡ do triển khai bằng hoặc mã băm, nhưng không phải cả hai. Có openpojo giúp trong một dự án Java. EqualsAndHashCodeMatchRule giúp làm điều đó. Các câu trả lời đã tồn tại cung cấp đủ chi tiết liên quan đến việc kiểm tra phần còn lại của hợp đồng.
Michiel Leegwater

Câu trả lời:


73

EqualsVerifier là một dự án mã nguồn mở tương đối mới và nó thực hiện rất tốt công việc kiểm tra hợp đồng bằng. Nó không có các vấn đề mà EqualsTester từ GSBase có. Tôi chắc chắn sẽ giới thiệu nó.


7
Chỉ sử dụng EqualsVerifier đã dạy cho tôi vài điều về Java!
David,

Tôi có điên không? EqualsVerifier dường như không kiểm tra ngữ nghĩa của Value Object! Nó cho rằng lớp của tôi triển khai đúng bằng equals (), nhưng lớp của tôi sử dụng hành vi "==" mặc định. Làm sao có thể?! Tôi phải thiếu một cái gì đó đơn giản.
JB Rainsberger

Aha! Nếu tôi không ghi đè bằng (), thì EqualsVerifier giả định rằng mọi thứ đều ổn !? Đó không phải là những gì tôi mong đợi. Tôi mong đợi hành vi tương tự để không ghi đè bằng equals () cũng như việc ghi đè bằng equals () để thực hiện chính xác điều tương tự super.equals (). Kỳ dị.
JB Rainsberger

7
@JBRainsberger Xin chào! Người tạo EqualsVerifier tại đây. Tôi xin lỗi vì bạn thấy nó khó hiểu. Re: not overriding equals: bạn có thể kích hoạt hành vi mong đợi của mình với "allFieldsShouldBeUsed ()". Đây sẽ là mặc định trong phiên bản tiếp theo, vì những lý do bạn nêu. Re: các trường hợp giống hệt nhau: Tôi không nghĩ mình có thể giúp bạn nếu không xem một số mã ví dụ. Bạn có thể giải thích thêm trong một câu hỏi mới hoặc trên danh sách gửi thư EV tại groups.google.com/forum/?fromgroups#!forum/equalsverifier không? Cảm ơn!
jqno

1
EqualsVerifier thực sự mạnh mẽ. Tôi đã thu được rất nhiều thời gian bằng cách không thử nghiệm từng tổ hợp tiềm năng của các đối tượng không bằng nhau. Các thông báo lỗi thực sự chính xác và mang tính hướng dẫn. Và trình xác minh rất có thể định cấu hình nếu quy ước mã hóa của bạn khác với kỳ vọng mặc định. Làm tốt lắm @jqno
gontard

11

Lời khuyên của tôi là hãy nghĩ về lý do tại sao / làm thế nào điều này có thể không đúng, và sau đó viết một số bài kiểm tra đơn vị nhằm vào những tình huống đó.

Ví dụ: giả sử bạn có một Setlớp tùy chỉnh . Hai tập hợp là bằng nhau nếu chúng chứa các phần tử giống nhau, nhưng có thể cấu trúc dữ liệu cơ bản của hai tập hợp bằng nhau khác nhau nếu các phần tử đó được lưu trữ theo một thứ tự khác nhau. Ví dụ:

MySet s1 = new MySet( new String[]{"Hello", "World"} );
MySet s2 = new MySet( new String[]{"World", "Hello"} );
assertEquals(s1, s2);
assertTrue( s1.hashCode()==s2.hashCode() );

Trong trường hợp này, thứ tự của các phần tử trong tập hợp có thể ảnh hưởng đến hàm băm của chúng, tùy thuộc vào thuật toán băm mà bạn đã triển khai. Vì vậy, đây là loại kiểm tra tôi sẽ viết, vì nó kiểm tra trường hợp mà tôi biết rằng một số thuật toán băm có thể tạo ra các kết quả khác nhau cho hai đối tượng mà tôi đã xác định là bằng nhau.

Bạn nên sử dụng một tiêu chuẩn tương tự với lớp tùy chỉnh của riêng bạn, bất kể đó là gì.


6

Nó đáng để sử dụng các addon junit cho việc này. Kiểm tra lớp EqualsHashCodeTestCase http://junit-addons.sourceforge.net/ bạn có thể mở rộng điều này và triển khai createInstance và createNotEqualInstance, điều này sẽ kiểm tra các phương thức equals và hashCode có đúng không.


4

Tôi muốn giới thiệu EqualsTester từ GSBase. Về cơ bản, nó thực hiện những gì bạn muốn. Tôi có hai vấn đề (nhỏ) với nó mặc dù:

  • Hàm tạo thực hiện tất cả công việc, điều mà tôi không coi là thực hành tốt.
  • Nó không thành công khi một thể hiện của lớp A bằng với một thể hiện của một lớp con của lớp A. Điều này không nhất thiết là vi phạm hợp đồng bằng.

2

[Tại thời điểm viết bài này, ba câu trả lời khác đã được đăng.]

Để nhắc lại, mục đích của câu hỏi của tôi là tìm ra các trường hợp kiểm tra tiêu chuẩn để xác nhận điều đó hashCodeequalsđồng ý với nhau. Cách tiếp cận của tôi đối với câu hỏi này là tưởng tượng ra các con đường phổ biến mà các lập trình viên thực hiện khi viết các lớp được đề cập, cụ thể là dữ liệu bất biến. Ví dụ:

  1. Viết equals()mà không viết hashCode(). Điều này thường có nghĩa là bình đẳng được định nghĩa có nghĩa là bình đẳng của các trường của hai trường hợp.
  2. Viết hashCode()mà không viết equals(). Điều này có thể có nghĩa là lập trình viên đang tìm kiếm một thuật toán băm hiệu quả hơn.

Trong trường hợp số 2, vấn đề dường như không tồn tại đối với tôi. Không có trường hợp bổ sung nào được thực hiện equals(), vì vậy không yêu cầu trường hợp bổ sung nào có mã băm bằng nhau. Tệ nhất, thuật toán băm có thể mang lại hiệu suất kém hơn cho các bản đồ băm, nằm ngoài phạm vi của câu hỏi này.

Trong trường hợp # 1, kiểm tra đơn vị tiêu chuẩn đòi hỏi phải tạo ra hai phiên bản của cùng một đối tượng với cùng một dữ liệu được truyền cho hàm tạo và xác minh các mã băm bằng nhau. Điều gì về dương tính giả? Có thể chọn các tham số hàm tạo chỉ xảy ra để mang lại các mã băm bằng nhau trên một thuật toán không liên kết. Một bài kiểm tra đơn vị có xu hướng tránh các tham số như vậy sẽ đáp ứng tinh thần của câu hỏi này. Cách tắt ở đây là kiểm tra mã nguồn equals(), suy nghĩ kỹ và viết một bài kiểm tra dựa trên đó, nhưng mặc dù điều này có thể cần thiết trong một số trường hợp, nhưng cũng có thể có những bài kiểm tra phổ biến bắt gặp các vấn đề chung - và những bài kiểm tra như vậy cũng đáp ứng tinh thần của câu hỏi này.

Ví dụ: nếu lớp được kiểm tra (gọi nó là Dữ liệu) có một phương thức khởi tạo nhận một Chuỗi và các cá thể được xây dựng từ Chuỗi là các equals()cá thể được sinh ra đã có equals(), thì một bài kiểm tra tốt có thể sẽ kiểm tra:

  • new Data("foo")
  • khác new Data("foo")

Chúng tôi thậm chí có thể kiểm tra mã băm new Data(new String("foo"))để buộc Chuỗi không bị xen vào, mặc dù điều đó có nhiều khả năng mang lại mã băm chính xác hơn Data.equals()là mang lại kết quả chính xác, theo ý kiến ​​của tôi.

Câu trả lời của Eli Courtwright là một ví dụ về việc nghĩ ra cách phá vỡ thuật toán băm dựa trên kiến ​​thức về equalsđặc điểm kỹ thuật. Ví dụ về một bộ sưu tập đặc biệt là một ví dụ hay, vì đôi khi những thứ do người dùng tạo xuất hiện Collectionvà khá dễ bị sai sót trong thuật toán băm.


1

Đây là một trong những trường hợp duy nhất mà tôi có nhiều xác nhận trong một bài kiểm tra. Vì bạn cần kiểm tra phương thức bằng, bạn cũng nên kiểm tra phương thức Mã băm cùng một lúc. Vì vậy, trên mỗi trường hợp kiểm tra phương thức bằng nhau của bạn, hãy kiểm tra hợp đồng Mã băm.

A one = new A(...);
A two = new A(...);
assertEquals("These should be equal", one, two);
int oneCode = one.hashCode();
assertEquals("HashCodes should be equal", oneCode, two.hashCode());
assertEquals("HashCode should not change", oneCode, one.hashCode());

Và tất nhiên kiểm tra mã băm tốt là một bài tập khác. Thành thật mà nói, tôi sẽ không bận tâm đến việc kiểm tra kỹ lưỡng để đảm bảo Mã băm không thay đổi trong cùng một lần chạy, loại vấn đề đó sẽ được xử lý tốt hơn bằng cách bắt nó trong một bài đánh giá mã và giúp nhà phát triển hiểu tại sao đó không phải là cách tốt để viết các phương thức hashCode.



0

Nếu tôi có một lớp học Thing, như hầu hết những người khác, tôi viết một lớp học ThingTest, lớp này chứa tất cả các bài kiểm tra đơn vị cho lớp đó. Mỗi người ThingTestcó một phương pháp

 public static void checkInvariants(final Thing thing) {
    ...
 }

và nếu Thinglớp ghi đè Mã băm và bằng thì nó có một phương thức

 public static void checkInvariants(final Thing thing1, Thing thing2) {
    ObjectTest.checkInvariants(thing1, thing2);
    ... invariants that are specific to Thing
 }

Phương pháp đó chịu trách nhiệm kiểm tra tất cả các bất biến được thiết kế để giữ giữa bất kỳ cặp Thingđối tượng nào. Các ObjectTestphương pháp đó đại biểu có trách nhiệm kiểm tra tất cả các bất biến mà phải giữ giữa bất kỳ cặp của các đối tượng. Như equalshashCodelà các phương thức của tất cả các đối tượng, phương thức đó sẽ kiểm tra hashCodeequalsnhất quán.

Sau đó, tôi có một số phương pháp thử nghiệm tạo các cặp Thingđối tượng và chuyển chúng sang checkInvariantsphương thức theo cặp . Tôi sử dụng phân vùng tương đương để quyết định cặp nào đáng thử nghiệm. Tôi thường tạo mỗi cặp để chỉ khác nhau ở một thuộc tính, cộng với một bài kiểm tra kiểm tra hai đối tượng tương đương.

Đôi khi tôi cũng có checkInvariantsphương pháp 3 đối số , mặc dù tôi thấy rằng phương pháp đó ít hữu ích hơn trong các lỗi tìm kiếm, vì vậy tôi không làm điều này thường xuyên


Điều này tương tự với những gì một poster đề cập ở đây: stackoverflow.com/a/190989/545127
Raedwald
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.