As the holidays near, it’s time to give yourself (and your team) the gift of a happier development experience! So, over 12 days, we’ll unwrap 12 small (but mighty) approaches designed to level up your Rails apps. We’ll discuss, then suggest hands-on practice. These bite-sized tasks are designed to be completed in under an hour. They’ll need to be quick if we’re going to convince Santa to make the switch from “Sleighs” to Rails—he’s a busy dude!
Irina Nazarova CEO at Evil Martians
- Day 1: a renewing, anti-aging treatment
- Day 2: faster rails boot time
- Day 3: better yourself and better your environment
- Day 4: flaky flakes
- Day 5: the nutcracker suite of system testing
- Day 6: see you next Monday with more festive Rails tips!
Day 1: a renewing, anti-aging treatment
A libyear is a simple metric that shows your application’s “freshness”. In other words, it shows you how outdated your dependencies are–literally–in years.
Let’s generate a libyear report for one of your current applications:
# Install libyear
$ gem install libyear-bundler
# Generate libyear report
$ libyear-bundler
aasm 5.2.0 2021-05-02 5.5.0 2023-02-05 1.8
actioncable 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
actionmailbox 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
actionmailer 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
actionpack 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
actiontext 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
actionview 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
activejob 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
activemodel 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
activerecord 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
activestorage 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
activesupport 7.0.8.5 2024-10-15 8.0.0 2024-11-07 0.1
acts_as_list 1.1.0 2023-01-31 1.2.4 2024-11-19 1.8
...
System is 663.4 libyears behind
OK, so we’re 663.4 libyears behind–that’s not so bad! Actually, for larger apps, hitting the century mark is pretty easy. But don’t panic: nothing here is outdated… it’s just “vintage”!
(Yes, “vintage” like that disgusting bottle of long expired eggnog. But hey, eggnog is always festive, and this post has a holiday theme, so keep that in mind! 🎄)
In any case, at first glance, “libyears” might seem like a novelty metric rather than a useful one, but it’s actually great for comparing different subprojects and prioritizing upgrades.
Moreover, regularly running this gem encourages developers to update those small, forgotten gems (or find modern alternatives), instead of just focusing on the big and interesting ones like Rails.
Additionally, another benefit of the libyear rejuvenation treatment is that new versions often bring bug fixes and speed boosts for free—just a little love and attention needed!
Don’t let libyears catch up with you
Today’s holiday challenge is to dust off an old app where the libyears are starting to look more like lightyears.
(NOTE: above, the intended association of “lightyears” is, of course, being expressed with the vibes of “very old”, using “lightyears” as a measurement of time, rather than distance, which some readers would have reasonably anticipated.)
(SECOND, MORE RELEVANT NOTE: one common oversight is gems with stalled custom GitHub links or pinned versions due to bugs. Developers often forget to revert back to the upstream after the fix has been applied. Missing out on those precious updates from developers has never been easier!)
So, let’s update some of those gems! But wait—don’t just rashly run bundle update—that’s too much extra stress for holidays! Instead, let’s keep things merry and bright by updating just a couple of gems at a time.
But how do you decide what to update? While tools like libyear can provide guidance, let’s unwrap another gift from Santa’s sack to add to your holiday toolbox: bundle_update_interactive.
Not only does this gem make the updating process more interactive and sophisticated, it also lets you quickly review the changelog–with just one click! No more hangover regrets after those fancy, reckless upgrade parties (again, presumably facilitated via eggnog, the most festive of drinks).
Day 1 festive action plan:
- Generate a libyear metric and save the results before then making improvements.
- Check all gems with pinned versions or GitHub sources.
- Run bundle_update_interactive and update some of your gems.
- Once you’re happy with the results, generate a libyear metric again, calculate the difference, and share your achievements under the hashtag
#railsmas
!
It would be great to see you challenge the Rails community with your results—let’s see who’s got the youngest, the oldest, or the most rejuvenated app!
Day 2: faster rails boot time
One of the worst parts of the holiday season most people are afraid to admit: waiting for the presents. (Best part? Presents.) “What am I gonna get?” This is practically all one can think as you wait and wait throughout the year.
…but you know the kind of waiting that is even worse? Waiting for your Rails application to load.
So, today, we’ll focus on speeding up your boot time! It’s the gift that will keep on giving, whether for a code reload in development or a deployment in production.
Santa also does the whole “boot” thing
Before diving into specifics, let’s try to understand your personal boot time a little better. Take a look at your application now. To do so, use this shiny new CRuby profiler: Vernier.
First, let’s get Vernier installed and profile our boot process:
# Install Vernier sampling profiler
$ gem install vernier
# Capture a profile of the application's full boot process.
$ DISABLE_SPRING=1 vernier run -- bundle exec rails runner 'puts "Railsmas is here!"'
Railsmas is here!
#<Vernier::Result 2.821947994 seconds, 7 threads, 3943 samples, 3103 unique>
written to /tmp/profile20241126-134618-4btew9.vernier.json.gz
Every application is unique as a (oh, let’s say) snowflake, and depending on the duration of the boot process, you’ll either get a very clear or really messy graph.
Don’t forget to also generate a second report with eager loading enabled inside config/environments/development.rb
, as it provides more data that can draw out valuable insights:
config.eager_load = true
Now open the generated profile in the Vernier UI:
When working with the call tree, always take note of the percentage of the report spent during the boot for each subtree.
Note: it’s more effective to dig into the largest parts first. Pay attention to the big initializer spans where the highest percentage of boot runtime is spent during loading.
We should always remember that during loading, we’re running all the initialization and class-level code. Some examples: reading huge JSON files, eager-loading all time zone information, attempting to use i18n on a class level which eager loads all translations, and so on.
Extensive DSLs are also often common offenders: Active Admin, Grape, GraphQL, RSwag, as well as many others. We may try to separate loading these large parts of the app into separate instances on production or skip them locally when we’re not working on these parts.
External dependencies might contain all those issues as well, so the key here is to audit your Gemfile. Do you really need all those gems? Unused gems add unnecessary boot time and increase complexity. Slim things down, and give your application the gift of simplicity!
You could also move some gems into separate groups to load them conditionally.
Santa’s gift
Now for the main action. Santa, a magical being who has powers both enigmatic and indecipherable, has come with a token of festive altruism: a one-line change in Devise’s configuration that can significantly improve your boot time. (By default, Devise forces all application routes to be reloaded, effectively loading them twice.) However, we can easily disable this default behavior in config/initializers/devise.rb
:
# When false, Devise will not attempt to reload routes on eager load.
# This can reduce the time taken to boot the app but if your application
# requires the Devise mappings to be loaded during boot time the application
# won't boot properly.
config.reload_routes = false
Of course, the impact of this change scales with the number of routes in the application, but it should provide a noticeable improvement in load time.
Day 2 festive action plan:
Install Vernier or rbspy (Ruby < 3.2.1) and create two profiles for your application: one with eager loading enabled; one without eager loading.
Analyze the generated profiles in Vernier UI:
- Explore the call tree for the most time-consuming subtrees and methods.
- Pay close attention to the percentage of the report spent during the boot for each subtree.
Find the offenders and optimize them:
- Consider moving large parts of the app into separate instances in production or skipping them locally.
- Remove unused gems from your Gemfile.
- Move some gems into separate groups to load them conditionally.
- Disable Devise’s route reloading if applicable.
Ensure the application loading remains functional by testing your application after making the changes.
…and share your 2nd day insights under #railsmas!
Ho ho! What did you think of Santa’s boot speed prowess?
Day 3: better yourself and better your environment
New Year, new vibes! Yesterday, we gave our app a bit of an energy boost, so today, let’s focus on improving our development setup! Suddenly, DHHanish Santa suggests we should migrate to a brand new Linux-endorsed notebook—tempting, but let’s not go there just yet.
Today, we’re thrilled to unwrap a special gift that was actually prepared by Shopify two years ago. Rest assured, this refurbished gem is truly a treasure—Ruby LSP!
What makes this so special? After all, we’ve had tools like RubyMine and Solargraph for years—who needs another Language Server Protocol (LSP) implementation for Ruby?
The answer lies in one immensely powerful feature: an addon system written in Ruby. While ruby-lsp
includes advanced staples like jump-to-definition, inline documentation, and code navigation, the addon system takes it even further by allowing anyone to easily expand its functionality.
The real power, however, lies in distributing these addons—you don’t even need to install addons manually—they load automatically (like Railties) when placed inside ./lib/ruby_lsp/**/addon.rb
of your gem. This means every user of both Ruby LSP and your gem enjoys an enhanced editor experience without any extra effort!
Just imagine it: all the convention-over-configuration magic is unveiled—the dark sorcery behind ActiveModelSerializer, CanCanCan, ActiveAdmin, become just a bunch of people in strange costumes.
And keep in mind the LSP API reminds RuboCop cops—and, of course, thanks to Ruby—creating addons becomes straightforward.
You could even develop a custom application LSP addon to highlight your application’s unique DSLs before midnight on New Year’s Eve!
Here’s a little miracle—a sneak peek of a plugin for our beloved Action Policy gem to enable simple jump-to-definition support for its DSL. Now we can effortlessly jump from a controller to the relevant policy and back:
Wow, it works! Even better, this plugin would work for every Ruby LSP user if we contribute our new code to the upstream gem.
Day 3 festive action plan:
- Set up Ruby LSP in the editor of your choice.
- Explore the new, advanced features available right out of the box, including all the standard yet more powerful staples—for example, effortless navigation to hidden elements within Rails’ “convention over configuration” motto—and much more.
- Consider creating a simple plugin for your application’s (or a beloved gem’s) custom DSL to help your colleagues save time working with the code.
By the way, share your Ruby LSP ideas under #railsmas—your suggestions might inspire others! After all, the holidays are all about coming together. (And, of course, the presents.)
Day 4: flaky flakes
Every year, we hear (and absolutely adore) the same tired holiday jingles. We simply can’t get enough of them!
Some are quite catchy, and we find ourselves mentally looping through them. We need something to fight this kind of repetitiveness, like our gift for today: dealing with flaky tests, which force you to repeat test suite runs again and again. And we all loathe this repetitiveness, just as much as any nefarious holiday earworm.
First of all, we should start with proper test configuration to catch the flakies early. Check that you’re running the tests in the random order:
RSpec.configure do |config|
config.order = :random
Kernel.srand config.seed
end
This prevents you from relying on subtle global state cleanup sequences (which are sometimes found in applications’ test suites). And if you have a moderate test suite without this option, anticipate lots of unexpected failures. Embrace the holiday chaos, unpleasant at first, presents later.
By the way, what’s all the fuss about global state leaks? Basically, anything changed during a test, except for database interactions (safeguarded by default with transactional tests or old-school database_cleaner), is at risk of retaining lingering state and leaking into other test contexts.
The lengthy list of common culprits include:
- ActiveRecord models created outside transactions (such as inside
before(:all)
) - ENV changes
- Configuration settings (app’s config, routes, logger, I18n, etc.)
- Global, class or singleton level variables
- Filesystem modifications
- Additional data stores such as Redis, Elasticsearch, RabbitMQ, etc. (if you use real integrations in tests)
- Cache / in-memory stores
- Different queues such as for jobs, mailers, etc. (always prefer fake modes for performance benefits)
- Browser storages for system tests
- Database primary key counters
OK, got global state under control, but still experiencing flakiness? Like a brutal holiday flight delay-someone has messed up their Time
!
If inside the test we interact with time, it should be frozen at a specific time point (and always if it’s time-related asserts here).
Another common oversight is tests that are dependent on external systems or services, as it’s unstable and slow. Places to assess for risks in apps:
- HTTP requests to external APIs (including gems of services with hidden HTTP facade)
- Remote file storage access
- SMTP mailing
- DNS resolutions
- Raw sockets usage
Try to disable external requests altogether by bringing in WebMock into the test suite. Remember: Good tests are like Santa’s workshop-self-contained and not dependent on the outside world! (Disclaimer: unconfirmed lore.)
Last but not least, ordering! Here’s a self-explanatory snippet:
RSpec.describe "Delivery order" do
it "is a naughty test!" do
expect(Gift.all).to eq([gift1, gift2]) # Ho-ho-NO! Order undefined!
end
it "is a nice one!" do
expect(Gift.order(:id)).to eq([gift1, gift2]) # Santa approved!
end
it "is an extra nice test!" do
expect(Gift.all).to match_array([gift1, gift2]) # Santa’s secret method
end
end
A random endnote appears! Randomness isn’t always good, it also might cause havoc. Sometimes you need to use stable values, for example when using VCR cassettes.
Day 4 festive action plan:
- Ensure you’re running tests in random order.
- If you know you have a flaky test—choose one and try to fix it using our list of common problems.
- Don’t have one? We don’t believe your sweet lies!
Search forDate
andTime
usages in your tests and check if they use frozen time. - Try to cut your test suite from the Internet by installing the WebMock gem.
- Fortify your test suite by utilizing
match_array
or ordering in proper places.
Day 5: the nutcracker suite of system testing
Besides piles of presents, the holidays also present an opportune occasion for massive test parties, holiday gatherings where the main event is invariably… failure (because, well, system tests are involved). But don’t worry, help is just under the tree!
The plan to save the party consists of several parts.
Part one: being prepared to accept the inevitable-system tests assert more expectations on average, cover lots of application’s parts, run inside a live browser environment, and thus naturally prone to test breaches. Therefore, the crucial step is minimizing time spending developers waiting for the whole suite reruns.
The simplest trick here is to retry individual tests instead of the whole test run. We have rspec-retry and Minitest::Retry for corresponding test frameworks. This represents a sharp knife to approach the problem with, so our recommendation is to be cautious and include it only for system tests to avoid hiding genuine issues in unit tests. (Also, do not forget to limit the amount of retries, so we do not balloon the whole test suite run with so-so tests.)
Simple trick: the day after
Since we’ve already saved a lot of time by skipping manual test suite retries, we can now move on to part two: actually fixing the flakiness of the affected system tests. And just like disrespectful children trying to steal a peak at the elusive Santa on Christmas night, we can try to catch our failures using Rails automatic screenshotting. (Pro tip: do not forget to persist the failure screenshots on CI too.)
One Viewport size fits all
Speaking about screenshots, it’s important to set up a proper viewport size. This configuration depends on a particular driver-for an example with the now-hot Cuprite, check out our blog post. As a bonus, it also provides more stable tests as everyone will receive the same behaviour in scrolling pages.
Capybara matchers: the good, the bad, and the flaky
Having a rock solid configuration is not enough-you’ll need to be careful with test code. Capybara matchers are smart, but we have to be careful not to miss all conditions in the selector:
# Naughty developers use RSpec matchers that flake like artificial snow ❄️
# This matcher may find a Santa list before it’s filled by the elves.
text_field = find('.santa_list)
expect(text_field['value']).to match /Nice list/
# Nice developers make Santa happy with synchronized Capybara matchers 🎅
# This matcher retries until the elves finish updating the list!
expect(page).to have_field('.santa_list, with: /Nice list/)
Sleepless nights with Capybara Lockstep
While Capybara can retry some failed actions (like element expectations), it doesn’t know anything about JavaScript nor AJAX, so UI interactions, like modal forms or dynamic updates, where actions may be attempted before elements are ready (and network requests are still in flight) are prone to failure, depending on timing and machine performance.
Usually, developers try to solve it with explicit sporadic sleep
s. But there’s a better alternative: capybara-lockstep.
With the gem configured, Capybara’s new behavior means it waits for all JS async interactions to be completed before executing the next matcher. So, we have a guarantee that the page is ready which means we don’t need any unreliable sleep
s between test steps. capybara-lockstep also enhances test performance by intelligently skipping unnecessary waits, eliminating the need for inefficient fixed delays. It’s such a pivotal experience that we believe it should be included in every system test suite by default!
Day 5 festive action plan
- Install rspec-retry or Minitest::Retry to retry individual tests instead of whole suite runs.
- Ensure system tests failure screenshots are available.
- Set up a proper browser viewport configuration for more stable tests.
- Verify system test matchers to make sure they don’t contain incomplete conditions.
- Install capybara-lockstep to eliminate necessity in explicit unreliable slow
sleep
calls.
Share other tips and cautionary tales about failing system tests with #railsmas!
And also be sure to check back tomorrow for what we’ve collectively taken to calling “DAY 6”! 🎁🎁🎁🎁🎁🎁