Using a headless Rails for tasks and tests

Written . Tagged Ruby on Rails.

You might know that the Rails console gives you an app object to interact with:

1
2
3
4
5
6
7
8
>> app.items_path
# => "/items"
>> app.get "/items"
# => 200
>> app.response.body
# => "<!DOCTYPE html><html>My items!</html>"
>> app.response.success?
# => true

You might also know that this is the same thing you’re using in Rails integration tests:

1
2
get "/items"
expect(response).to be_success

In both cases you’re interacting with an ActionDispatch::Integration::Session.

Here are two more uses for it.

Rake tasks

If you have an app that receives non-GET webhooks, it’s a bit of a bother to curl those when you want to trigger them in development.

Instead, you can do it from a Rake task:

1
2
3
4
5
6
7
8
namespace :dev do
  desc "Fake webhook"
  task :webhook => :environment do
    session = ActionDispatch::Integration::Session.new(Rails.application)
    response = session.post "/my_webhook", { my: "data" }, { "X-Some-Header" => "some value" }
    puts "Done with response code #{response}"
  end
end

I used this in a current project to fake incoming GitHub webhooks.

You could of course make your controller a thin wrapper around some object that does most of the work, and just call that object from tasks, but the HTTP part isn’t neglible with things like webhooks, and it can be useful to go through the whole thing sometimes during development.

Non-interactive sessions in feature tests

Your Capybara tests can alternate between multiple interactive sessions very easily, which is super handy for testing real time interactions, e.g. a chat.

But Capybara only wants you to test through the fingers of a user. If the user doesn’t click to submit a form, you can’t easily trigger a POST request.

So if you want to test something like an incoming POST webhook during an interactive user session, you can again use our friend ActionDispatch::Integration::Session:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
it "reloads when a webhook comes in" do
  visit "/"

  expect_page_to_reload do
    the_heroku_webhook_is_triggered
  end
end

def expect_page_to_reload
  page.evaluate_script "window.notReloaded = true"
  yield
  sleep 0.01  # Sadly, we need this.
  expect(page.evaluate_script("window.notReloaded")).to be_falsy
end

def the_heroku_webhook_is_triggered
  session = ActionDispatch::Integration::Session.new(Rails.application)
  session.post("/heroku_webhook")
end

This too is extracted from a current project.