Assert but Verify
An example of the convenience of inheritance in Java gone wrong
Statue extends Bird
Please file this in the “Statue extends Bird
” category of inheritance lessons in Java. Consider this a cautionary tale of leveraging an inheritance relationship between utility classes purely for a convenience. It’s hard to remove Bird
once it has been extended as Statue
.
When we think of inheritance in Java, we usually think of classes that override instance methods from a parent class. The parent class may define some common attributes and behaviors, and the child class can add additional attributes and behaviors in addition to being able to use the parents attributes with a suitable scope defined and specialize the behaviors that were defined in the parent, also with a suitable scope defined.
We don’t usually think of inheriting static methods defined in utility classes. In this blog, I will share an example of a static utility class that extends another static utility class.
The Futility of Utility
Don’t create a static utility class that extends another static utility class. Even if the convenience feels overwhelmingly amazing, resist the temptation. If you are a library developer creating a static utility class, you can help prevent others from the potential pain by making the class final
.
I have written and spoken about what I call the futility of utility before, but this is a new kind of futility of a utility class. When you build Statue
and extend Bird
, you potentially accumulate $hi+ that you don’t define, but that you and your clients become increasingly dependent on. The Bird
doesn’t know about the Statue
. Over many years, it may unknowingly add new methods on the Statue
. Those methods harden over time so the Statue
may look like is an extension of the Bird
. The Statue
is not the Bird
, but the Statue
was designed to be Bird
.
The methods defined on the Bird
will probably be great, appropriate, and extremely useful. The Statue
extending the Bird
is not the decision of the Bird
, even if the Bird
allows itself to be extended. It is the decision of the designers of the Statue
. They have designed a BirdButStatue
utility class.
As a developer who has designed Java utility classes over the year, I try and prevent others from making the same mistakes I have made or allowed, by making utility classes I add final
. This makes it clear that the utility class is to only be used, as the final
keyword makes it impossible to extend. If you find a utility class in Eclipse Collections that is not final
, please report it as a potential bug via the GitHub issue tracker. Java library designers may have good reasons to not make a utility class final
, and hopefully document those reasons and leave guidance to future developers who depend on the utility classes.
AssertButVerify
There is a module in Eclipse Collections that is published as a separate .jar
with a utility class with only static methods named Verify
. Verify
extends Assert
, which also only has static methods that are public
. Verify
can do everything that Assert
can do. You can call public
static
methods on Verify
, which are actually defined on Assert
, and may or may not be “hiding” as specifically defined methods on Verify
.
This is the current class definition for the Verify
utility class in Eclipse Collections. Thankfully, no class can further extend Verify
. Verify
has been designed this way for over a decade, and has had absolutely no issues.
public final class Verify extends Assert
Why does Verify
extend Assert
?
Verify
was a utility class created a long time ago to make it easy to assert specific things about Collection
and Iterable
types. The assertions on Verify
have intention revealing names about what is being asserted about various collection types. A complete list of the methods on Verify
can be seen in a table below. The methods on this class are used extensively for testing collections in Eclipse Collections. A convenience decision was made to have Verify
extend the Assert
class, so that we didn’t have to switch between using Verify
and Assert
for common assertions in test cases (e.g. assertEquals
, assertNull
). Verify
is Assert
, but also Verify
. This was convenient for use in Eclipse Collections, because we didn’t have to remember which assertion methods were on Assert
and which were on Verify
, and could just import one class — Verify
. When we open sourced Eclipse Collections and made the class public in the eclipse-collections-testutils.jar
, our hands became unknowingly and unfortunately tied with this old inheritance dependency decision. Users of the library may have been potentially using the static methods defined on Assert
that are available via Verify
. This has not caused any issues for Eclipse Collections users or library developers, until now. I will explain what the issue is in the next section.
The following table shows the list of distinct and shared methods between Verify
and Assert
. Assert has no distinct methods in this table, because Verify
extends Assert
. If I drew a Venn Diagram to show the relationship, Assert
would be completely contained within Verify
.
As can be seen by the table above, Verify
has a large number of distinct assert
methods (165) for developers to use. These methods are extremely useful if you write a lot of collection tests. Bolting an additional 56 methods was purely convenience so we didn’t need to remember whether a method was on Verify
or if it was on Assert
. We got all the methods in one place. One less import! Yay!
This turns out to be a convenience gone wrong. Thankfully, we will be fixing this unfortunate design decision in the Eclipse Collections 12.0 release, when we are also upgrading to JUnit 5.
All Your Assertions R Belong to Us
We are looking to upgrade from JUnit 4 to JUnit 5 in Eclipse Collections, and this is now causing some short-term pain for the developers working on the library. We have no quick and easy way out of this decades old decision, but there is a path to correcting this convenience design decision, and it is being worked on now.
Eclipse Collections is in the process of upgrading to JUnit 5. As seen above, Verify
has a public API that currently extends the public API of Assert
. The type junit.org.Assert
is a utility class in JUnit 4. There is a new utility class in JUnit 5 that should be used instead of Assert
named Assertions
, which we have been migrating all of our tests to. What should we do with Verify
? Should it be changed to extend Assertions
in JUnit 5?
Old Statue seeking new BiggerBird to supply convenience
No, No, No. Although the Assertions
class does not prevent Verify
from extending it, there is very helpful guidance in the JavaDoc of Assertions
from the JUnit team.
We decided even before reading this guidance, that we would break the inheritance dependency between Assert
and Verify
, and not repeat the same convenience mistake with Assertions
. If Verify
were changed to extend Assertions
, we would have replaced a pigeon-sized bird with an ostrich-sized bird. The Assertions
class would add an additional 265 methods to the Verify
class if we extended it. This is in addition to the 26 methods we would need to add by hand that Assertions
doesn’t have in common with the Assert
class. Imagine if we did this what it might be like to eventually upgrade Verify
to JUnit 6. Ouch!
We don’t like making breaking changes in Eclipse Collections. We are still deciding whether we will make a source breaking change with Verify
once we correct the decision to extend Assert
. My current thinking is to add all of the Assert
methods as deprecated for removal and throw UnsupportedOperationException
in all of them with a JavaDoc example and a link to the equivalent method in Assertions
to use. When we release the next major release of Eclipse Collections we can then “safely” remove all of the deprecated methods. Phew!
Convenience has a shelf life. Changing inheritance design decisions can be painful and take a long time to correct.
In case you didn’t know
I spoke to a long time Java developer this week who was not aware that static methods can be “inherited” by subclasses. This is an oddity many developers hopefully won’t encounter in their daily development! If you add a static method with the same signature in a subclass, it is referred to as “hiding” not “overriding” the same method in the parent class. If you call static methods on instances of a subclass, the static method that is called will depend on the type of the variable which holds onto the instance. Even if a subclass hides a static method, if the type of an instance of a subtype is set to the superclass type, the static method that is called will be the one defined on the superclass.
If you haven’t heard of or encountered this feature before, the following link might be useful. There are also some blogs out there you can search for to find out how method “hiding” works with static methods on classes.
Thankfully, this static method inheritance feature is not available for static methods on interfaces. This is a very good thing, IMO. It would have made it potentially very messy to add the of()
factory methods to List
, Set
, and Map
. The SortedSet
interface extends Set
, and the SortedMap
interface extends Map
, but neither interface defines their own of()
methods. Imagine if the SortedSet
and SortedMap
interfaces inherited the of()
method from Set
and Map
. That would be very odd.
Final thoughts
My recommendation is to make your static utility classes final. If you’re going to define a utility class with only static methods, make sure you have a very good reason to not make the class final. My recommendation for users of libraries, don’t extend static utility classes if you are building your own utility classes. You don’t want to turn a Bird
into a Statue
. Add only the behaviors you need and keep your Statue
independent and clean. Just use static utility methods as intended, do not extend them.
Thanks for reading!
I am the creator of and committer for the Eclipse Collections OSS project, which is managed at the Eclipse Foundation. Eclipse Collections is open for contributions.