Intersepting `assert` in tests

I have some code at the boundary of my app that takes input from a library. The library has some guarentees that are documented in the user documentation, but which aren't enforceable by the compiler (e.g. "this array will never be empty", "the members of this array each have a unique id", etc.).

I see three ways of handling this:

  1. Don't check these assumptions are true, just assume they are. This leaves me vulnerable to mysterious bugs in case that library ever makes breaking changes.
  2. Validate those assumptions by throwing an error if they're not met. This is great, acts like a detector for sneaky breaking changes in the library, and it's trivial to test. However, it proliferates throws and try all over my code, for functions that otherwise have no need to throw.
  3. Validate those assumptions using assert/precondition/fatalError and such. This seems like a good trade off. The assumptions are documented and validated in code, and the call sites don't have to be paranoid about handling errors that'll almost never actually happen.

I like approach 3, but it was tricky to test. I've made wrappers for assert/assertionFailure/precondition that let me intercept these calls and stub them in tests (but not yet preconditionFailure or fatalError, since those return Never and are much trickier to stub). This works for all of my own calls to these functions.

This works so far, but then I realized I have untested code paths related to, say, Dictionary.init(uniqueKeysWithValues:). Every call to this function is encoding an assumption that the input will have unique keys for all values. If not, it'll explode. I'm looking for a way to try to cover that case in my tests, but I can't find a way to "override" the trapping behavior that's done in this function.

I can replace each such call with Dictionary.init(_:uniquingKeysWith:), where the block calls my stubbed assert, but this really pollutes my code. Is there a way to improve this?

I read some threads about this, where a common point was that the trapping behavior (e.g. of the subscript operator of Arrays) is intentionally not indirect (therefor not intercept-able) out of performance considerations. Fair enough, but are all standard library traps equally rigid?

TL;DR: What's the best way to test code that defends against invalid input using assert and friends, without replacing it with throws/try all over the code base?

Lol why didn't I think of that. Yeah, I'll do that!

The library case was what clued me into this, but the issue still persists for other code paths that call crashable stuff (the dictionary example, for one)

There are some library solutions for this, which catch errors at the signal level in order to enable testing fatalError et al.

2 Likes