As you might know from previous posts, I keep a Rails playground project around. That’s a small application with a bunch of models, controllers and accompanying tests. It allows me to quickly try out a new gem that was mentioned in a blog post, see the effects of a configuration flag, or quickly prototype other ideas.
Sometimes however, even that very simple Rails application is too big. For example, when I want to share the application with friends or the internet. A default Rails application consists of a bunch of files that span multiple directories. That’s no longer suitable for a GitHub Gist or a simple email. Also, telling people to look at a bunch of files, but ignoring others, isn’t too easy. What’s important, what can be ignored?
Rails is a great framework and it’s really damn easy to start a new project, but I sometimes miss the beauty of Sinatra applications, where everything is contained in a single file - easy to ready, easy to modify, easy to share.
Turns out, there’s a way how to put all of your Rails application code into a self-executing file. I first discovered this while browsing the Rails’ GitHub issues, where people would share a self-contained rails application in a single file. Later on I noticed that those bug report templates are even mentioned in the Rails Guides.
Let’s look at an example that even integrates a 3rd party gem, like
factory_girl_rails, and can even handle simple views. I’ll walk through it block by block, see the end of this post for the full snippet.
The first building block we’re using is Bundler’s inline feature, which lets us specify our dependencies the same way as with a traditional
Gemfile, but without the need to create a separate file. Note that this won’t create a
Gemfile.lock, so you’d want to be rather strict when defining versions. By passing
true to the
gemfile block, we can even install gems automatically when we run the script.
begin require "bundler/inline" rescue LoadError => e $stderr.puts "Bundler version 1.10 or later is required. Please update your Bundler" raise e end gemfile(true) do source "https://rubygems.org" gem "rails" gem "pg" gem "factory_girl_rails" end
Next we’ll require the parts of Rails that we actually wanna use,
active_controller/railtie, and configure ActiveRecord to use our local installation of postgres, with a database called
require "active_record" require "action_controller/railtie" ActiveRecord::Base.establish_connection(adapter: "postgresql", database: "railstestdb") ActiveRecord::Base.logger = Logger.new(STDOUT)
Let’s continue with our ActiveRecord migrations. For this app, we’ll need a book model, that’s associated to categories via a categorization join model. You can write your migrations as you would with individual files.
ActiveRecord::Schema.define do create_table :books, force: true do |t| t.string :name t.timestamps end create_table :categories, force: true do |t| t.string :name t.timestamps end create_table :categorizations, force: true do |t| t.references :book t.references :category t.boolean :primary, default: false, null: false t.timestamps end end
Now we continue with the three models and their associations:
class Book < ActiveRecord::Base has_many :categorizations has_many :categories, through: :categorizations end class Category < ActiveRecord::Base has_many :categorizations has_many :books, through: :categorizations def self.primaries Category.joins(:categorizations).merge(Categorization.primaries) end end class Categorization < ActiveRecord::Base belongs_to :book belongs_to :category def self.primaries where(primary: true) end end
We’ll add a class
TestApp that inherits from
Rails::Application, which will draw our routes (just
/primary_categories, for now).
class TestApp < Rails::Application secrets.secret_token = "secret_token" secrets.secret_key_base = "secret_key_base" config.logger = Logger.new($stdout) Rails.logger = config.logger routes.draw do resources :primary_categories, only: :index end end
Next we define a
PrimaryCategoriesController, that will return all categories which have been marked as
primary in a book-category association (via the
Categorizations join model).Note that we can even render templates with ERB syntax via
class PrimaryCategoriesController < ActionController::Base include Rails.application.routes.url_helpers def index @primary_categories = Category.primaries render inline: "# of primary categories: <%= @primary_categories.count %>" end end
gemfile block we’ve references
factory_girl, so let’s define a factory for our book model that gives us two traits: a book with a primary category, and a book with a secondary category (the
primary flag is defined on the
Categorization join model). Also add two category factories.
FactoryGirl.define do factory :book do name "Thing Explainer: Complicated Stuff in Simple Words" trait :with_primary_category do after(:create) do |book, _| book.categorizations << Categorization.create!(category: create(:science_category), book: book, primary: true) end end trait :with_secondary_category do after(:create) do |book, _| book.categorizations << Categorization.create!(category: create(:fun_facts_category), book: book, primary: false) end end end factory :science_category, class: Category do name "Science & Scientists" end factory :fun_facts_category, class: Category do name "Trivia & Fun Facts" end end
Now onto the tests. We’ll
require "minitest/autorun", so that our tests are invoked automatically when we run the script.First we test our
Category.primaries method, and ensure that it returns only categories that are marked as primary in an association with a book.
require "minitest/autorun" class CategoryTest < Minitest::Test def test_primary_categories FactoryGirl.create(:book, :with_primary_category, :with_secondary_category) assert_equal Category.primaries, [Category.find_by_name('Science & Scientists')] end end
Finally, we write a controller test that assures that we correctly return the number of primary categories for the
class PrimaryCategoriesTest < Minitest::Test include Rack::Test::Methods def test_index get "/primary_categories" assert last_response.ok? assert_equal last_response.body, "# of primary categories: 1" end private def app Rails.application end end
That’s all! You can find the complete snippet here. Simply download or clone it (via git), create the test database via
createdb railstestdb and invoke the script via
ruby rails_single_file.rb. That will take a few seconds, depending on which gems you already have on your local machine. After that, it will prepare the application, and run the tests:
Finished in 0.332263s, 6.0193 runs/s, 9.0290 assertions/s. 2 runs, 3 assertions, 0 failures, 0 errors, 0 skips
I really like this approach, and it will allow me to keep experiments around in separate files, making them easier accessible than browsing through the git history of my playground project.