I recently encountered a unit test that looked like this.
The use of branching and conditionals in tests is an anti-pattern since we want tests to be predictable, each test to focus on a single code execution path and generally keep things simple.
The obvious solution is to replace the conditional with .to.not.be.null
or .to.not.be.undefined
or the catch-all .to.exist
(less cognitive overhead). So why was this code written using an if
and throw
in the first place?
The answer is that asserting existence of the object here causes TypeScript error TS2532: Object is possibly 'undefined'.
.
This is because the implementation in chai creates an assertion object and evaluates it, then an error is thrown if the assertion fails. TypeScript can’t infer that the .to.exist
check will throw if the object is null.
This is not a new problem and a proposal for asserting control flow has been discussed in TypeScript#8655 and an implementation proposed in TypeScript#32695.
Assert Not Null
The first solution is a more elegant variation if the original if
and throw
.
Unfortunately, TypeScript as of now doesn’t infer the conditional inside a function, either, so, you need to wrap the call and ensure it returns an object to make this option work.
We get an error instead of an assertion.
Improving Errors
We can augment assertNotNull
with an expect
to get a proper assertion failure instead of an error.
The result is better.
Casting a Type
We can cast the result of reticulate()
and TypeScript will happily let us by.
This is problematic. If the signature of reticulate()
were to change, we would just be forcing the response to pretend to be a Spline
, getting no new compile-time errors and leaving nonsensical tests.
Allowing Null
Finally, we can use TypeScript !
and explicitly check .to.exist
.
This is my preferred method, but requires disabling strictNullChecks
in tests (read more about this here).
The result, in my opinion, is the cleanest.
Links
- stackoverflow#57066536: How can I avoid an if/else in TypeScript with mocha and undefined returns?
- sample code from above
- TypeScript#8655: Control flow based type narrowing for assert(…) calls.
- TypeScript#32695: A proposed
assert
implementation.