Rails Engines, Rspec, Zeus and Guard - Part 1

11 Mar 2015, by Sia Sajjadi

Last week we started work on a project and it made more sense to build it as a rails engine, it takes a bit of configuration. So here are the steps you can take to build a new rails 4 engine that delegates user authentication to the host app and has a rich testing environment.

Gem versions:
|-------------|--------|
| mri         | 2.1.5  |
| rails       | 4.1.8  |
| zeus        | 0.15.4 |
| rspec       | 3.1.0  |
| guard       | 2.10.2 |
| guard-rspec | 4.3.1  |
| guard-zeus  | 2.0.1  |

Step 1 - Create a mountable engine

===========================================

1.1 Generate the new engine

I made sure to use the same rails version as my host app, and I was using ruby 2.1.5 to generate.

rails _4.1.8_ plugin new pishi -T --mountable --dummy-path=spec/dummy

1.2 Add a ruby version

cd pishi
echo 2.1.5 > .ruby-version

1.3 Commit

git add .
git commit -m "Create Engine Pishi"

Step 2 - Add Gems

====================

2.1 Add Development Gems to Gemfile

Gemfile

source "http://rubygems.org"

gemspec

group :development, :test do

  # You need these
  gem "rspec-rails"
  gem "guard"
  gem "guard-rspec"
  gem "guard-zeus"
  gem "guard-bundler"

  gem "factory_girl_rails"

  # You don't need these, but I use them
  gem "rb-fsevent"
  gem "ffaker"
  gem "pry"

end

group :development do
  # You don't need these, but I use them
  gem "better_errors"
  gem "awesome_print"
  gem "brakeman"
end

group :test do
  # You don't need these, but I use them
  gem "vcr"
  gem "webmock"
  gem "capybara"
  gem "simplecov", require: false
end

2.2 Bundle Install

bundle Install

2.3 Commit

git add .
git commit -m "Add Gems"

3 Configure To Use Rspec & Factory Girl

==========================================

3.1 Configure Engine

lib/pishi/engine.rb

# require dependencies declared in your gemspec
Gem.loaded_specs['pishi'].dependencies.each do |d|
 require d.name
end

module Pishi
  class Engine < ::Rails::Engine

    isolate_namespace Pishi

    config.generators do |g|
      g.test_framework :rspec
      g.fixture_replacement :factory_girl, dir: "spec/factories"
    end

  end
end

3.2 Install RSpec

rails generate rspec:install

spec/spec_helper.rb

# This file is copied to spec/ when you run "rails generate rspec:install"

# Add this to load files in spec/support
Dir[File.expand_path("../support/**/*.rb", __FILE__)].each {|f| require f}

RSpec.configure do |config|

  config.expect_with :rspec do |expectations|
    expectations.include_chain_clauses_in_custom_matcher_descriptions = true
  end

  config.mock_with :rspec do |mocks|
    mocks.verify_partial_doubles = true
  end

  config.filter_run :focus
  config.run_all_when_everything_filtered = true

  config.disable_monkey_patching!

  if config.files_to_run.one?
    config.default_formatter = "doc"
  end

  config.profile_examples = 10

  config.order = :random

  Kernel.srand config.seed

end

spec/rails_helper.rb

# This file is copied to spec/ when you run "rails generate rspec:install"
ENV["RAILS_ENV"] ||= "test"

require "spec_helper"

# Add this to load your dummy app's environment file. This file will require
# the application.rb file in the dummy directory, and initialise the dummy app.
# Very simple, now you have your dummy application in memory for your specs.
require File.expand_path("../dummy/config/environment", __FILE__)

require "rspec/rails"
require "capybara/rspec"
require "vcr"

ActiveRecord::Migration.maintain_test_schema!

RSpec.configure do |config|

  config.include Capybara::RSpecMatchers

  config.use_transactional_fixtures = true

  config.infer_spec_type_from_file_location!

end

3.3 Commit Changes

git add .
git commit -m "Configure for Rspec and Factory Girl"

4 Configure Guard with Zeus

==============================

4.1 Create a Guard file

guard init zeus

4.1.1 (Optional) Make generic watch directives to use with other guard strategies

Some times coworkers don't like to use Zeus, or it's not practical for a particular set of tests.
So load your watch directives into a Proc, and pass it to each guard strategy via &block

Guardfile

guard :bundler do
  watch("Gemfile")
  watch(/^.+\.gemspec/)
end

