
true vs truthy on the boundaries of your applicationtrue and false exclusively in predicate methods.to eq true instead of .to be_truthy in RSpecRuby, being duck-typed, does not strictly enforce "boolean" values:
if value
do_this
else
do_that
endIn 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 # => nilNow 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
endAnd we can test it like:
expect(contrived_thing).to be_fooBut there could be a problem! What if we have lots of ContrivedThings? 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?) endendNow 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/ # => 0which 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)
endWhich 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_fooThere'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
endFollowing the above option, what if our original test wasn't:
expect(contrived_thing).to be_foobut instead:
expect(contrived_thing.foo?).to eq trueThis 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.