EqualsVerifier Documentation

repository·main·Indexed 20 days ago

https://github.com/jqno/equalsverifier

A Java library for unit tests to verify that the equals and hashCode methods of a class correctly implement the Java equality contract. It provides strict and lenient verification modes, tools for handling abstract delegation, BigDecimal equality, and JPMS accessibility, as well as a 'nodep' uberjar variant to avoid Byte Buddy dependency conflicts.

Tokens
27.7K
Snippets
88
Records
128
Agent score
69%

What's inside EqualsVerifier

  1. Compare `instanceof` vs `getClass()` for `equals` implementations

    main

    There are two primary patterns for type checking in an equals method:

    This approach is compatible with the Liskov substitution principle and works well with frameworks like Hibernate or Mockito that use bytecode manipulation to create subclasses.

    public boolean equals(Object obj) {
        if (!(obj instanceof Foo)) return false;
        // ...
    }

    Note: instanceof handles null checks automatically.

    2. Using getClass()

    This approach ensures that only objects of the exact same class are considered equal. However, it causes equals to return false when comparing a base class instance to a subclass instance, even if the subclass adds no new state.

    public boolean equals(Object obj) {
        if (obj == null || getClass() != obj.getClass()) return false;
        // ...
    }
    // instanceof pattern
    public boolean equals(Object obj) {
        if (!(obj instanceof Foo)) return false;
        // ...
    }
    
    // getClass pattern
    public boolean equals(Object obj) {
        if (obj == null || getClass() != obj.getClass()) return false;
        // ...
    }
  2. How EqualsVerifier works internally

    main

    EqualsVerifier uses reflection and bytecode manipulation to test your classes without requiring you to manually construct complex test data:

    1. Instantiation: It creates an instance of your class without calling its constructor (similar to mocking frameworks), resulting in an object where all fields are 0 or null.
    2. Subclassing: If the class is not final, it generates a subclass for testing purposes.
    3. Field Injection: It invents values for all fields and assigns them using reflection.
    4. Permutation Testing: It calls equals and hashCode repeatedly across various permutations of these objects to validate expected behavior.
    5. Signature Inspection: It uses reflection to confirm the equals method signature is a proper override.
  3. Providing prefab values for ignored fields

    main

    If you use #withIgnoredFields(), EqualsVerifier may still request prefab values for those specific fields.

    This happens because EqualsVerifier performs two additional checks on ignored fields:

    1. It ensures that these fields cannot cause NullPointerExceptions during equality checks.
    2. It verifies that the field indeed does not participate in the equals contract.

    To satisfy these checks, EqualsVerifier needs actual values to work with. If it cannot automatically generate them, you must provide them using the prefab value methods.

  4. Handle transient fields

    main

    Fields marked with the Java transient keyword or the JPA @Transient annotation are automatically ignored by EqualsVerifier, as these fields typically should not participate in equality.

    If these fields do participate in equals, EqualsVerifier will fail the test. You can suppress this specific check by suppressing Warning.TRANSIENT_FIELDS.

  5. Understand the limitations of EqualsVerifier (False Positives and Negatives)

    main

    EqualsVerifier is designed to verify standard equals and hashCode implementations, but it has specific limitations:

    • False Positives: It may not detect incorrect logic if the error only occurs in a very specific, niche part of the class's state space that the verifier does not traverse. You must write intentionally bad code to bypass its checks.
    • False Negatives: It may report errors on perfectly valid code that uses high-performance optimizations or complex internal tricks. A primary example is java.lang.String, which will fail verification because of its internal implementation details.

    If your code is failing verification but you are certain it is correct, you may be writing high-performance niche code or complex API-level code that exceeds the scope of standard verification.

  6. Configure EqualsVerifier for JPA entities

    main

    EqualsVerifier provides specialized support for JPA entities. When a class is marked with @Entity, @Embeddable, or @MappedSuperclass, EqualsVerifier automatically applies certain behaviors:

    • Implicitly suppresses Warning.NONFINAL_FIELDS: Since JPA entities are mutable by design, non-final fields are allowed.
    • Ignores finality constraints: It will not enforce that the class, equals, or hashCode methods are final, as this can interfere with Hibernate proxies.

    Note: Because these constraints are relaxed, your classes remain vulnerable to subclasses breaking the equals contract.

  7. How Kotlin data classes are handled

    main

    EqualsVerifier recognizes that equality in Kotlin data classes is based on primary constructor parameters. Consequently, it automatically ignores any other properties defined in the class body.

    Requirement: To enable this behavior, the org.jetbrains.kotlin:kotlin-reflect library must be present in your classpath. If it is not available, data classes will be treated like standard classes, and non-constructor properties will not be ignored.

    data class MyDataclass(val value: String) {
      val isEmpty = value.isEmpty()
    }
    // 'isEmpty' is automatically ignored by EqualsVerifier
  8. What properties does EqualsVerifier test?

    main

    EqualsVerifier performs an exhaustive check of your equals and hashCode implementations, covering:

    • The equals contract: Reflexivity, Symmetry, Transitivity, Consistency, and Non-nullity.
    • Inheritance: Validates the five properties of the equals contract within an inheritance hierarchy.
    • The hashCode contract: Ensures hashCode is implemented correctly.
    • Field Consistency: Verifies that equals and hashCode are defined using the same set of fields.
    • Signature Validation: Checks that equals correctly overrides Object.equals(Object) rather than overloading it.
    • Immutability & Finality: Checks that fields are final (for consistency) and that the class or its methods are final (for symmetry and transitivity in hierarchies).
    • Type-specific handling: Ensures Arrays.equals/deepEquals are used for array fields and Float.compare/Double.compare are used for float and double fields.
  9. How EqualsVerifier handles inheritance and state

    main

    EqualsVerifier distinguishes between two types of subclasses to determine if symmetry is being violated:

    • Subclasses that add state: These classes add new fields. To maintain symmetry, these classes typically require a canEqual method so that a subclass instance can be compared correctly against other instances of the same type. In this model, a subclass instance is generally not equal to its superclass instance.
    • Subclasses that add only behavior: These classes do not add new fields. In this model, a subclass instance should be equal to its superclass instance if their state is identical.

    By default, EqualsVerifier assumes the latter (behavior-only) model. If your class follows the 'adds state' model, you must explicitly tell EqualsVerifier by calling .withRedefinedSuperclass().

  10. Understand mutation coverage limitations for hashCode

    main
    When using mutation testing tools like PITest, you should not expect 100% mutation coverage on hashCode methods. Mutation testing often changes operators (like * or +) in the hashCode implementation. Because hashCode is not strictly defined by its mathematical properties in a way that a test can easily catch every single operator mutation without 'overfitting' (pinning the hash to a specific value), 100% coverage is neither practical nor desirable.
  11. Limitations with non-standard equality branches

    main

    EqualsVerifier cannot achieve 100% code coverage if your equals method contains non-standard logic, such as branches based on specific field values (e.g., if (x == 42)) or external state (random numbers, environment variables).

    EqualsVerifier does not attempt to brute-force all possible values to trigger these branches. If your business logic requires these specific branches, you must test them manually using standard unit tests, as EqualsVerifier is designed to test the standard contract of equality rather than exhaustive value permutations.