true
vs truthy on the boundaries of your applicationtrue
and false
exclusively in predicate methods.to eq true
instead of .to be_truthy
in RSpec
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 falseyTherefore:
"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...
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?) endend
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...
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.
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
There's clearly some heat around this topic in general but the official Ruby FAQ recommends:
true
or false
.So one way around our bug is:
def complicated_foo?
complicated_derived_thing =~ /fancy regex/ ? true : false # => true/false
end
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.
Learn more about how The Gnar builds Ruby on Rails applications.