Rails: How to stub the env in Rails 7+

Rails 7.1 added a new method Rails.env.local?. If you want to stub the Rails env correctly, use ActiveSupport::EnvironmentInquirer like this:

# check if the value of stubbed_env is valid
allow(Rails).to receive(:env).and_return(ActiveSupport::EnvironmentInquirer.new(stubbed_env.to_s))

Rails: Restrict deletion of a parent record using scoped associations

When deletion of a parent record should be blocked by one or more conditions on the same child table, declare a has_many association per condition with its own scope and dependent: :restrict_with_error.

The pattern

The example below uses two scoped associations to keep things readable, but the same approach works with one, three, or any number of independent restrictions.

class Account < ApplicationRecord
  has_many :subscriptions

  has_many :active_subscriptions,
    -> { where(status: :active) },
    class_name: 'Subscri...

Rails: When to use :inverse_of in has_many, has_one or belongs_to associations

When you have two models in a has_many, has_one or belongs_to association, the :inverse_of option in Rails tells ActiveRecord that they're two sides of the same association.

Example with a has_many / belongs_to association:

class Forum < ActiveRecord::Base
  has_many :posts, inverse_of: :forum
end

class Post < ActiveRecord::Base
  belongs_to :forum, inverse_of: :posts
end

Knowing the other side of the same association Rails can optimize object loading so forum and forum.posts[0].forum will reference the same o...

Rails: Overwriting default accessors

All columns of a model's database table are automagically available through accessors on the Active Record object.

When you need to specialize this behavior, you may override the default accessors (using the same name as the attribute) and simply call the original implementation with a modified value. Example:

class Poet < ApplicationRecord

  def name=(value)
    super(value.strip)
  end

end

Note that you can also avoid the original setter and directly read/write from/to the instance's attribute storage. However this is dis...

How to set up database_cleaner for Rails with Cucumber and RSpec

Add gem 'database_cleaner' to your Gemfile. Then:

Cucumber & Rails 3+

# features/support/database_cleaner.rb

DatabaseCleaner.clean_with(:deletion) # clean once, now
DatabaseCleaner.strategy = :transaction
Cucumber::Rails::Database.javascript_strategy = :deletion

Cucumber & Rails 2

The latest available cucumber-rails for Rails 2 automatically uses database_cleaner when cucumber/rails/active_record is required -- but only if transactional fixtures are off. To have database_cleaner work correctly:

  1. Add the ...

How to use Simplecov to find untested code in a Rails project with RSpec and Cucumber

Simplecov is a code coverage tool. This helps you to find out which parts of your application are not tested.

Integrating this in a rails project with rspec, cucumber and parallel_tests is easy.

  1. Add it to your Gemfile and bundle

    group :test do
      gem 'simplecov', require: false
    end
    
  2. Add a .simplecov file in your project root:

    SimpleCov.start 'rails' do
      # any custom configs like groups and filters can be here at a central place
      enable_cov...
    

Working with or without time zones in Rails applications

Rails supports time zones, but there are several pitfalls. Most importantly because Time.now and Time.current are completely different things and code from gems might use one or the other.

Especially configuring an application that cares only about one time zone is a bit tricky.

The following was tested on Rails 5.1 but should apply to Rails 4.2 as well.

Using only local time

Your life will be easier if your application does not need to support time zones. Disable them like this:

config.time_zone = 'Berlin' # Your local ...

How to define a table name prefix for all Rails models in a given namespace

ActiveRecord computes table names of model classes, and results are usually just like you'd expect.
Adding a prefix for all classes in a namespace can feel a odd or broken, but does not have to be. It's actually very easy when done right.

Summary (tl;dr)

Here's the short version: Define a table_name_prefix method in the namespace module, and do not define any table_name_prefix in ActiveRecord classes inside of it. If this sounds familiar, we have [a card about using it already](https://makandracards.com/makandra/47198-rails-namespac...

Lightning Talk: Coverage based Test Case Prioritization in Ruby on Rails

For my computer science bachelor's thesis I programmed and evaluated a CLI Test Case Prioritization (TCP) tool for makandra. It has been written as a Ruby Gem and was tested and evaluated against one Ruby on Rails project. This card will summarize and present the research results, the evaluation and the programmed CLI tool.

The code has been published for educational purposes on GitHub. The german bachelor's thesis has also been included for download at the end.


...

Customizing Rails error messages for models and attributes

Rails has generic error messages you can define in your locale .yml files. You may override those application-wide error messages using model or attribute scope like this:

en:
  activerecord:
    errors:
      messages:
        invalid: is invalid # used for any invalid attribute in the application
        presence: etc
      models:
        car:
          invalid: does not work # used for invalid car attributes
          attributes:
            driver:
              invalid: not allowed to drive # used if the car's driver attribut...

Rails: Report CSP Violations to a log file

You can report CSP violations to a log file. Note that there will be a lots of noise, that is not related to errors in your application but raised from various browser extensions of your visitors.

Add the endpoint in you CSP config/initializers/content_security_policy.rb:

Rails.application.configure do
  config.content_security_policy do |policy|
    # Settings for the policy

    policy.report_uri '/content_security_policy_report'
  end
end

Create a controller app/controllers/content_security_policy_reports_controller.rb ...

Rails: Testing file downloads with request specs

tl;dr

Prefer request specs over end-to-end tests (Capybara) to joyfully test file downloads!

Why?

Testing file downloads via Capybara is not easy and results in slow and fragile tests. We tried different approaches and the best one is just okay.

