CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-com-google-truth--truth

Fluent assertion framework for Java that provides clear, readable test assertions and informative failure messages

Pending
Overview
Eval results
Files

custom-assertions.mddocs/

Custom Assertions and Extensions

Extension mechanisms for creating custom subject types, correspondence-based comparisons, and advanced assertion patterns.

Capabilities

Subject Factory Pattern

The primary mechanism for extending Truth with custom subjects.

/**
 * Given a factory for some Subject class, returns a builder whose that(actual) method 
 * creates instances of that class.
 * @param factory factory for creating custom subjects
 */
public static <S extends Subject, T> SimpleSubjectBuilder<S, T> assertAbout(Subject.Factory<S, T> factory);

/**
 * Factory interface for creating Subject instances.
 */
public interface Subject.Factory<SubjectT extends Subject, ActualT> {
    /**
     * Creates a new Subject.
     * @param metadata failure metadata for context  
     * @param actual the value under test
     */
    SubjectT createSubject(FailureMetadata metadata, ActualT actual);
}

Custom Subject Example:

// Define a custom subject for Person objects
public class PersonSubject extends Subject {
    private final Person actual;
    
    protected PersonSubject(FailureMetadata metadata, Person actual) {
        super(metadata, actual);
        this.actual = actual;
    }
    
    // Custom assertion methods
    public void hasName(String expectedName) {
        check("getName()").that(actual.getName()).isEqualTo(expectedName);
    }
    
    public void hasAge(int expectedAge) {
        check("getAge()").that(actual.getAge()).isEqualTo(expectedAge);
    }
    
    public void isAdult() {
        check("getAge()").that(actual.getAge()).isAtLeast(18);
    }
    
    // Factory for creating PersonSubject instances
    public static Subject.Factory<PersonSubject, Person> persons() {
        return PersonSubject::new;
    }
}

// Usage
Person person = new Person("Alice", 25);
assertAbout(persons()).that(person).hasName("Alice");
assertAbout(persons()).that(person).hasAge(25);
assertAbout(persons()).that(person).isAdult();

Custom Subject Builder Pattern

Advanced extension mechanism for complex custom subjects.

/**
 * A generic, advanced method of extension of Truth to new types.
 * @param factory factory for creating custom subject builders
 */
public static <CustomSubjectBuilderT extends CustomSubjectBuilder> CustomSubjectBuilderT assertAbout(
    CustomSubjectBuilder.Factory<CustomSubjectBuilderT> factory);

/**
 * Base class for advanced custom subject builders.
 */
public abstract class CustomSubjectBuilder {
    /**
     * Factory interface for creating CustomSubjectBuilder instances.
     */
    public interface Factory<CustomSubjectBuilderT extends CustomSubjectBuilder> {
        /**
         * Creates a new CustomSubjectBuilder.
         * @param metadata failure metadata for context
         */
        CustomSubjectBuilderT createSubjectBuilder(FailureMetadata metadata);
    }
}

Correspondence-Based Comparisons

Flexible comparison mechanism for custom element matching logic.

/**
 * Abstract class defining custom comparison logic between actual and expected elements.
 */
public abstract class Correspondence<A, E> {
    /**
     * Returns true if the actual and expected elements correspond according to this correspondence.
     * @param actual the actual element
     * @param expected the expected element
     */
    public abstract boolean compare(A actual, E expected);
    
    /**
     * Returns a description of the difference between actual and expected elements, or null.
     * @param actual the actual element
     * @param expected the expected element
     */
    public String formatDiff(A actual, E expected) {
        return null; // Default implementation
    }
}

Static Factory Methods for Correspondence

/**
 * Returns a Correspondence that compares elements using the given binary predicate.
 * @param predicate the binary predicate for comparison
 * @param description human-readable description of the correspondence
 */
public static <A, E> Correspondence<A, E> from(BinaryPredicate<A, E> predicate, String description);

/**
 * Returns a Correspondence that compares actual elements to expected elements by 
 * transforming the actual elements using the given function.
 * @param actualTransform function to transform actual elements
 * @param description human-readable description of the transformation
 */
public static <A, E> Correspondence<A, E> transforming(Function<A, E> actualTransform, String description);

