How to assert for fun and profit
A hastily put together, hasty start guide to assertingPublished on Fri November 18, 2022 with tags: general-programming, basics, edurant.
Updated on Fri November 18, 2022.
Recently, I had the (dis-)pleasure of using Java. In a function, as a
responsible1 developer, I made sure to verify pre and post conditions via
the assert
keyword. Later, a caller violated one of the preconditions
and, instead of raising a clear assertion failure, the program failed with a
weird error further down the function.
This was puzzling to me. The bit of code that raised an error was the exact bit of code written with the assumption documented in the assertion I wrote, so something else must’ve been wrong, since the assertion passed.
Then it hit me, a realization from long ago: The entire Java ecosystem failed
to understand assert
and has been avoiding it using it, so much so,
that neither IntelliJ IDEA, nor better tools, pass -ea
to the JVM by
default during development.
I wish to clear up some of the confusion surrounding assertions, and help more people use them effectively.
Note
This post won’t have code written in any particular language, more so pseudocode written in a syntax with nice highlighting support from Pygments. There is, however, some discussion of languages, since it inevitably plays a role in how you assert.
What is an assert?
So, if the Java folk managed to get it so wrong, what is the right approach?
When do you use an assert and what meaning should it portray? What are the
semantics of assert !this.getChildList().isEmpty();
?
Fundamentally, to assert is to specify that, at the point in code where the
assert
is invoked, a condition MUST hold true, no matter what, and
if it doesn’t, the code is broken in some way.
Each assertion, however, must satisfy a few properties, without which, it’s
most likely assert
abuse:
Assertions must be pure; that is, assertions must not impact the state of the running program in any way. Assertions serve only to verify the state of code at a given point in execution. Failure to adhere to this rule will lead to code that behaves different depending on whether assertions are enabled, even in the happy path.
The following program, for instance, is problematic:
int i = 5; ((i = 6) == 6); assert
as the value of
i
will change if and only if assertions are enabled. While this particular case is fairly obvious, in the general case, there’s no solution to the problem of detecting whether an expression is pure, at least in imperative languages.Assertions should be self-explanatory, in essence, if someone reading your code sees an
assert
, its purpose should be clear. If it’s even slightly ambiguous, add a message to your assertion (the exact way you do that depends on your language of choice).Assertions may be expensive. Since assertions can be turned on or off, it’s not problematic if they incur a noticeable performance cost, though, more often than not, it’s a good idea to implement multiple types of assertions for cases where not every developer would benefit from them all being on (for an example, see
--enable-checking
in GCC),Assertions MUST NOT be the only thing between misbehavior and input data, including data returned from other APIs (e.g.
fopen
calls, i.e.auto x = fopen ("foo"); assert (foo != NULL);
is invalid). Anything dealing with foreign data must not assume anything about the state or contents of the data, and as such, cannot everassert
, as it doesn’t have an assertion to make. If you take away only one thing from this article, let it be this: assertions do not verify arbitrary inputs.
The pronunciation of assert
In math, it is common for an operation to not be pronounced the way it is spelled; for instance log2n is pronounced “base two logarithm of n”, or some variation of that, depending on who you ask. In computer science, a similar thing applies.
So, how is assert f(x);
pronounced? Something along the lines of “The
property f(x) must hold true, or else the program is malformed,” though, the
pronunciation of “assert that f(x)” is more concise, if the person you’re
talking to knows what it means to assert.
This pronunciation is how I want you, dear reader, to think about asserts,
asserts are more effective comments, they are a checked and functional
construct to state what your function is meant to do and what it expects to be
able to do it, something much more powerful than a mere /* comment */
.
Anticipated questions
What assertions should I write?
All the ones you feel necessary, really. Personally, I feel like it’s good practice to write down an assertion when I:
- Enter a function, to verify arguments not verified by the type system,
- Think that something must hold true, or
- Implement an algorithm that does something unintuitive or ambiguous.
Picking when to assert is much like picking when to comment, except more explicit and more effective.
Why write a check that can be disabled?
Because your testsuite should be thorough enough to benefit from assertions being enabled, and end users not to (i.e, if you’re a library author, your assertions should still benefit the developers depending on your code, but not the end user that depends on their code).
Assertions can be expensive, non-asserting contract validation that is always on cannot: the former grants a very large freedom to do very rigorous verification.
I wouldn’t blame you for invoking Objects.requireNotNull
on your
arguments, but think about whether that’s better than an assertion for the end
user. My position is that it’s not, since the user would be seeing precisely
the same exception, manifesting in exactly the same way. Note that it’s not
wrong to use both, as assertions can be far more powerful.
This kind of checking is, of course, more useful for languages which handle
null
more fatally, but I’d argue that most memory errors that are not
result of all-out failure or corruption can be avoided through good use of C++,
or other languages with highly expressive type systems2, in a much
more effective manner than Java.
Are assertions not a tool for testing?
Yes and no, they are most useful during development and testing stages, but are not exclusive to testing, and especially not unit testing. A highly assert-dense codebase should grant you much greater confidence in your coverage, while making your code more expressive through the mere fact that you’re explicitly stating what must be the case at a given moment.