Tests for file downloads via Capybara ...

  • ... are slow,
  • ... are fragile (breaks CI, breaks if Selenium driver changes, ...),
  • ... need workarounds for your specia...

Rails: Using PostgreSQL full-text search without a gem

PostgreSQL can cosplay as a full-text search engine. It doesn't have the features or fidelity of ElasticSearch or Algolia, but it's good enough if you just need to search and rank large volumes of text.

This card will teach you how to index, search and rank your Rails models in a PostgreSQL full-text index. We will do this without using any gems aside from ActiveRecord. While there are gems like pg_search or pg_fulltext, manual integration requires very...

Rails: Keeping structure.sql stable between developers

Why Rails has multiple schema formats

When you run migrations, Rails will write your current database schema into db/schema.rb. This file allows to reset the database schema without running migrations, by running rails db:schema:load.

The schema.rb DSL can serialize most common schema properties like tables, columns or indexes. It cannot serialize more advanced database features, like views, procedures, triggers or custom ditionaries. In these cases you must switch to a SQL based schema format:

# in application.rb
config.a...

Rails: Talking to the database without instantiating ActiveRecord objects

Instantiating ActiveRecord objects comes expensive. To speed up things, you can choose a more direct way to talk to your database: the ActiveRecord::ConnectionAdapters::DatabaseStatements module.

Using the module and its methods is not suggested in the usual in-app workflow, as validations, callbacks, custom getters/setters etc. are ignored. However, for database-centered stuff like migrations, these fill the gap between writing pure SQL and full...

Rails: Accessing strong parameters

Rails wraps your parameters into an interface called StrongParameters. In most cases, your form submits your data in a nested structure which goes hand in hand with the strong parameters interface.

Example:

curl -X POST -d "user[name]=bob" https://example.com/users
class UsersController
  def create
    User.create!(params.expect(user: [:name])) # Or User.create!(params.require(:user).permit(:name))
  end
end

This works well most of the time...

How to speed up JSON rendering with Rails

I was recently asked to optimize the response time of a notoriously slow JSON API endpoint that was backed by a Rails application.
While every existing app will have different performance bottlenecks and optimizing them is a rabbit hole of arbitrary depth, I'd like to demonstrate a few techniques which could help reaching actual improvements.

The baseline

The data flow examined in this card are based on an example barebone rails app, which can be used to reproduce the r...

Rails: Prefer parsing dates with Date.strptime()

It is very common to parse dates from strings. It seems obvious to use Date.parse for this job. However this method does not validate the input and tries to guess the format of the string.

This can lead to a very unexpected results:

Date.parse('Foobar_09_2018')
# Tue, 09 Oct 2018

In most of the cases it would be better to use Date.strptime as you can provide a date or time pattern to match against.

Date.strptime('Foobar_09_2018', '%d_%m_%Y')
# ArgumentError (invalid strptime format - `%d_%m_%Y')

Date.strptime('01_09...

How to: Rails cache for individual rspec tests

Rails default config uses the ActiveSupport::Cache::NullStore and disables controller caching for all environments except production:

config.action_controller.perform_caching = false
config.cache_store = :null_store

If you want to test caching you have at least two possibilities:

  1. Enable caching for every test (not covered by this card and straightforward)
  2. Enable caching for individual test

Enable caching for individual test (file cache)

1. Leave the defau...

Rails: Looking up constants by their name string

TL;DR: Rails ships two methods to convert strings to constants, constantize and safe_constantize. Neither is safe for untrusted user input. Before you call either method you must validate the input string against an allowlist. The only difference between the two methods is that unresolvable constants raise an error with constantize, but return nil with safe_constantize. If you validate the input string against an allowlist, an error should never happen.

Preventing Dangerous Lookups

Suppose an application uses eit...

Rails: Concurrency-safe API request counting with `upsert`

Concurrent counter increments are a race-condition trap. The Rails idiom (load the record, increment in Ruby, save) breaks under concurrent writes. Between the read and the write, another transaction can read the same value and produce a lost update.

The Postgres-native fix is to compute the increment server-side inside an atomic INSERT ... ON CONFLICT DO UPDATE statement and gate it by a unique index.

The shape that fails

counter = RequestCounter.find_or_create_by(api_key:, billing_period:)
counter.update!(request_count: co...

Rails asset pipeline: Using ESNext without a transpiler

If your app does not need to support IE11, you can use most ES6 features without a build step. Just deliver your plain JavaScript without transpilation through Babel or TypeScript, and modern browsers will run them natively.

Features supported by all modern browsers include:

  • fat arrow functions (() => { expr })
  • let / const
  • class
  • async / await
  • Promises
  • Generators
  • Symbols
  • Rest arguments (...args)
  • Destructuring

You won't be able to use import and export, or use npm modules.

See this [ES6 compatibility mat...

Apply a new callback to existing records

So you added a new callback to your model that (e.g.) caches some data when it is saved. Now you need to run that callback for the 10000 existing records in the production database. You have two options here:

  1. Write a clever migration, possibly by embedding the model into the migration script.
  2. Open the Rails console after deployment and re-save every single record. You should probably add two chores to your issue tracker so you won't forget t...

Collection of Rails development boosting frameworks

Development environment setup

Rails Composer

Basically a comprehensive Rails Template. Prepares your development environment and lets you select web server, template engine, unit and integration testing frameworks and more.

Generate an app in minutes using an application template. With all the options you want!

Code generators

Rails Bricks

A command line wizard. Once you get it running, it creates sleek applications.

RailsBricks enables you to cre...