/**
 * Returns a Correspondence for comparing Double values with the given tolerance.
 * @param tolerance the maximum allowed difference
 */
public static Correspondence<Double, Double> tolerance(double tolerance);

Correspondence Examples:

import com.google.common.truth.Correspondence;
import java.util.function.Function;

// Case-insensitive string comparison
Correspondence<String, String> CASE_INSENSITIVE = 
    Correspondence.from(String::equalsIgnoreCase, "ignoring case");

List<String> actualNames = Arrays.asList("Alice", "BOB", "charlie");
List<String> expectedNames = Arrays.asList("alice", "bob", "Charlie");

assertThat(actualNames)
    .comparingElementsUsing(CASE_INSENSITIVE)
    .containsExactlyElementsIn(expectedNames);

// Transform-based comparison
Correspondence<Person, String> BY_NAME = 
    Correspondence.transforming(Person::getName, "by name");

List<Person> people = Arrays.asList(new Person("Alice"), new Person("Bob"));
assertThat(people)
    .comparingElementsUsing(BY_NAME)
    .containsExactly("Alice", "Bob");

// Numeric tolerance
Correspondence<Double, Double> TOLERANCE = Correspondence.tolerance(0.01);
List<Double> measurements = Arrays.asList(1.001, 2.002, 3.003);
List<Double> expected = Arrays.asList(1.0, 2.0, 3.0);

assertThat(measurements)
    .comparingElementsUsing(TOLERANCE)
    .containsExactlyElementsIn(expected);

Enhanced Correspondence with Diff Formatting

/**
 * Returns a new Correspondence that formats diffs using the given formatter.
 * @param formatter the formatter for creating diff descriptions
 */
public Correspondence<A, E> formattingDiffsUsing(DiffFormatter<? super A, ? super E> formatter);

/**
 * Interface for formatting differences between actual and expected values.
 */
@FunctionalInterface
public interface DiffFormatter<A, E> {
    /**
     * Returns a description of the difference between actual and expected values.
     * @param actual the actual value
     * @param expected the expected value
     */
    String formatDiff(A actual, E expected);
}

Diff Formatting Example:

// Custom correspondence with detailed diff formatting
Correspondence<Person, Person> BY_PERSON_FIELDS = 
    Correspondence.from((actual, expected) -> 
        Objects.equals(actual.getName(), expected.getName()) &&
        Objects.equals(actual.getAge(), expected.getAge()), "by name and age")
    .formattingDiffsUsing((actual, expected) -> 
        String.format("expected: [name=%s, age=%d], but was: [name=%s, age=%d]",
            expected.getName(), expected.getAge(),
            actual.getName(), actual.getAge()));
            
List<Person> actual = Arrays.asList(new Person("Alice", 25));
List<Person> expected = Arrays.asList(new Person("Alice", 30));

// This will provide detailed diff information in the failure message
assertThat(actual)
    .comparingElementsUsing(BY_PERSON_FIELDS)
    .containsExactlyElementsIn(expected);

Custom Failure Messages with Facts

/**
 * Returns a new subject builder with custom failure message context.
 * @param messageToPrepend message to prepend to failure messages
 */
public StandardSubjectBuilder withMessage(String messageToPrepend);

/**
 * Class representing structured failure information.
 */
public final class Fact {
    /**
     * Creates a fact with a key and value.
     * @param key the fact key
     * @param value the fact value
     */
    public static Fact fact(String key, Object value);
    
    /**
     * Creates a fact with only a key (no value).
     * @param key the fact key
     */
    public static Fact simpleFact(String key);
}

Custom Subject with Rich Failure Messages:

public class PersonSubject extends Subject {
    private final Person actual;
    
    protected PersonSubject(FailureMetadata metadata, Person actual) {
        super(metadata, actual);
        this.actual = actual;
    }
    
    public void hasValidEmail() {
        if (actual.getEmail() == null || !isValidEmail(actual.getEmail())) {
            failWithActual(
                fact("expected", "valid email address"),
                fact("but email was", actual.getEmail()),
                simpleFact("valid email format: user@domain.com")
            );
        }
    }
    
