RSpec: Messages in Specific Order

tl;dr

You can use ordered to ensure that messages are received in a specific order.

Example

expect(ClassA).to receive(:call_first).ordered
expect(ClassB).to receive(:call_second).ordered
expect(ClassB).to receive(:call_third).ordered

#ordered supports further chaining

Example

RSpec: Composing a custom matcher from existing matchers

When you find similar groups of expect calls in your tests, you can improve readability by extracting the group into its own matcher. RSpec makes this easy by allowing matchers to call other matchers.

Example

The following test checks that two variables foo and bar (1) have no lowercase characters and (2) end with an exclamation mark:

expect(foo).to_not match(/[a-z]/)
expect(foo).to end_with('!')

expect(bar).to_not match(/[a-z]/)
expect(bar).to end_with('!')

We can extract the repeated matcher chains into a custom m...

Ruby: Using `sprintf` to replace a string at fixed named references

The sprintf method has a reference by name format option:

sprintf("%<foo>d : %<bar>f", { :foo => 1, :bar => 2 }) # => 1 : 2.000000
sprintf("%{foo}f", { :foo => 1 })                      # => "1f"

The format identifier %<id> stands for different data types to be formatted, such as %f for floats:

sprintf('%f', 1) # => 1.000000

Example:

This is quite useful to replace ...

Using Rationals to avoid rounding errors in calculations

Ruby has the class Rational which allows you to store exact fractions. Any calculation on these variables will now use fractional calculations internally, until you convert the result to another data type or do a calculation which requires an implicit conversion.

Example use case:

Lets say you want to store the conversion factor from MJ to kWh in a variable, which is 1/3.6. Using BigDecimals for this seems like a good idea, it usually helps with rounding errors over a float, but the...

A reasonable default CSP for Rails projects

Every modern Rails app should have a Content Security Policy enabled.

Very compatible default

The following "default" is a minimal policy that should

  • "just work" for almost all applications
  • give you most of the benefits of a CSP

In your config/initializers/content_security_policy.rb, set

Rails.application.config.content_security_policy do |policy|
  policy.object_src :none
  policy.script_src :unsafe_eval, :strict_dynamic, :https # Browsers with support for "'strict-dynamic'" will ignore "https:"
  po...

How to: Upgrade CarrierWave to 3.x

While upgrading CarrierWave from version 0.11.x to 3.x, we encountered some very nasty fails. Below are the basic changes you need to perform and some behavior you may eventually run into when upgrading your application. This aims to save you some time understanding what happens under the hood to possibly discover problems faster as digging deeply into CarrierWave code is very fun...

Whitelists and blacklists

The following focuses on extension allowlisting, but it is the exact same thing for content type allowlisting with the `content_ty...

Zeitwerk: How to collapse folders in Rails

All direct child directories of app are automatically added to the eager- and autoload paths. They do NOT create a module for namespacing. This is intuitive, since there normally is no module Model, or module Controller. If you want to add a new base directory, there's no additional config needed.

Example

app
├── controllers
├── helpers
├── inputs # No config needed 
├── mailers
├── models
├── uploaders # No config needed
├── util # No config needed
└── workers # No config needed

Sometimes it's handy to group files wit...

Solving "TypeError (nil can't be coerced into Integer)" in the Rails console / IRB

On the Rails console, assigning an object to a variable can lead to this strange error (without stacktrace):

irb > recipient = Recipient.find(123)
Traceback (most recent call last):
TypeError (nil can't be coerced into Integer)
irb > recipient
#<Recipient ...

The error is only in the output – the assignment is working. It only occurs when using the --nomultiline option, and thus [only with IRB 1.2.0+ and before Ruby 3](https://github.com/makandra/geordi/blob...

Ruby: `extend` extends the singleton class's inheritance chain

In the discussion of the difference between include and extend in Ruby, there is a misconception that extend would add methods to the singleton class of a ruby object as stated in many posts on this topic. But in fact, it is added to the ancestors chain of the singleton class! Even though it is technically not the same, practically this can be considered the same in most use cases.

Example

This means, that we are able to overwrite these methods or call the parent version with super depending in which order and in whi...

Heads Up: Selenium 4 uses a binary to determine the chromedriver

I recently stumbled over a problem that my feature tests broke in CI because of a mismatching chromedriver version.

In this specific project we have a fixed Chromium version in a Debian 12 environment instead of Chrome. The tests however used a recent chrome version instead.

$ chromedriver --version
ChromeDriver 117.0.5938.149 (e3344ddefa12e60436fa28c81cf207c1afb4d0a9-refs/branch-heads/5938@{#1539})
$ chromium --version
Chromium 117.0.5938.149 built on Debian 12.1, running on Debian 12.1

> WARN Selenium [:selenium_manager] The chromed...

Heads up: Quering array columns only matches equally sorted arrays

Given you have an array column like this:

create_table "users", force: :cascade do |t|
  t.integer "movie_ids", default: [], array: true
end

You might think that the following queries yield the same result:

User.where(movie_ids: [16, 17])
User.where(movie_ids: [17, 16])

Turn's out - they are not! They do care about array ordering more than I do.

To query for identical arrays independent of their order you have to either:

  1. Sort both the query and database content. If you're on Rails 7.1 you can use the new [`normal...

