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: a test suite so fast, Santa’s elves are jealous
- Day 7: better database migrations
- Day 8: silent night, secure night
- Day 9: one more sleep until the next gift…
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 #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!
Day 6: a test suite so fast, Santa’s elves are jealous
As the holiday season approaches, time seems to turn slower for all the excited, entitled young youngsters as they eagerly await the thrill of ripping open the paper separating them from their sweet, sweet presents. This wait can be skincrawlingly brutal. Perhaps this phenomenon is somewhat similar to what a developer experiences while waiting for the last bits of a test suite to finish before they can smash the merge button.
So, to avoid a visit from the Spirit of Christmas Impatience (a character that Dickens left on the cutting room floor), today we’re unwrapping strategies to speed up your tests. Because why not make the development process as brisk and pleasant as a winter breeze?
First, let’s identify the bottlenecks. Pinpointing slow tests is the first step to a brighter, faster suite. To start, enable profiling and let’s shed some light on those sluggish specs:
# For RSpec:
bin/rails spec --profile
# Or for Minitest enjoyers (Rails 7.1+):
bin/rails test --profile
This will show you the slowest tests from your suite, but, just from this alone, we don’t really know why they’re slow (unless you put sleep christmas.from_now
there). To dive deeper into how your tests work, use TestProf. It’s like a toolkit of miracles for your test suite, revealing not only issues of a particular test, but also systemic inefficiencies of the whole test suite that can save you significant time.
Once you’ve identified the culprits slowing down your test suite, it’s time to optimize. Let’s talk about some key areas where some fine-tuning and small changes can make a big difference in your testing cadence.
First, ah, Devise—the trusty guardian of our user data. Its encryption in tests can be a bit like that overzealous relative who insists on wrapping every gift with triple layers of tape. Consider using a speeding up hashing algorithm in your test environment:
# config/initializers/devise.rb
Devise.setup do |config|
# Use a lower cost factor in tests
config.stretches = Rails.env.test? ? 1 : 12
end
Devise’s algorithm takes a lot of time—are you surprised? Logs are yet another “Grinch”, heartlessly stealing our precious test suite time. While they can be useful during tests, they can also be excessive. To speed things up, consider reducing your logs’ verbosity, or disabling them entirely.
# config/environments/test.rb
unless ENV['WITH_LOGS']
config.logger = Logger.new(nil)
config.log_level = :fatal
end
Don’t underestimate this small change because it might cut up to 10% from the total test suite running time.
Coverage tools fall into the same category. Run them selectively, perhaps only in CI, to keep your local tests running fast:
if ENV['COVERAGE']
require 'simplecov'
end
Another culprit is the logic buried inside callbacks. One common oversight is PaperTrail, which is great for tracking changes, but during tests, it can feel tracking every snowflake in a blizzard instead of just some more conventional way of doing the weather forecast. Turn PaperTrail off to streamline your test runs.
Background jobs also can be sneaky time sinks. Make sure you’re using Sidekiq::Testing.fake!
mode to skip job processing without the overhead of actual execution, keeping your tests focused and fast.
External HTTP requests also slow everything down! Use VCR to record and replay HTTP interactions, ensuring your tests run smoothly without waiting for the network. VCR cassettes also fortify tests against intermittent test failures.
Day 6 festive action plan
- Turn on test profile and try to optimize the slowest one
- Install TestProf and take a look at the profile of your suite, try to fix global issues first.
And as a reminder, here are a list of common issues:
- Devise hashing cost factor
- Logs and Coverage
- Logic in callbacks (i.e. PaperTrail)
- Disabled Sidekiq fake mode
- External requests
Then, once you’ve done that share your own tips and findings on speeding up tests with #railsmass. We want to see your speedshop results!
Day 7: better database migrations
As we continue Railsmas, it’s time to give our database toolset a makeover. Not having your database in order can cause cascading problems, like watching a house of cards tumble helplessly apart. Or, to keep with the theme, more like watching a gingerbread house pathetically crumble because you didn’t do something right.
Anyway, let’s start with migrations, since they can introduce risks if not handled carefully. For instance, running some migrations on large tables can lock your database, causing downtime.
On this count, the strong_migrations
gem is the best gift you can give your team. This gem acts as a safety net, catching potentially dangerous migrations before they wreak havoc on production:
bundle add strong_migrations
bin/rails g strong_migrations:install
Next, let’s tackle database consistency. The database_consistency
gem is like your holiday checklist, and you can be sure that your database constraints and model validations are aligned, which prevents discrepancies that could lead to bugs, making your application, well, better.
bundle add database_consistency --group development --require false
bundle exec database_consistency
NullConstraintChecker fail User code column is required in the database but does not have a validator disallowing nil values
NullConstraintChecker fail User company_id column is required in the database but do not have presence validator for association (company)
...
Over time, your migration files can accumulate like decorations in the attic, both cumbersome to manage. This is where the squasher gem comes in handy. It allows you to consolidate old migrations into a single file, simplifying your migration history.
gem install squasher
# Squash all migrations prior to 1st January 2024
squasher 2024 -m 8.0 # for Rails 8.0
Finally, let’s introduce a “little elf” that will keep your database as organized as Santa’s workshop! The actual_db_schema
gem automates migrations on branch switches. This gem ensures your database schema remains consistent across branches by automatically rolling back WIP migrations; it saves you from the hassle of manually managing schema discrepancies when switching branches.
Day 7 festive action plan
- First and foremost introduce
strong_migrations
to your workflow. - Sync database checks and model validations with
database_consistency
. - Consider squashing old ActiveRecord migrations with squasher.
- Simplify working with multiple branches locally with
actual_db_schema
.
Then, share your best practices in database maintenance using #railsmas!
Day 8: silent night, secure night
On the eighth day of Railsmas, we’re wrapping our apps in a warm, cozy blanket of security—because nothing ruins a celebratory spirit faster than a data breach. So, let’s “deck the halls” with some security best practices and tools.
Audit all the way
bundler-audit
and ruby_audit
scan your dependencies for known vulnerabilities, ensuring your app doesn’t rely on insecure versions of libraries. Make sure you’re running them regularly on CI:
gem install bundler-audit ruby_audit
bundle-audit check --update
ruby-audit check
(Feeling extra festive? Let Dependabot check and handle dependency updates for you.)
Brakeman: let it scan, let it scan, let it scan
Brakeman is the ultimate security scanner for Rails apps, and it’s been a trusted tool for years. It scans your codebase for vulnerabilities like SQL injection, cross-site scripting (XSS), and other common issues, helping you identify and fix problems before they become serious.
gem install brakeman
brakeman
(By the way: Starting with Rails 8, the rails new
command automatically adds Brakeman to your CI pipeline. So if you’re spinning up a new app, Brakeman will already be keeping your app secure.)
Outdated configurations: the ghosts of Rails past
Over time, best practices evolve, and it’s easy for old settings to linger in your app, quietly undermining your security.
The Rails Security Guide is your go-to resource for identifying outdated or insecure configurations. Take some time to review your app’s settings and make sure they align with current best practices.
And don’t forget about your dependencies! For example, if you’re using Devise for authentication, check your config.stretches
setting against the latest default value, at the time of writing, the recommended number of stretches is 12. Anything lower, and it’s time for an update:
# config/initializers/devise.rb
config.stretches = Rails.env.test? ? 1 : 12
Note, it will only affect new passwords generated after the change. Existing users will continue using their original stretch count until they change their passwords.
Day 8 festive action plan
- Add
bundler-audit
andruby_audit
to your CI (or use Dependabot). - Run Brakeman to scan your app for vulnerabilities.
Then, review your app for outdated configurations:
- Review your app against the Rails Security Guide.
- Check configurations of security related dependencies like Devise, Lockbox, and so on.
And don’t forget to share your findings with the hashtag #railsmas—but only after you’ve patched those zero days, because Santa’s watching!
And also be sure to check back tomorrow for what we’ve collectively taken to calling “DAY 9”! 🎁🎁🎁🎁🎁🎁🎁🎁🎁