by Mark Lodato
TL;DR
- Be careful about
true
vs truthy on the boundaries of your application - Consider returning
true
andfalse
exclusively in predicate methods - Consider testing
.to eq true
instead of.to be_truthy
inRSpec
Truthiness in Ruby
Ruby, being duck-typed, does not strictly enforce "boolean" values:
if value
do_this
else
do_that
end
In this case, we don't care if value
is true
or false
or 1
or 0
or
a complex object. Ruby has a method for determining if any kind of value is
"truthy" or "falsey". For Ruby, the rules are fairly lean:
nil
is falseyfalse
is falsey- Everything else is truthy
Therefore:
"Hello World" if 1 # => "Hello World"
"Hello World" if nil # => nil
Now there might be some surprises if you come from other languages:
"Hello World" if "" # => "Hello World"
"Hello World" if 0 # => "Hello World"
Those wouldn't fly in all languages, but that's fine...unless...
Project Boundaries
Let's contrive a thing to make a point:
class ContrivedThing < ApplicationRecord
def self.foo
where(id: all.select(&:foo?))
end
def foo?
complicated_foo?
end
private
def complicated_foo?
# ... do complex work ...
complicated_derived_thing =~ /fancy regex/
end
end
And we can test it like:
expect(contrived_thing).to be_foo
But there could be a problem! What if we have lots of ContrivedThing
s?
all.select(&:foo?)
loads all the records into memory,
does a complicated calculation on each to
determine which are foo?
-ey, and then wraps them in a huge binds list
for query chaining.
As much as possible, we'd like to keep this in SQL so we decide to memoize
the value of foo?
in a boolean column named foo
in the database:
class ContrivedThing < ApplicationRecord
before_save :memoize_foo
private
def memoize_foo
return unless certain_things_changed?
assign_attributes(foo: complicated_foo?)
end
end
Now we can delete ContrivedThing::foo
and ContrivedThing#foo?
(because
ActiveRecord
will create scopes and predicate methods for
boolean columns) and we've vastly improved the performance of the scope.
But there's a catch...
The Truth of Truthiness
We've been using foo?
as a truthy/falsey identifier but what is it actually?
Well its return value comes from the =~
operator which
returns integer or nil.
So we might have:
"string" =~ /string/ # => 0
which is properly truthy in Ruby because there was a match. Unfortunately,
we're directly serializing that value to our postgresql database which
coerces zero to false.
Now we have a bunch of values that used to be truthy in Ruby but now are falsey
because postgres converted them to a strict false
value.
How Could We Avoid This?
Carefully Considering the Destination
One approach involves
converting the value
before we head off to the database:
def memoize_foo
return unless certain_things_changed?
boolean_value = ActiveModel::Type::Boolean.new.cast(complicated_foo?)
# Rails 4.2 - ActiveRecord::Type::Boolean.new.type_cast_from_database()
# Rails 4.1 and below - ActiveRecord::ConnectionAdapters::Column.value_to_boolean
assign_attributes(foo: boolean_value)
end
Which works wonderfully, but can be forgotten and is only really caught
if you happened to write a test like:
thing = ContrivedThing.new()
# setup to allow complicated_foo? to return 0
thing.save
expect(thing).to be_foo
Ensure Boolean Values in Predicate Methods
There's clearly some heat around
this topic in general but the
official Ruby FAQ recommends:
predicate methods (those whose name ends with a question mark) return
true
orfalse
.
So one way around our bug is:
def complicated_foo?
complicated_derived_thing =~ /fancy regex/ ? true : false # => true/false
end
Ensure Boolean Values in Tests
Following the above option, what if our original test wasn't:
expect(contrived_thing).to be_foo
but instead:
expect(contrived_thing.foo?).to eq true
This sort of testing isn't nearly as pretty, but helps prevent these integration
pains.
Summary
- Ruby's handling of predicate methods, predicate matchers, and truthy/falsey
values allows for light, expressive, pleasing code, but can result in subtle
bugs when interacting with outside sources. - Be extra careful at application boundary points that there are no assumptions
about how languages interpret different values. - When in doubt, be overly specific to ensure values can't be misunderstood.
Learn more about how The Gnar builds Ruby on Rails applications.