RSpec: Leverage the power of Capybara Finders and Matchers for view specs

View specs are a powerful tool to test several rendering paths by their cases instead of using a more costing feature spec. This is especially useful because they become quite convenient when used with Capybara::Node::Finders and Capybara::RSpecMatchers. This allows to wirte view unit specs as you can isolate specific part...

Spreewald, Cucumber: Selector for the nth element

The recommended additional setup of the spreewald gem, a useful set of cucumber steps, includes adding a file for defining custom selectors which can be used as prose within steps:

When I follow "Edit" within the controls section

Where the controls section can be any arbitrary defined css selector within selectors.rb


Often it can be useful to select the nth element of a specific selector. Luckily, this can ...

RSpec: Efficiently rerunning failed examples during development

Note

Don't use reruns as a mean to work around flaky tests. You should always try to fix those instead of rerunning them regularly.

Setup

Configure RSpec to persist the result of your test runs to a file. This is necessary to be able to rerun examples.
Add this to your spec/spec_helper.rb :

  config.example_status_persistence_file_path = 'spec/examples.txt'

Rerun all failed examples using --only-failures

bundle exec rspec --only-failures (or `...

Using Ruby's Method objects for inspecting methods

Do you remember finding where a method is defined?

I recently learned from a senior colleague that Method objects are quite useful within a debugging feast to find out the currently defined internals of methods, because they are either called within the current context or because you want to learn something about the API of the current objects.

Why is this useful?

This is especially useful since Ru...

Use rbenv-each to run a command for every installed Ruby version

The linked rbenv plugin rbenv-each is very helpful to keep QoL gems up to date that are not part of the Gemfile.

For example, you can bump the geordi version for all your rubies with the following command:

rbenv each gem update geordi

Another useful example would be to bulk-update bundler or rubygems.

Note that rbenv-each hasn't been updated since 2018, but it is fully functiona...

Don't use log level :debug in your production environments

Catch phrase

You don't want sensitive user data in your logs.

Background

Rails per default filters sensitive data like passwords and tokens and writes [FILTERED] to the logs. The code which is responsible for enabling that usually lives in filter_parameter_logging.rb (Rails.application.config.filter_parameters). Here is an example of a filtered log entry:

Unfiltered:
`User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."token" = $1 LIMIT $2 [["token", "secret-token"], ["LIMIT", 1]]`

After the filter is appl...

Upgrade Rails: Awareness list

Disclaimer

This card is a collection of guides and things to have in mind when upgrading to a specific version. It is not meant to be complete, so please feel free to contribute!

General workflows

Upgrade to Rails 7

Advanced plotting in Ruby with Gnuplot

Besides Plotting graphs in Ruby with Gruff, which comes handy for many uses cases, you sometimes might need configuration for more advanced plots, e.g. for academic concerns. Then using Gnuplot, the first academic open source plotting software, might be a good option.

There are several wrappers for Ruby available and I mainly looked at one of the two most frequently used ones, which are [ruby_gnuplot](https://github.com/rdp/ruby_gnuplot...

Split your parallel tests by execution time and keep execution logs up to date

Both knapsack and parallel_tests have the option to split groups by historic execution time. The required logs for this might be outdated since you manually have to update and push them into your repository.

The following card includes an option how you can keep them consistently up to date with no extra effort locally and/or remotely.

How to always split by execution logs

Parallel Tests

The parallel_tests gem has the option flag `--group...

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.


...

Git restore vs. reset for reverting previous revisions

The git doc states on the difference of these two commands:

  • git-restore[1] is about restoring files in the working tree from either the index or another commit. This command does not update your branch. The command can also be used to restore files in the index from another commit.
  • git-reset[1] is about updating your branch, moving the tip in order to add or remove commits from the branch. This operation changes the commit history.

git reset can also be used to restore th...

redirect_to and redirect

There are multiple ways to redirect URLs to a different URL in Rails, and they differ in small but important nuances.

Imagine you want to redirect the following url https://www.example.com/old_location?foo=bar to https://www.example.com/new_location?foo=bar.

Variant A

You can use ActionController::Redirecting#redirect_to in a controller action

class SomeController < ActionController::Base
  def old_location
    redirect_to(new_location_url(params.permit(:foo))) 
  end
end

This will:

  • It will redirect with a 302 st...

Do not pass params directly into url_for or URL helpers

Rails' url_for is useful for generating routes from a Hash, but can lead to an open redirect vulnerability.

Your application's generated route methods with a _url suffix are also affected because [they use url_for unter the hood](https://github.com/rails/rails...