CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-nl-jqno-equalsverifier--equalsverifier

Java library for verifying the contract of equals and hashCode methods in unit tests

Pending
Overview
Eval results
Files

relaxed-equality-api.mddocs/

Relaxed Equality API

Specialized verification for classes with relaxed equality rules where multiple instances can be equal despite different internal state. This is common in normalized representations, value objects, and classes that implement canonical forms.

Capabilities

Relaxed Equal Examples

Creates a verifier for classes where multiple distinct instances can be equal to each other.

/**
 * Factory method. Asks for a list of equal, but not identical, instances of T.
 * 
 * For use when T is a class which has relaxed equality rules. This happens when two
 * instances of T are equal even though the its internal state is different.
 * 
 * This could happen, for example, in a Rational class that doesn't normalize: new
 * Rational(1, 2).equals(new Rational(2, 4)) would return true.
 * 
 * Using this factory method requires that andUnequalExamples be called to supply a list of
 * unequal instances of T.
 * 
 * This method automatically suppresses ALL_FIELDS_SHOULD_BE_USED.
 * 
 * @param first An instance of T
 * @param second Another instance of T, which is equal, but not identical, to first
 * @param more More instances of T, all of which are equal, but not identical, to one another
 *     and to first and second
 * @return A fluent API for a more relaxed EqualsVerifier
 */
@SafeVarargs
public static <T> RelaxedEqualsVerifierApi<T> forRelaxedEqualExamples(
    T first,
    T second,
    T... more
);

Usage Examples:

import nl.jqno.equalsverifier.EqualsVerifier;

// Example with rational numbers that don't normalize
Rational oneHalf = new Rational(1, 2);
Rational twoQuarters = new Rational(2, 4);
Rational fourEighths = new Rational(4, 8);

EqualsVerifier.forRelaxedEqualExamples(oneHalf, twoQuarters, fourEighths)
    .andUnequalExamples(new Rational(1, 3), new Rational(3, 4))
    .verify();

// Example with normalized string representations
NormalizedString str1 = new NormalizedString("  Hello World  ");
NormalizedString str2 = new NormalizedString("hello world");
NormalizedString str3 = new NormalizedString("HELLO WORLD");

EqualsVerifier.forRelaxedEqualExamples(str1, str2, str3)
    .andUnequalExamples(new NormalizedString("Different Text"))
    .verify();

Adding Unequal Examples

Provides unequal examples to complete the relaxed verification setup.

/**
 * Provides single unequal example and returns SingleTypeEqualsVerifierApi
 * @param example An instance of T that is not equal to any of the equal examples
 * @return A SingleTypeEqualsVerifierApi for further configuration and verification
 */
public SingleTypeEqualsVerifierApi<T> andUnequalExample(T example);

/**
 * Provides multiple unequal examples and returns SingleTypeEqualsVerifierApi
 * @param first An instance of T that is not equal to any of the equal examples
 * @param more More instances of T that are not equal to any of the equal examples
 * @return A SingleTypeEqualsVerifierApi for further configuration and verification
 */
@SafeVarargs
public final SingleTypeEqualsVerifierApi<T> andUnequalExamples(T first, T... more);

Usage Examples:

// Single unequal example
EqualsVerifier.forRelaxedEqualExamples(oneHalf, twoQuarters)
    .andUnequalExample(new Rational(1, 3))
    .verify();

// Multiple unequal examples
EqualsVerifier.forRelaxedEqualExamples(oneHalf, twoQuarters, fourEighths)
    .andUnequalExamples(
        new Rational(1, 3), 
        new Rational(3, 4), 
        new Rational(2, 3)
    )
    .verify();

// With additional configuration
EqualsVerifier.forRelaxedEqualExamples(normalizedStr1, normalizedStr2)
    .andUnequalExamples(differentStr1, differentStr2)
    .suppress(Warning.NONFINAL_FIELDS)
    .verify();

Common Use Cases

Rational Number Classes:

public class Rational {
    private final int numerator;
    private final int denominator;
    
    public Rational(int numerator, int denominator) {
        // Note: This implementation doesn't normalize fractions
        this.numerator = numerator;
        this.denominator = denominator;
    }
    
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Rational)) return false;
        Rational other = (Rational) obj;
        // Cross multiplication to check equality without normalization
        return this.numerator * other.denominator == other.numerator * this.denominator;
    }
    
    @Override
    public int hashCode() {
        // Normalize for consistent hash codes
        int gcd = gcd(numerator, denominator);
        return Objects.hash(numerator / gcd, denominator / gcd);
    }
}

// Test with relaxed equality
@Test
public void testRationalEquals() {
    Rational oneHalf = new Rational(1, 2);
    Rational twoQuarters = new Rational(2, 4);
    Rational threeHalves = new Rational(3, 6);  // Actually equals 1/2
    
    EqualsVerifier.forRelaxedEqualExamples(oneHalf, twoQuarters, threeHalves)
        .andUnequalExamples(new Rational(1, 3), new Rational(2, 3))
        .verify();
}

Case-Insensitive String Wrappers:

