I'm writing tests for a personal project (in Java17 using JUnit5/Jupiter and Google Truth ) where I use the multiple-inheritance of interfaces to define classes. For example:
public interface Taggable {
public String getTagName();
public String getSimpleContent();
// ... and more
}
public interface CreatureContainer {
public Collection<Creature> getCreatures();
public boolean addCreature(Creature c);
public void announce(String message);
// ... and more
}
public class Area implements Taggable, CreatureContainer { // possibly more
private String name;
private Set<Creature> creatures;
// Constructor etc
@Override
public String getTagName() {
return this.getClass().getName();
}
@Override
public String getSimpleContent() {
return this.name;
}
@Override
public Collection<Creature> getCreatures() {
return creatures;
}
// ...etc
}
As the Google Truth Subject class is a CLASS and there's no interfaces to use to mirror my structure, I cannot write something like:
public class TaggableSubject extends Subject {};
public class CreatureContainerSubject extends Subject {};
public class AreaSubject extends TaggableSubject, CreatureContainerSubject {}; // ERROR!!
I can see a few options:
I write one Subject
for each of Taggable
, CreatureContainer
, and Area
. In the AreaSubject
I include methods such as
public TaggableSubject asTaggable() {
return check("this").that(actual); // just give it a raw actual?
}
public CreatureContainerSubject asCreatureContainer() {
return check("this").that(actual);
}
I only write the AreaSubject
, and include a method for each thing in from the inherited interfaces.
Such as:
public StringSubject tagName() {
return check("getTagName()").that(actual.getTagName());
}
// or similarly
public void hasTagName(String tagName) {
this.tagName().isEqualTo(tagName);
}
public void hasCreature(Creature c) {
check("getCreatures()").that(actual.getCreatures()).contains(c);
}
To keep moving forward, I'm going to go with the first option, but I'd still like to know:
Which of the options is more true to the Google Truth paradigm? Or is there a better option?
Thank you.
I don't remember any specific precedent for this, but I would say:
TaggableSubject
, CreatureContainerSubject
, and AreaSubject
and for AreaSubject
to contain all the methods from each of its supertypes. (Of course, as you say, it can extend at most one of the other types, so you need to implement at least some assertion methods with delegation.)Taggable
instances or CreatureContainer
instances that aren't Area
instances, then there's no need for anything except AreaSubject
.AreaSubject
very often, then the pragmatic choice might be to implement Option 1, which requires less code.If you do go for the ideal user experience, then you can optionally avoid duplicating the implementations of the assertion methods into AreaSubject
by delegating to your other Subject
implementations:
public class AreaSubject extends Subject {
public void hasCreature(Creature c) {
check("this").about(creatureContainers()).that(actual).hasCreature(c);
}
...
}
That doesn't actually simplify the code if the implementations are already simple. And it actually makes the failure message slightly worse because it makes the message include "this
." (We've considered providing a way to avoid that, but we haven't done so. For now, we employ hacks where we want it.) But if you have complex implementations that you want to delegate to, then it can be handy.
If you're curious: I'd mentioned precedent:
PathSubject
, which could extend either IterableSubject
or ComparableSubject
but which we made extend neither—and which we made not declare any of the methods added by those classes! This would be annoying to users if they actually wanted to use Iterable
- or Comparable
-specific assertions on a Path
, so I wouldn't recommend it in your case. (In practice, it seems to work fine because users often don't think of Path
as Iterable
or Comparable
, at least not for purposes for asserting about it.)MultisetSubject
and an IterableOfProtosSubject
. That suggests that we should also have a MultisetOfProtosSubject
. And in fact, because we don't, users sometimes get build errors (from ambiguity about which of the two assertThat
methods to call) if they call assertThat(someMultisetOfProtos)
, and they have to change their code to avoid that (e.g., by casting the argument). This is the sort of trouble that you'd likely see if you offered assertThat(Taggable)
and assertThat(CreatureContainer)
but not assertThat(Area)
.(Sorry for letting this question sit an entire month.)