Trust, but verify

Self-confident code does not ask questions about the data given to it. It enforces validity, asserts that its expectations are met, or ignores data which isn’t up to its standards. Previously we’ve looked at some methods for enforcement, using fetch() and Array(). Today I want to talk about the second tactic, assertions.

Assertions and contracts get comparatively little attention in the Ruby world. I’m not sure why that is. Some might say that pervasive unit-testing has rendered contract-checking redundant or less important than it is in less test-infected programming communities. Or even that TDD and assertions represent opposing philosophies of how to address correctness in software. I disagree; I think that the two techniques are complementary.

The pragmatic essence of code assertions is the idea of failing fast. Most violations of a contract will eventually result in an error; the question is, how far away from the original contract violation will the exception be raised? And will the expectation which wasn’t satisfied be clearly marked?

The advantages of assertions are more than theoretical. Studies of software projects have shown that projects which employ assertions tend to have fewer defects.

Assertions need not use an elaborate Design by Contract framework (although such libraries do exist for Ruby). They don’t even have to use the word “assert”. Here’s an idiom I like to use in methods which receive an “options” hash:

def initialize(options={}) @color = options.delete(:color) { "chartreuse" } @flavor = options.delete(:flavor) { "bacony" } @texture = options.delete(:texture) { "squamous"} raise "Unknown options #{options.keys.join(', ')}" if !options.empty? end

The last line of the method guards against the not-uncommon scenario of a misspelled option key. It’s an assertion even though there’s no assert() in sight.

Ruby does not come with its own assert() method, but implementing one of your own is a trivial exercise:

def assert(condition, message = "Assertion failed") raise Exception, message unless condition end

The only thing notable about this code is that we raise Exception explicitly, rather than some derivative of it such as RuntimeError . Assertion violations are by definition indicative of an error in the code, which means we should give the program little opportunity to rescue the exception and continue. By raising Exception, we ensure the error will bypass default rescue clauses.

begin assert("black" == "white", "Zebra attack!") rescue nil end # raises "Zebra attack!" despite the rescue

It’s possible to go overboard with assertions. Specifying every nitpicky detail about your inputs can lead to brittle, hard-to-test code and violates the spirit of dynamic typing. But used judiciously they can help to document expectations, keep yourself and your API consumers honest, and reduce time spent debugging errors.

Some guidelines for using assertions effectively:

Assert at module boundaries. Don’t pepper every internal method with assertions. Instead, use them as gatekeepers between modules. Especially use them where your code interacts with a third-party API to document and validate your beliefs about how that API works. This can greatly help your learning process as you get the hang of an unfamiliar library, as well as alerting you to changes introduced by new versions of the third-party code.

Only use assertions where coercion/enforcement is not an option. If it is possible to coerce a value into what you need, or to provide a sensible default for a missing value, prefer those approaches to making assertions.

Don’t assert exact types. Idiomatic Ruby doesn’t care about an object’s class; only that it supports the needed protocol (methods). Prefer value comparisons to respond_to?() checks, respond_to?() checks to kind_of?() , and kind_of?() to instance_of?() .

assert(s.instance_of?(String)) # bad assert(s.kind_of?(String)) # better assert(s.respond_to?(:downcase)) # better assert(s =~ /[[:alnum]]{6,20}/) # best

Finally, if you like the idea of using more assertions in your code, you might be interested in FailFast, a gem I wrote which provides a number convenience methods for concise assertion checking.