public class CaseInsensitiveString {
    private final String value;
    
    public CaseInsensitiveString(String value) {
        this.value = value;
    }
    
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof CaseInsensitiveString)) return false;
        CaseInsensitiveString other = (CaseInsensitiveString) obj;
        return this.value.equalsIgnoreCase(other.value);
    }
    
    @Override
    public int hashCode() {
        return value.toLowerCase().hashCode();
    }
}

// Test with relaxed equality
@Test
public void testCaseInsensitiveStringEquals() {
    CaseInsensitiveString lower = new CaseInsensitiveString("hello");
    CaseInsensitiveString upper = new CaseInsensitiveString("HELLO");
    CaseInsensitiveString mixed = new CaseInsensitiveString("HeLLo");
    
    EqualsVerifier.forRelaxedEqualExamples(lower, upper, mixed)
        .andUnequalExamples(new CaseInsensitiveString("world"))
        .verify();
}

Normalized Path Classes:

public class NormalizedPath {
    private final String path;
    
    public NormalizedPath(String path) {
        this.path = path; // Store original, normalize in equals/hashCode
    }
    
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof NormalizedPath)) return false;
        NormalizedPath other = (NormalizedPath) obj;
        return normalize(this.path).equals(normalize(other.path));
    }
    
    @Override
    public int hashCode() {
        return normalize(this.path).hashCode();
    }
    
    private String normalize(String path) {
        return path.replaceAll("/+", "/")
                  .replaceAll("/\\./", "/")
                  .replaceAll("/$", "");
    }
}

// Test with relaxed equality
@Test
public void testNormalizedPathEquals() {
    NormalizedPath path1 = new NormalizedPath("/home/user/documents");
    NormalizedPath path2 = new NormalizedPath("/home//user/./documents/");
    NormalizedPath path3 = new NormalizedPath("/home/user/documents/");
    
    EqualsVerifier.forRelaxedEqualExamples(path1, path2, path3)
        .andUnequalExamples(new NormalizedPath("/home/user/downloads"))
        .verify();
}

URL Classes with Different Representations:

public class FlexibleUrl {
    private final String scheme;
    private final String host;
    private final Integer port;
    private final String path;
    
    public FlexibleUrl(String url) {
        // Parse URL and store components
        // This constructor could create different internal representations
        // for the same logical URL
    }
    
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof FlexibleUrl)) return false;
        FlexibleUrl other = (FlexibleUrl) obj;
        
        // Normalize comparison - default ports, case-insensitive hosts, etc.
        return normalizeScheme(this.scheme).equals(normalizeScheme(other.scheme)) &&
               normalizeHost(this.host).equals(normalizeHost(other.host)) &&
               normalizePort(this.port, this.scheme).equals(normalizePort(other.port, other.scheme)) &&
               normalizePath(this.path).equals(normalizePath(other.path));
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(
            normalizeScheme(scheme),
            normalizeHost(host),
            normalizePort(port, scheme),
            normalizePath(path)
        );
    }
}

// Test with relaxed equality
@Test
public void testFlexibleUrlEquals() {
    FlexibleUrl url1 = new FlexibleUrl("http://Example.COM:80/path/");
    FlexibleUrl url2 = new FlexibleUrl("HTTP://example.com/path");
    FlexibleUrl url3 = new FlexibleUrl("http://example.com:80/path/");
    
    EqualsVerifier.forRelaxedEqualExamples(url1, url2, url3)
        .andUnequalExamples(new FlexibleUrl("https://example.com/path"))
        .verify();
}

Advanced Configuration

Since RelaxedEqualsVerifierApi returns a SingleTypeEqualsVerifierApi, all single-class configuration options are available:

// Complex relaxed equality verification with full configuration
EqualsVerifier.forRelaxedEqualExamples(
        new ComplexRational(1, 2, metadata1),
        new ComplexRational(2, 4, metadata2),
        new ComplexRational(4, 8, metadata3)
    )
    .andUnequalExamples(new ComplexRational(1, 3, metadata4))
    .suppress(Warning.NONFINAL_FIELDS)
    .withPrefabValues(Metadata.class, redMetadata, blueMetadata)
    .withIgnoredFields("creationTime", "lastModified")
    .verify();

Important Notes

  1. Automatic Warning Suppression: The forRelaxedEqualExamples method automatically suppresses Warning.ALL_FIELDS_SHOULD_BE_USED because relaxed equality typically doesn't use all fields in the same way.

  2. Equal Examples Requirement: All examples provided to forRelaxedEqualExamples must be equal to each other according to the class's equals method.

  3. Unequal Examples Requirement: All examples provided to andUnequalExamples must not be equal to any of the equal examples.

  4. hashCode Consistency: The class must still maintain the contract that equal objects have equal hash codes, even if they have different internal representations.

Install with Tessl CLI

npx tessl i tessl/maven-nl-jqno-equalsverifier--equalsverifier

docs

configuration-api.md

index.md

multiple-class-api.md

relaxed-equality-api.md

single-class-api.md

warning-system.md

tile.json