Read more

Best practices: Writing a Rails script (and how to test it)

Dominik Schöler
January 30, 2024Software engineer at makandra GmbH

A Rails script lives in lib/scripts and is run with bin/rails runner lib/scripts/.... They are a simple tool to perform some one-time actions on your Rails application. A Rails script has a few advantages over pasting some prepared code into a Rails console:

  • Version control
  • Part of the repository, so you can build on previous scripts for a similar task
  • You can have tests (see below)
Illustration online protection

Rails Long Term Support

Rails LTS provides security patches for old versions of Ruby on Rails (2.3, 3.2, 4.2 and 5.2)

  • Prevents you from data breaches and liability risks
  • Upgrade at your own pace
  • Works with modern Rubies
Read more Show snapshot

Although not part of the application, your script is code and should adhere to the common quality standards (e.g. no spaghetti code). However, a script has a few special requirements. Please consider these when writing a Rails script.

Be idempotent

Your script may crash while halfway through its task. Running it again should not break anything, but continue where it left off.

Be atomic

Use database transactions for atomic changes, i.e. changes that must happen together or not at all.

ActiveRecord::Base.transaction do
  # Change something
  # Another, dependent change 

Be transparent

The script should always state what it is about to do, and await confirmation from the user. If a decision is needed, it should print all relevant information.

You can copy interaction helpers from Geordi Show snapshot .

When processing lists of records, print an identifier for each item. This way, you can watch it progress, and you'll know what the script was at in case it crashes.

Be robust

Your script will encounter invalid records, and! will crash your script. Either use if, or wrap the respective code like this:

  # If using transactions, nest them here
  # ...! ...
rescue ActiveRecord::RecordInvalid
  # Handle error

On error, log the record for manual care.

Consider testing your script

Not every script needs a test. However, sometimes you'll want to add some. Here is how. (Inspired by Functional core, imperative shell Show snapshot .)

  • Structure your script like this:
    # This class will be specced
    # To simplify this, it should not read input
    class YourScript
      # Methods offering functionality
    unless Rails.env.test?
      script =
      # All interactivity (i.e. reading input) goes here
  • Place tests in lib/scripts/spec and call them my_script_spec.rb. This way they're close to their corresponding script, but not included in regular full-app test runs. They can prove correctness of the script, while not breaking your test suite should your script outdate.
  • Structure your test like this:
    require_relative '../your_script_name'
    describe 'the purpose of your script' do
      subject { }
      it 'has output' do
        expect { subject.print_summary }.to output(/success/).to_stdout

Further reading

Dominik Schöler
January 30, 2024Software engineer at makandra GmbH
Posted by Dominik Schöler to makandra dev (2024-01-30 08:00)