1
0
Fork 0
mirror of https://github.com/thoughtbot/shoulda-matchers.git synced 2022-11-09 12:01:38 -05:00

Fix delegate_method when used with shoulda-context

When specifying a custom subject, a straightforward usage of
`delegate_method`, such as the following

    class UserPresenter
      delegate :id, to: :user

      attr_reader :user

      def initialize(user)
        @user = user
      end
    end

    class UserPresenterTest < ActiveSupport::TestCase
      subject { UserPresenter.new(User.new) }
      should delegate_method(:id).to(:user)
    end

would error with something like:

    ArgumentError: wrong number of arguments (0 for 1)

This happens because the `should` method in shoulda-context asks the
DelegateMethodMatcher object for its description so that it can use it
to give the test a name and create a method that Test::Unit can run.
Now, the matcher's description needs a subject in order to determine
whether a class or an instance is being tested here -- if a class is
being tested the description will be "should delegate #id to \#user
object", if a class then "should delegate .id to .user object".
Unfortunately the matcher doesn't know what the subject is before
its #description method is called -- it only knows about this when it
gets evaluated.

Within the matcher we do have access to the current context class, so we
could read the subject block off of it and evaluate it. However, in
order to properly do this we also need access to the instance of the
test itself, which we do not have until the matcher is evaluated (by
which point it's too late).

Since there's really no way to solve this problem apart from rewriting a
lot of shoulda-context, and since often times your subject is an
instance and not a class, just assume it's an instance in this case.
This commit is contained in:
Elliot Winkler 2014-09-12 16:39:48 -06:00
parent db7723d925
commit c683aed2f0
6 changed files with 128 additions and 7 deletions

View file

@ -0,0 +1,47 @@
Feature: Independent matchers
Background:
When I generate a new Ruby application
Scenario: A Ruby application that uses Minitest and the delegate_method matcher
When I add Minitest to the project
And I write to "lib/post_office.rb" with:
"""
class PostOffice
end
"""
And I write to "lib/courier.rb" with:
"""
require "forwardable"
class Courier
extend Forwardable
def_delegators :post_office, :deliver
attr_reader :post_office
def initialize(post_office)
@post_office = post_office
end
end
"""
And I write a Minitest test to "test/courier_test.rb" with:
"""
require "test_helper"
require "courier"
require "post_office"
class CourierTest < {{MINITEST_TEST_CASE_CLASS}}
subject { Courier.new(post_office) }
should delegate_method(:deliver).to(:post_office)
def post_office
PostOffice.new
end
end
"""
And I set the "TESTOPTS" environment variable to "-v"
And I successfully run `bundle exec ruby -I lib -I test test/courier_test.rb`
Then the output should indicate that 1 test was run
And the output should contain "Courier should delegate #deliver to #post_office object"

View file

@ -24,6 +24,11 @@ Minitest::Reporters.use!(Minitest::Reporters::SpecReporter.new)
EOT
end
When /I write a Minitest test to "([^"]+)" with:/ do |path, contents|
contents.sub!('{{MINITEST_TEST_CASE_CLASS}}', minitest_test_case_class)
write_file(path, contents)
end
When 'I add Test::Unit to the project' do
steps %{
When I configure the application to use shoulda-context

View file

@ -0,0 +1,21 @@
module MinitestHelpers
def minitest_test_case_class
if minitest_gte_5?
'Minitest::Test'
else
'MiniTest::Unit::TestCase'
end
end
def minitest_gte_5?
if minitest_version
Gem::Requirement.new('>= 5').satisfied_by?(minitest_version)
end
end
def minitest_version
Bundler.definition.specs['minitest'][0].version
end
end
World(MinitestHelpers)

View file

@ -1,9 +1,10 @@
require 'shoulda/matchers/assertion_error'
require 'shoulda/matchers/doublespeak'
require 'shoulda/matchers/error'
require 'shoulda/matchers/matcher_context'
require 'shoulda/matchers/rails_shim'
require 'shoulda/matchers/warn'
require 'shoulda/matchers/version'
require 'shoulda/matchers/warn'
require 'shoulda/matchers/independent'

View file

@ -108,14 +108,13 @@ module Shoulda
# @return [DelegateMethodMatcher]
#
def delegate_method(delegating_method)
DelegateMethodMatcher.new(delegating_method, self)
DelegateMethodMatcher.new(delegating_method).in_context(self)
end
# @private
class DelegateMethodMatcher
def initialize(delegating_method, context)
def initialize(delegating_method)
@delegating_method = delegating_method
@context = context
@method_on_target = @delegating_method
@target_double = Doublespeak::ObjectDouble.new
@ -126,6 +125,11 @@ module Shoulda
@subject_double_collection = nil
end
def in_context(context)
@context = MatcherContext.new(context)
self
end
def matches?(subject)
@subject = subject
@ -192,11 +196,19 @@ module Shoulda
:target_method
def subject
@subject || context.subject
@subject
end
def subject_is_a_class?
if @subject
@subject.is_a?(Class)
else
context.subject_is_a_class?
end
end
def class_under_test
if subject.is_a?(Class)
if subject_is_a_class?
subject
else
subject.class
@ -230,7 +242,7 @@ module Shoulda
end
def class_or_instance_method_indicator
if subject.is_a?(Class)
if subject_is_a_class?
'.'
else
'#'

View file

@ -0,0 +1,35 @@
module Shoulda
module Matchers
class MatcherContext
def initialize(context)
@context = context
end
def subject_is_a_class?
if inside_a_shoulda_context_project? && outside_a_should_block?
assume_that_subject_is_not_a_class
else
context.subject.is_a?(Class)
end
end
protected
attr_reader :context
private
def inside_a_shoulda_context_project?
defined?(Shoulda::Context)
end
def outside_a_should_block?
context.is_a?(Class)
end
def assume_that_subject_is_not_a_class
false
end
end
end
end