Subject: Remove named(), actual(), and type parameters

Subject: Remove named(), actual(), and type parameters

Note: The decisions proposed in this document have already been implemented. We are publishing the document for any users who are interested in our thought process.

Background

Subject has 2 type parameters

Truth’s Subject class defines 2 type parameters:

/**
 * ...
 *
 * @param <S> the self-type, allowing {@code this}-returning methods to avoid needing subclassing
 * @param <T> the type of the object being tested by this {@code Subject} [i.e., the actual value]
 * ...
 */
public class Subject<S extends Subject<S, T>, T> {

(See a footnote note on <S>1.)

Each type parameter has one purpose:

This proposal (eventually, a few pages from now…) is to remove those type parameters and so, necessarily, to remove the 2 methods that use them. (OK, it’s not strictly necessary to remove the 2 methods, but we’ll discuss that later.)

Self-type parameters make subclassing hard

When you implement a Subject, you have to decide what to supply for the type parameters. Your options:

1. Specify concrete values for both type parameters

class ThrowableSubject extends Subject<ThrowableSubject, Throwable>

This is the most popular and convenient option. The problem arises when someone wants to subclass your subject:

a. Calling named on the subclass will return plain ThrowableSubject instead of the subtype. The subclass can override named, but few do.

b. It’s impossible to declare an appropriate Subject.Factory for the subtype. At best, you can declare a factory that accepts any Throwable (which might or might not be what you want) and returns a plain ThrowableSubject (unlikely to be what you want). Both of these are problems that we have inside Truth, and so do at least some other users. (And likely some other users wanted to make this work but couldn’t figure it out: The users who are defining such a factory are mostly doing so because I personally edited their code to define it.) Note that defining a factory and casting isn’t a convenient solution for users2, as any non-assert_() users of the subject (like check() or expectFailure()) have to ensure they pass the right argument type and cast the result.

I think we could half solve (b) by loosening the generics of Subject.Factory from:

<SubjectT extends Subject<SubjectT, ActualT>, ActualT>

to:

<SubjectT extends Subject<SubjectT, ?>, ActualT>

This would permit declaring a Subject.Factory that requires a more specific type for the actual value (e.g., Multiset instead of Iterable). However, it would still return the original subject type (IterableSubject, rather than MultisetSubject). And the subclass would also need to override named to solve (a).

2. Declare your own <S, T> parameters

class ComparableSubject<S extends ComparableSubject<S, T>, T extends Comparable>
    extends Subject<S, T>

This is the most flexible option (arguably, the correct option for extensible subjects), but:

a. It’s a mouthful. And keep in mind that subjects may have their own type parameters:

public abstract class TransformedValueSubject<
        S extends TransformedValueSubject<S, D, V, O>,
        D extends OriginalValueSubject.ValueDescription,
        V extends Value,
        O extends OriginalValueSubject<O, D, V>>
    extends TransformedSubject<S, D, V, O> {

Compare that to a version without <S, D> (and, as a result, also without <V>):

public abstract class TransformedValueSubject<O extends OriginalValueSubject>
    extends TransformedSubject<O> {

b. It’s not possible to define a Subject.Factory for the type itself. If you want users to be able to create a plain instance of your type, say, ViewSubject, then you need to declare an AbstractViewSubject with the type parameters plus a ViewSubject that extends AbstractViewSubject<ViewSubject, View>. If you do, the users see two types, with the assertions defined on a different type than the one they have an instance of3.

3. A hybrid option, where you specify a concrete actual-value type but declare a self-type parameter

class AbstractCheckedFutureSubject
        <S extends [AbstractCheckedFuture]Subject<S, CheckedFuture<?, ?>>>
    extends Subject<S, CheckedFuture<?, ?>>

This works well if all your subclasses are happy to accept a plain CheckedFuture (rather than having some subclasses that need to require a specific subclass of that); otherwise, not. Of course, it’s a mouthful, too. And you still need a non-abstract subclass if you want users to be able to create an instance of this plain type, as in 2(b).

4. Give up on extension

Maybe you give up on extending IterableSubject (or letting people extend your custom subject) entirely, or you ask for advice, or you realize on your own that you can delegate to IterableSubject instead of extending it, implementing methods wholesale or as needed.

Delegation is especially common with ThrowableSubject.

We see the IterableSubject problem in our own ProtoTruth (though the situation there is more complex).

Now, normally we favor composition over inheritance. However, this is a bit of a different case: Just as MyException extends Throwable, it’s generally reasonable for MyExceptionSubject to extend ThrowableSubject (if it weren’t blocked by these generics issues). And extension has advantages: Custom subjects pick up new methods from the superclass automatically, and they’re covered by any static analysis that finds issues like type mismatches. Plus, any custom methods added by the custom subject stand out from the default IterableSubject methods, which are defined in another file. Additionally, it’s (usually; there are other edge cases) possible to import a new assertThat method without breaking existing code, since assertThat(SubFoo) is likely to expose all the same assertions as assertThat(Foo).

(It is still reasonable for some subjects to choose not to extend an existing subject type, perhaps to limit the number of assertions they expose to a more tractable set. (For example, ProtoTruth doesn’t want to expose the no-arg isInOrder, since proto classes don’t define a natural order.) I’d just like for them to have a choice.)

Truth offers multiple ways to add to failure messages

There are a few, and there are likely to be more someday.

Truth currently provides 2 ways for the caller of an assertion to add to the assertion’s default failure message:

Under the old, prose-style failure messages, the messages these produce looked significantly different. Under the new, key-value-style failure messages, they look almost the same: Both put “username” on a new line at the beginning of the message. The only difference is that named prefixes it with “name: .”

In addition to those 2 ways, we recently added another way tailor-made for the specific case in which one assertion is being implemented in terms of another:

(Note that implementations of Subject classes naturally have influence over the failure message in other ways, thanks to other methods they can call and implement. I mention only check("username") here because it’s the most similar to named and assertWithMessage, and in fact people used named in place of check("username") before the latter existed. But keep in mind that there are plenty of existing options and future possibilities here, too4.)

On top of the existing ways for callers to add to the failure message, it’s likely that we’ll add some others in the future. For example, we’ve had several requests for adding context or scoping assertions. We’ve also speculated about a Fact-based method.

You could even argue that we have some other ways of supplying messages, like assert_().fail(...) and maybe someday Truth.fail(...) – and maybe even an assertThrows someday. And hopefully we’ll soon automatically infer a good description.

Some subjects fail to include the name passed to named in their failure messages

About half(!) of custom assertion methods omit it, and so do some assertions in Truth itself. The usual cause is a call to the no-arg check() method. These should someday be fixed, but I have automation for only about half the work, and we may need to add new APIs to support some callers, so the rest won’t happen anytime soon. (Other assertions drop all context, but that is easier to fix.)

Also note that, for most subjects that have subclasses, named doesn’t return the right type on the subclasses, thanks to the generics issues described above.

Issue A: Remove named

Users would use assertWithMessage (or withMessage) instead.

Another way that people may be interested in looking at this: What has changed since we originally added named? named was added in version 0.17 in 2014 before Truth became a Google project developed in our depot, so it didn’t go through API Review, but it’s still useful to consider what’s changed:

Issue B: Remove actual

Each subclass would have to declare a field of the appropriate type and store the actual value during its constructor. (It’s legal Java for every class in a hierarchy to declare a private field named actual.)

+  @Nullable private final Integer actual;
+
   protected IntegerSubject(FailureMetadata metadata, @Nullable Integer integer) {
     super(metadata, integer);
+    this.actual = integer;
   }

Issue C: Remove type parameters

I propose to remove both parameters. (Removing only one is much more difficult, as discussed above.)

To re-reiterate: This is the primary goal of all the proposals in this doc.

Again, it may be useful to review what has changed in the past several years. That includes considering how Truth is different from FEST (which seems to have been our inspiration for the type parameters). FEST uses the self type for chaining multiple assertions on the same value, as in assertThat(x).isNotNull().isNotEqualTo(other).contains(x); Truth does not. So even if FEST dropped its equivalent to named, that wouldn’t permit it to remove its self-type parameter. Truth has a better case for dropping it, and one of the original authors had considered doing so (though dropped the effort for reasons I’m unsure of – perhaps just that we had more pressing issues).

  1. “Avoid needing subclassing” might not be the best way to put this. The point is that we can declare a method that returns S, and we need to implement it only a single time in Subject itself. 

  2. self-nitpicking: OK, it’s also the type of the constructor parameter that subclasses call to pass that actual value to Subject. However, Subject uses the actual value only for assertions like isNotNull(), where it doesn’t need to know that it’s specifically a T. The T type specifically is in service of actual(), which is in service of subclasses. 

  3. It turns out that a full (OK, almost full) solution does exist. It hadn’t occurred to me, but some users found it. The solution is to use CustomSubjectBuilder. This lets you force the input to be of whatever type you want and lets you return whatever subject type you want. In short, because you can’t write:

    public static Factory<FooSubject, Foo> foos() {
      return FooSubject::new;
    }
    

    You instead write:

    public static CustomSubjectBuilder.Factory<FooSubjectBuilder> foos() {
      return FooSubjectBuilder::new;
    }
    
    public static final class FooSubjectBuilder extends CustomSubjectBuilder {
      FooSubjectBuilder(FailureMetadata metadata) {
        super(metadata);
      }
    
      public FooSubject that(Foo actual) {
        return new FooSubject(metadata(), actual);
      }
    }
    

    I am simultaneously horrified that this is necessary, impressed that some people found it, and tickled that CustomSubjectBuilder ended up satisfying this unforeseen use case. 

  4. You could also make ViewSubject the abstract type. Then you’d create a private ConcreteViewSubject subtype and a factory for the subtype. I think this will work, but you’ll have to expose the private ConcreteViewSubject type in the public views() method that exposes your factory, and then your assertThat method will either have to expose it again or else declare a return type of ViewSubject, which is a little weird in its own way. (And it gets worse if you have other type parameters that you want to survive a call to named, like what we used to have on IterableSubject.)