    private boolean isValidEmail(String email) {
        return email.contains("@") && email.contains(".");
    }
}

Extension Best Practices

Creating Domain-Specific Subjects:

// Subject for HTTP Response objects
public class HttpResponseSubject extends Subject {
    private final HttpResponse actual;
    
    protected HttpResponseSubject(FailureMetadata metadata, HttpResponse actual) {
        super(metadata, actual);
        this.actual = actual;
    }
    
    public void hasStatus(int expectedStatus) {
        check("getStatus()").that(actual.getStatus()).isEqualTo(expectedStatus);
    }
    
    public void isSuccessful() {
        int status = actual.getStatus();
        if (status < 200 || status >= 300) {
            failWithActual(
                fact("expected", "successful status (200-299)"),
                fact("but status was", status)
            );
        }
    }
    
    public void hasHeader(String name, String value) {
        check("getHeader(" + name + ")")
            .that(actual.getHeader(name))
            .isEqualTo(value);
    }
    
    public StringSubject hasBodyThat() {
        return check("getBody()").that(actual.getBody());
    }
    
    // Factory method
    public static Subject.Factory<HttpResponseSubject, HttpResponse> httpResponses() {
        return HttpResponseSubject::new;
    }
}

// Usage
HttpResponse response = makeHttpRequest();
assertAbout(httpResponses()).that(response).isSuccessful();
assertAbout(httpResponses()).that(response).hasStatus(200);
assertAbout(httpResponses()).that(response).hasHeader("Content-Type", "application/json");
assertAbout(httpResponses()).that(response).hasBodyThat().contains("success");

Types

/**
 * Base class for all Truth subjects providing common assertion methods.
 */
public class Subject {
    /**
     * Constructor for use by subclasses.
     * @param metadata failure metadata containing context information
     * @param actual the value under test
     */
    protected Subject(FailureMetadata metadata, Object actual);
    
    /**
     * Factory interface for creating Subject instances.
     */
    public interface Factory<SubjectT extends Subject, ActualT> {
        SubjectT createSubject(FailureMetadata metadata, ActualT actual);
    }
    
    /**
     * Creates a derived subject for chaining assertions.
     * @param format format string for the derived subject description
     * @param args arguments for the format string
     */
    protected StandardSubjectBuilder check(String format, Object... args);
    
    /**
     * Reports a failure with the actual value and additional facts.
     * @param facts additional facts to include in the failure message
     */
    protected void failWithActual(Fact... facts);
}

/**
 * Abstract class defining custom comparison logic between actual and expected elements.
 */
public abstract class Correspondence<A, E> {
    // Methods documented above
}

/**
 * Base class for advanced custom subject builders.
 */
public abstract class CustomSubjectBuilder {
    /**
     * Constructor for CustomSubjectBuilder.
     * @param metadata failure metadata for context
     */
    protected CustomSubjectBuilder(FailureMetadata metadata);
    
    /**
     * Factory interface for creating CustomSubjectBuilder instances.
     */
    public interface Factory<CustomSubjectBuilderT extends CustomSubjectBuilder> {
        CustomSubjectBuilderT createSubjectBuilder(FailureMetadata metadata);
    }
}

/**
 * Class representing structured failure information.
 */
public final class Fact {
    // Methods documented above
}

/**
 * Functional interface for binary predicates used in Correspondence.from().
 */
@FunctionalInterface
public interface BinaryPredicate<A, E> {
    /**
     * Applies this predicate to the given arguments.
     * @param actual the actual value
     * @param expected the expected value
     */
    boolean apply(A actual, E expected);
}

/**
 * Interface for formatting differences between actual and expected values.
 */  
@FunctionalInterface
public interface DiffFormatter<A, E> {
    /**
     * Returns a description of the difference between actual and expected values.
     * @param actual the actual value
     * @param expected the expected value
     */
    String formatDiff(A actual, E expected);
}

Install with Tessl CLI

npx tessl i tessl/maven-com-google-truth--truth

docs

array-assertions.md

collection-assertions.md

core-assertions.md

custom-assertions.md

exception-assertions.md

index.md

java8-assertions.md

map-assertions.md

numeric-assertions.md

string-assertions.md

testing-utilities.md

tile.json