We deploy our front-end code to production multiple times per day. Our ability to do this is closely tied to our confidence in our test suite and deployment infrastructure. Tens of thousands of tests are run against our code whenever we click the deploy button. Given that this is the case, when something happens that causes us to lose confidence in the reliability of our tests, it is important that we fix it as soon as possible.
Over the years, we have collected a few Ruby on Rails helpers with utility methods that we’ve found ourselves reusing throughout the application. For example, we have FormatHelper, which contains methods for formatting numbers in a variety of different ways; BrowserHelper, to aid in browser version and capability detection; DisclosuresHelper, which provides text for various legal and regulatory disclosures; and many more.
Recently, we added a new class that required a couple helper methods from one of these modules, so the following code was added:
module FormatHelper
def phone_number
end
end
class MyClass
include FormatHelper
def self.my_method
phone_number
end
end
MyClass.my_method
MyClass.phone_number
MyClass.new.phone_number
Code language: Ruby (ruby)
The effect of including the module is that its methods are made available to instances of MyClass, but MyClass is attempting to use it in a class method. What we should have done is extend FormatHelper:
class MyClass
extend FormatHelper
def self.my_method
phone_number
end
end
MyClass.my_method
MyClass.phone_number
Code language: Ruby (ruby)
All code we write is thoroughly tested, and the code in question was no exception:
describe MyClass do
describe '::my_method' do
it 'does the thing' do
MyClass.expects(:phone_number).once
MyClass.my_method
end
end
end
Code language: Ruby (ruby)
In our class, we incorrectly extended FormatHelper rather than including it. This had the effect of adding the phone_number helper method to instances of the class rather than the class itself. We would expect the above test to catch this error and fail. However, when we ran this test it passed – but why? Our first guess was that the module was still being included somehow/somewhere. We can find out how and where a module is being included by taking advantage of the Module.included callback, which is invoked whenever the module is included somewhere.
module FormatHelper
def self.included(base)
puts base
puts caller
end
def phone_number
end
end
Code language: Ruby (ruby)
And right there at the top of the file ‘blueprints.rb’ is the culprit:
include FormatHelper
Code language: Ruby (ruby)
The FormatHelper module was being included in this file, blueprints.rb, while in the global scope, but why did this cause our test to still pass?
A brief segue into Ruby main
When you load up irb, by default you are in the global scope, also known as ‘main’. Since everything is an object in ruby, ‘main’ is too – it is an instance of the ‘Object’ class with the added special property of methods being added to it (or modules included) also being added the ‘Object’ class itself. The following diagram shows a simplified snapshot of the Ruby hierarchy in the context of the issue we investigated:

The Ruby class ‘Object’ is near the top of the Ruby hierarchy, and any changes to the ‘Object’ class will affect any classes that inherit from it, which is pretty much all Ruby objects.
def some_method
end
class MyClass
end
puts MyClass.private_methods.include?(:some_method)
my_class = MyClass.new
puts my_class.private_methods.include?(:some_method)
Code language: Ruby (ruby)
Back to our regularly scheduled programming
blueprints.rb defines test fixtures that are loaded as part of our Rails test environment initialization. If you recall that blueprints.rb included the MyHelper module at the top of the file and therefore in the global scope, every single class and object would always have access to all of the methods in the module, whether that module is being included correctly in a specific file or not. However, this didn’t happen in production, because the blueprints.rb file is only loaded in the test environment and therefore the module wasn’t globally included. The end result is what we saw previously: a test passing when it should be failing.
In order to fix this, I needed to ensure that the module wasn’t included globally. However, since the helpers are used later in the file I couldn’t simply remove it.
include FormatHelper
class Person
def self.blueprint
yield
end
end
Person.blueprint do
phone_number
end
Code language: Ruby (ruby)
My first idea was to include the module in Person, so the scope of all of the loaded methods would be within that class.
class Person
include FormatHelper
def self.blueprint
yield
end
end
Person.blueprint do
phone_number
end
Code language: Ruby (ruby)
Unfortunately that didn’t work, because when a block is yielded it executes in the same scope it was created in. The FormatHelper methods were included in the scope of the Person class, but the block was executed within the global scope.
My next thought was to include the module within the block itself.
Person.blueprint do
include FormatHelper
phone_number
end
Code language: Ruby (ruby)
This didn’t work either, because blocks in Ruby inherit the scope they were created in. This block is in the main scope, so including a module here added all of the module methods to the global scope.
One other option I had was to change the module to use module functions, like so:
module FormatHelper
class << self
def phone_number
end
end
end
Code language: Ruby (ruby)
However, the result of this would be that these helpers could then
only be called directly on the module rather than being included and called on the instance. They are commonly used as mixins throughout the codebase and we don’t particularly want to lose that ability. However, we can get a combination of both module and mixin functions:
module FormatHelper
extend self
def phone_number
end
end
Code language: Ruby (ruby)
This had the effect of adding instance methods onto the
eigenclass of FormatHelper, callable via FormatHelper.phone_number, while still allowing FormatHelper to be included as a regular module to apply its methods on to instances of other classes.
Now, when we’re in the global scope (e.g. in our blueprints.rb file), these methods can be called via:
Person.blueprint do
FormatHelper.phone_number
end
Code language: Ruby (ruby)
And they can still be used as mixins where applicable:
class MyClass
extend FormatHelper
def self.my_method
phone_number
end
end
Code language: Ruby (ruby)
The original, immediate problem is now resolved – our original tests are failing correctly, and we can again be confident in our test suite and therefore our continuous deployment process. However, it is critically important to not just fix the symptom, but also to investigate and resolve the cause. From this investigation, we can pretty readily infer that global includes are bad, so we should ensure they don’t happen: code reviewers shouldn’t have to be on the lookout for global includes in pull requests, so we now have a custom
RuboCop to lint for global includes and fail offending builds accordingly.