watch_directives = Proc.new do
  require "ostruct"

  # Generic Ruby apps
  rspec = OpenStruct.new
  rspec.spec = ->(m) { "spec/#{m}_spec.rb" }
  rspec.spec_dir = "spec"
  rspec.spec_helper = "spec/spec_helper.rb"

  watch(%r{^spec/.+_spec\.rb$})
  watch(%r{^lib/(.+)\.rb$})     { |m| rspec.spec.("lib/#{m[1]}") }
  watch(rspec.spec_helper)      { rspec.spec_dir }

  # Rails example
  rails = OpenStruct.new
  rails.app = %r{^app/(.+)\.rb$}
  rails.views_n_layouts = %r{^app/(.*)(\.erb|\.haml|\.slim)$}
  rails.controllers = %r{^app/controllers/(.+)_controller\.rb$}
  rails.routes = "config/routes.rb"
  rails.app_controller = "app/controllers/application_controller.rb"
  rails.spec_helper = "spec/rails_helper.rb"
  rails.spec_support = %r{^spec/support/(.+)\.rb$}
  rails.views = %r{^app/views/(.+)/.*\.(erb|haml|slim)$}

  watch(rails.app) { |m| rspec.spec.(m[1]) }
  watch(rails.views_n_layouts) { |m| rspec.spec.("#{m[1]}#{m[2]}") }
  watch(rails.controllers) do |m|
    [
      rspec.spec.("routing/#{m[1]}_routing"),
      rspec.spec.("controllers/#{m[1]}_controller"),
      rspec.spec.("acceptance/#{m[1]}")
    ]
  end

  watch(rails.spec_support)    { rspec.spec_dir }
  watch(rails.spec_helper)     { rspec.spec_dir }
  watch(rails.routes)          { "spec/routing" }
  watch(rails.app_controller)  { "spec/controllers" }

  # Capybara features specs
  watch(rails.views)     { |m| rspec.spec.("features/#{m[1]}") }

end

# you can now add
# guard :rspec, cmd: "bundle exec rspec" do, &watch_directives
# and you'll only need to keep track of one set of directives

guard :zeus, &watch_directives

4.2 Create Zeus configs

engine_plan.rb

require "zeus/rails"

ROOT_PATH = File.expand_path(Dir.pwd)
ENVIRONMENT_PATH  = File.expand_path("spec/dummy/config/environment",  ROOT_PATH)
ENV_PATH  = File.expand_path("spec/dummy/config/environment",  ROOT_PATH)
BOOT_PATH = File.expand_path("spec/dummy/config/boot",  ROOT_PATH)
APP_PATH  = File.expand_path("spec/dummy/config/application",  ROOT_PATH)
ENGINE_ROOT = File.expand_path(Dir.pwd)
ENGINE_PATH = File.expand_path("lib/pishi/engine", ENGINE_ROOT)

class EnginePlan < Zeus::Rails
end

Zeus.plan = EnginePlan.new

zeus.json

{
  "command": "ruby -rubygems -r./engine_plan -eZeus.go",

  "plan": {
    "boot": {
      "default_bundle": {
        "development_environment": {
          "prerake": {"rake": []},
          "runner": ["r"],
          "console": ["c"],
          "server": ["s"],
          "generate": ["g"],
          "destroy": ["d"],
          "dbconsole": []
        },
        "test_environment": {
          "test_helper": {"test": ["rspec", "testrb"]}
        }
      }
    }
  }
}

4.3 Commit Changes

git add .
git commit -m "Configure Zeus"

Step 5 Inherit From Host Application

=======================================

5.1 Inherit ApplicationController

app/controllers/pishi/application_controller.rb

module Pishi
  # This line will inherit from the host applications ApplicationController,
  # giving access to your authentication, current_user, etc...
  class ApplicationController < ::ApplicationController

  end
end

Explanation

There are several ways to authenticate users, authorise them, etc... something like

  before_action :authenticate_user

in the host ApplicationController should just work now. If your particular case is more involved, then you'll know what to do.

In any case, in our host application we use a test helper like:

spec/support/authentication_helper.rb

def sign_in(user)
  session[:authentication_token] = user.authentication_token
end

So in our engine, which doesn't need to know about users at all, I stub it like so

spec/support/authentication_helper.rb

def sign_in(user = nil)
  user ||= OpenStruct.new authentication_token: SecureRandom.hex(6)
  session[:authentication_token] = user.authentication_token
  user
end

If in the future I do need a more specialised user object, I can just pass it in.

5.2 Commit Changes

git add .
git commit -m "Configure ApplicationController"

Optional - Use Postgres instead of SQlite

============================================

Our host application uses postgresql, and I'd like to default to that for now.

spec/dummy/config/database.yml

default: &default
  adapter: postgresql
  pool: 5
  timeout: 5000

development:
  <<: *default
  database: pishi_development
  username: develop
  password: develop

test:
  <<: *default
  database: pishi_test
  username: test
  password: test

Remove sqlite and add pg to the gemspec

pishi.gemspec

$:.push File.expand_path("../lib", __FILE__)

# Maintain your gem's version:
require "pishi/version"

# Describe your gem and declare its dependencies:
Gem::Specification.new do |s|
  s.name        = "pishi"
  s.version     = Pishi::VERSION
  s.authors     = ["TODO: Your name"]
  s.email       = ["TODO: Your email"]
  s.homepage    = "TODO"
  s.summary     = "TODO: Summary of Pishi."
  s.description = "TODO: Description of Pishi."
  s.license     = "MIT"

  s.files = Dir["{app,config,db,lib}/**/*", "Rakefile", "README.rdoc"]

  s.required_ruby_version = '~> 2.1'

  s.add_dependency "rails", "~> 4.1.8"
  s.add_dependency "pg"

end

Fin

======

You should now have a functioning enginge with a good testing suite, just run

guard start

And make sure you have the spec files that your Guardfile will look for, or you'll get a bunch of random errors.

Notes

Remember to use bin/rails g to generate controllers, models, scaffolds, etc... I haven't yet checked why zeus g doesn't work properly.


Cookies help us deliver our services. By using our services, you agree to our use of cookies.