Add support for decoupled specs

Closes #424
This commit is contained in:
Andrew Haines 2013-01-18 16:40:57 +00:00
parent ef37241c10
commit a039671202
9 changed files with 416 additions and 18 deletions

View File

@ -264,6 +264,22 @@ In your `Spork.prefork` block of `spec_helper.rb`, add this:
require 'draper/test/rspec_integration'
```
### Isolated tests
In tests, Draper needs to build a view context to access helper methods. By default, it will create an `ApplicationController` and then use its view context. If you are speeding up your test suite by testing each component in isolation, you can eliminate this dependency by putting the following in your `spec_helper` or similar:
```ruby
Draper::ViewContext.test_strategy :fast
```
In doing so, your decorators will no longer have access to your application's helpers. If you need to selectively include such helpers, you can pass a block:
```ruby
Draper::ViewContext.test_strategy :fast do
include ApplicationHelper
end
```
## Advanced usage
### Shared Decorator Methods

View File

@ -24,10 +24,11 @@ module Draper
base.class_eval do
include Draper::ViewContext
extend Draper::HelperSupport
before_filter ->(controller) {
Draper::ViewContext.current = nil
Draper::ViewContext.current_controller = controller
}
before_filter do |controller|
Draper::ViewContext.clear!
Draper::ViewContext.controller = controller
end
end
end

View File

@ -1,37 +1,89 @@
require 'draper/view_context/build_strategy'
require 'request_store'
module Draper
module ViewContext
# Hooks into a controller or mailer to save the view context in {current}.
def view_context
super.tap do |context|
Draper::ViewContext.current = context
end
end
def self.current_controller
RequestStore.store[:current_controller] || ApplicationController.new
# Returns the current controller.
def self.controller
RequestStore.store[:current_controller]
end
def self.current_controller=(controller)
# Sets the current controller.
def self.controller=(controller)
RequestStore.store[:current_controller] = controller
end
# Returns the current view context, or builds one if none is saved.
def self.current
RequestStore.store[:current_view_context] ||= build_view_context
RequestStore.store[:current_view_context] ||= build
end
def self.current=(context)
RequestStore.store[:current_view_context] = context
# Sets the current view context.
def self.current=(view_context)
RequestStore.store[:current_view_context] = view_context
end
# Clears the saved controller and view context.
def self.clear!
self.controller = nil
self.current = nil
end
# Builds a new view context for usage in tests. See {test_strategy} for
# details of how the view context is built.
def self.build
build_strategy.call
end
# Configures the strategy used to build view contexts in tests, which
# defaults to `:full` if `test_strategy` has not been called. Evaluates
# the block, if given, in the context of the view context's class.
#
# @example Pass a block to add helper methods to the view context:
# Draper::ViewContext.test_strategy :fast do
# include ApplicationHelper
# end
#
# @param [:full, :fast] name
# the strategy to use:
#
# `:full` - build a fully-working view context. Your Rails environment
# must be loaded, including your `ApplicationController`.
#
# `:fast` - build a minimal view context in tests, with no dependencies
# on other components of your application.
def self.test_strategy(name, &block)
@build_strategy = Draper::ViewContext::BuildStrategy.new(name, &block)
end
# @private
def self.build_strategy
@build_strategy ||= Draper::ViewContext::BuildStrategy.new(:full)
end
# @deprecated Use {controller} instead.
def self.current_controller
ActiveSupport::Deprecation.warn("Draper::ViewContext.current_controller is deprecated (use controller instead)", caller)
self.controller || ApplicationController.new
end
# @deprecated Use {controller=} instead.
def self.current_controller=(controller)
ActiveSupport::Deprecation.warn("Draper::ViewContext.current_controller= is deprecated (use controller instead)", caller)
self.controller = controller
end
# @deprecated Use {build} instead.
def self.build_view_context
current_controller.view_context.tap do |context|
if defined?(ActionController::TestRequest)
context.controller.request ||= ActionController::TestRequest.new
context.request ||= context.controller.request
context.params ||= {}
end
end
ActiveSupport::Deprecation.warn("Draper::ViewContext.build_view_context is deprecated (use build instead)", caller)
build
end
end
end

View File

@ -0,0 +1,48 @@
module Draper
module ViewContext
# @private
module BuildStrategy
def self.new(name, &block)
const_get(name.to_s.camelize).new(&block)
end
class Fast
def initialize(&block)
@view_context_class = Class.new(ActionView::Base, &block)
end
def call
view_context_class.new
end
private
attr_reader :view_context_class
end
class Full
def initialize(&block)
@block = block
end
def call
controller.view_context.tap do |context|
context.singleton_class.class_eval(&block) if block
end
end
private
attr_reader :block
def controller
(Draper::ViewContext.controller || ApplicationController.new).tap do |controller|
controller.request ||= ActionController::TestRequest.new if defined?(ActionController::TestRequest)
end
end
end
end
end
end

View File

@ -0,0 +1,116 @@
require 'spec_helper'
def fake_view_context
double("ViewContext")
end
def fake_controller(view_context = fake_view_context)
double("Controller", view_context: view_context, request: double("Request"))
end
module Draper
describe ViewContext::BuildStrategy::Full do
describe "#call" do
context "when a current controller is set" do
it "returns the controller's view context" do
view_context = fake_view_context
ViewContext.stub controller: fake_controller(view_context)
strategy = ViewContext::BuildStrategy::Full.new
expect(strategy.call).to be view_context
end
end
context "when a current controller is not set" do
it "uses ApplicationController" do
view_context = fake_view_context
stub_const "ApplicationController", double(new: fake_controller(view_context))
strategy = ViewContext::BuildStrategy::Full.new
expect(strategy.call).to be view_context
end
end
it "adds a request if one is not defined" do
controller = Class.new(ActionController::Base).new
ViewContext.stub controller: controller
strategy = ViewContext::BuildStrategy::Full.new
expect(controller.request).to be_nil
strategy.call
expect(controller.request).to be_an ActionController::TestRequest
expect(controller.params).to eq({})
# sanity checks
expect(controller.view_context.request).to be controller.request
expect(controller.view_context.params).to be controller.params
end
it "adds methods to the view context from the constructor block" do
ViewContext.stub controller: fake_controller
strategy = ViewContext::BuildStrategy::Full.new do
def a_helper_method; end
end
expect(strategy.call).to respond_to :a_helper_method
end
it "includes modules into the view context from the constructor block" do
view_context = Object.new
ViewContext.stub controller: fake_controller(view_context)
helpers = Module.new do
def a_helper_method; end
end
strategy = ViewContext::BuildStrategy::Full.new do
include helpers
end
expect(strategy.call).to respond_to :a_helper_method
end
end
end
describe ViewContext::BuildStrategy::Fast do
describe "#call" do
it "returns an instance of a subclass of ActionView::Base" do
strategy = ViewContext::BuildStrategy::Fast.new
returned = strategy.call
expect(returned).to be_an ActionView::Base
expect(returned.class).not_to be ActionView::Base
end
it "returns different instances each time" do
strategy = ViewContext::BuildStrategy::Fast.new
expect(strategy.call).not_to be strategy.call
end
it "returns the same subclass each time" do
strategy = ViewContext::BuildStrategy::Fast.new
expect(strategy.call.class).to be strategy.call.class
end
it "adds methods to the view context from the constructor block" do
strategy = ViewContext::BuildStrategy::Fast.new do
def a_helper_method; end
end
expect(strategy.call).to respond_to :a_helper_method
end
it "includes modules into the view context from the constructor block" do
helpers = Module.new do
def a_helper_method; end
end
strategy = ViewContext::BuildStrategy::Fast.new do
include helpers
end
expect(strategy.call).to respond_to :a_helper_method
end
end
end
end

View File

@ -0,0 +1,117 @@
require 'spec_helper'
module Draper
describe ViewContext do
describe "#view_context" do
let(:base) { Class.new { def view_context; :controller_view_context; end } }
let(:controller) { Class.new(base) { include ViewContext } }
it "saves the superclass's view context" do
controller.new.view_context
expect(ViewContext.current).to be :controller_view_context
end
it "returns the superclass's view context" do
expect(controller.new.view_context).to be :controller_view_context
end
end
describe ".controller" do
it "returns the stored controller from RequestStore" do
RequestStore.stub store: {current_controller: :stored_controller}
expect(ViewContext.controller).to be :stored_controller
end
end
describe ".controller=" do
it "stores a controller in RequestStore" do
store = {}
RequestStore.stub store: store
ViewContext.controller = :stored_controller
expect(store[:current_controller]).to be :stored_controller
end
end
describe ".current" do
it "returns the stored view context from RequestStore" do
RequestStore.stub store: {current_view_context: :stored_view_context}
expect(ViewContext.current).to be :stored_view_context
end
it "falls back to building a view context" do
RequestStore.stub store: {}
ViewContext.should_receive(:build).and_return(:new_view_context)
expect(ViewContext.current).to be :new_view_context
end
end
describe ".current=" do
it "stores a view context in RequestStore" do
store = {}
RequestStore.stub store: store
ViewContext.current = :stored_view_context
expect(store[:current_view_context]).to be :stored_view_context
end
end
describe ".clear!" do
it "clears the stored controller and view controller" do
store = {current_controller: :stored_controller, current_view_context: :stored_view_context}
RequestStore.stub store: store
ViewContext.clear!
expect(store[:current_controller]).to be_nil
expect(store[:current_view_context]).to be_nil
end
end
describe ".build" do
it "calls the build strategy" do
ViewContext.stub build_strategy: ->{ :new_view_context }
expect(ViewContext.build).to be :new_view_context
end
end
describe ".build_strategy" do
it "defaults to full" do
expect(ViewContext.build_strategy).to be_a ViewContext::BuildStrategy::Full
end
it "memoizes" do
expect(ViewContext.build_strategy).to be ViewContext.build_strategy
end
end
describe ".test_strategy" do
protect_module ViewContext
context "with :fast" do
it "creates a fast strategy" do
ViewContext.test_strategy :fast
expect(ViewContext.build_strategy).to be_a ViewContext::BuildStrategy::Fast
end
it "passes a block to the strategy" do
ViewContext::BuildStrategy::Fast.stub(:new).and_return{|&block| block.call}
expect(ViewContext.test_strategy(:fast){:passed}).to be :passed
end
end
context "with :full" do
it "creates a full strategy" do
ViewContext.test_strategy :full
expect(ViewContext.build_strategy).to be_a ViewContext::BuildStrategy::Full
end
it "passes a block to the strategy" do
ViewContext::BuildStrategy::Full.stub(:new).and_return{|&block| block.call}
expect(ViewContext.test_strategy(:full){:passed}).to be :passed
end
end
end
end
end

View File

@ -0,0 +1,38 @@
require 'draper'
require 'rspec'
require 'active_model/naming'
require_relative '../app/decorators/post_decorator'
Draper::ViewContext.test_strategy :fast
Post = Struct.new(:id) { extend ActiveModel::Naming }
describe PostDecorator do
let(:decorator) { PostDecorator.new(source) }
let(:source) { Post.new(42) }
it "can use built-in helpers" do
expect(decorator.truncated).to eq "Once upon a..."
end
it "can use built-in private helpers" do
expect(decorator.html_escaped).to eq "<script>danger</script>"
end
it "can't use user-defined helpers from app/helpers" do
expect{decorator.hello_world}.to raise_error NoMethodError, /hello_world/
end
it "can't use path helpers" do
expect{decorator.path_with_model}.to raise_error NoMethodError, /post_path/
end
it "can't use url helpers" do
expect{decorator.url_with_model}.to raise_error NoMethodError, /post_url/
end
it "can't be passed implicitly to url_for" do
expect{decorator.link}.to raise_error
end
end

View File

@ -3,8 +3,12 @@ require 'rake/testtask'
RSpec::Core::RakeTask.new :rspec
RSpec::Core::RakeTask.new :fast_spec do |t|
t.pattern = "fast_spec/**/*_spec.rb"
end
Rake::TestTask.new :mini_test do |t|
t.test_files = ["mini_test/mini_test_integration_test.rb"]
end
task :default => [:rspec, :mini_test]
task :default => [:rspec, :mini_test, :fast_spec]

View File

@ -1,5 +1,7 @@
require 'bundler/setup'
require 'draper'
require 'action_controller'
require 'action_controller/test_case'
RSpec.configure do |config|
config.treat_symbols_as_metadata_keys_with_true_values = true
@ -28,3 +30,7 @@ end
def protect_class(klass)
before { stub_const klass.name, Class.new(klass) }
end
def protect_module(mod)
before { stub_const mod.name, mod.dup }
end