Rails Engines, Rspec, Zeus and Guard - Part 1
11 Mar 2015Last 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.