Added Rspec and Webrat plugins and started porting Selenium on Rails tests to Rspec Plain Text Stories driving Webrat driving Selenium.

This commit is contained in:
Luke Melia 2008-06-18 02:57:57 -04:00
parent 0600756bbf
commit 0f7d6f7a1d
602 changed files with 47788 additions and 29 deletions

View file

@ -0,0 +1,31 @@
module Spec
module Mocks
module ArgumentConstraintMatchers
# Shortcut for creating an instance of Spec::Mocks::DuckTypeArgConstraint
def duck_type(*args)
DuckTypeArgConstraint.new(*args)
end
def any_args
AnyArgsConstraint.new
end
def anything
AnyArgConstraint.new(nil)
end
def boolean
BooleanArgConstraint.new(nil)
end
def hash_including(expected={})
HashIncludingConstraint.new(expected)
end
def no_args
NoArgsConstraint.new
end
end
end
end

View file

@ -0,0 +1,216 @@
module Spec
module Mocks
class MatcherConstraint
def initialize(matcher)
@matcher = matcher
end
def matches?(value)
@matcher.matches?(value)
end
end
class LiteralArgConstraint
def initialize(literal)
@literal_value = literal
end
def matches?(value)
@literal_value == value
end
end
class RegexpArgConstraint
def initialize(regexp)
@regexp = regexp
end
def matches?(value)
return value =~ @regexp unless value.is_a?(Regexp)
value == @regexp
end
end
class AnyArgConstraint
def initialize(ignore)
end
def ==(other)
true
end
# TODO - need this?
def matches?(value)
true
end
end
class AnyArgsConstraint
def description
"any args"
end
end
class NoArgsConstraint
def description
"no args"
end
def ==(args)
args == []
end
end
class NumericArgConstraint
def initialize(ignore)
end
def matches?(value)
value.is_a?(Numeric)
end
end
class BooleanArgConstraint
def initialize(ignore)
end
def ==(value)
matches?(value)
end
def matches?(value)
return true if value.is_a?(TrueClass)
return true if value.is_a?(FalseClass)
false
end
end
class StringArgConstraint
def initialize(ignore)
end
def matches?(value)
value.is_a?(String)
end
end
class DuckTypeArgConstraint
def initialize(*methods_to_respond_to)
@methods_to_respond_to = methods_to_respond_to
end
def matches?(value)
@methods_to_respond_to.all? { |sym| value.respond_to?(sym) }
end
def description
"duck_type"
end
end
class HashIncludingConstraint
def initialize(expected)
@expected = expected
end
def ==(actual)
@expected.each do | key, value |
# check key for case that value evaluates to nil
return false unless actual.has_key?(key) && actual[key] == value
end
true
rescue NoMethodError => ex
return false
end
def matches?(value)
self == value
end
def description
"hash_including(#{@expected.inspect.sub(/^\{/,"").sub(/\}$/,"")})"
end
end
class ArgumentExpectation
attr_reader :args
@@constraint_classes = Hash.new { |hash, key| LiteralArgConstraint}
@@constraint_classes[:anything] = AnyArgConstraint
@@constraint_classes[:numeric] = NumericArgConstraint
@@constraint_classes[:boolean] = BooleanArgConstraint
@@constraint_classes[:string] = StringArgConstraint
def initialize(args, &block)
@args = args
@constraints_block = block
if [:any_args] == args
@expected_params = nil
warn_deprecated(:any_args.inspect, "any_args()")
elsif args.length == 1 && args[0].is_a?(AnyArgsConstraint) then @expected_params = nil
elsif [:no_args] == args
@expected_params = []
warn_deprecated(:no_args.inspect, "no_args()")
elsif args.length == 1 && args[0].is_a?(NoArgsConstraint) then @expected_params = []
else @expected_params = process_arg_constraints(args)
end
end
def process_arg_constraints(constraints)
constraints.collect do |constraint|
convert_constraint(constraint)
end
end
def warn_deprecated(deprecated_method, instead)
Kernel.warn "The #{deprecated_method} constraint is deprecated. Use #{instead} instead."
end
def convert_constraint(constraint)
if [:anything, :numeric, :boolean, :string].include?(constraint)
case constraint
when :anything
instead = "anything()"
when :boolean
instead = "boolean()"
when :numeric
instead = "an_instance_of(Numeric)"
when :string
instead = "an_instance_of(String)"
end
warn_deprecated(constraint.inspect, instead)
return @@constraint_classes[constraint].new(constraint)
end
return MatcherConstraint.new(constraint) if is_matcher?(constraint)
return RegexpArgConstraint.new(constraint) if constraint.is_a?(Regexp)
return LiteralArgConstraint.new(constraint)
end
def is_matcher?(obj)
return obj.respond_to?(:matches?) && obj.respond_to?(:description)
end
def check_args(args)
if @constraints_block
@constraints_block.call(*args)
return true
end
return true if @expected_params.nil?
return true if @expected_params == args
return constraints_match?(args)
end
def constraints_match?(args)
return false if args.length != @expected_params.length
@expected_params.each_index { |i| return false unless @expected_params[i].matches?(args[i]) }
return true
end
end
end
end

View file

@ -0,0 +1,84 @@
module Spec
module Mocks
class ErrorGenerator
attr_writer :opts
def initialize(target, name)
@target = target
@name = name
end
def opts
@opts ||= {}
end
def raise_unexpected_message_error(sym, *args)
__raise "#{intro} received unexpected message :#{sym}#{arg_message(*args)}"
end
def raise_unexpected_message_args_error(expectation, *args)
expected_args = format_args(*expectation.expected_args)
actual_args = args.empty? ? "(no args)" : format_args(*args)
__raise "#{intro} expected #{expectation.sym.inspect} with #{expected_args} but received it with #{actual_args}"
end
def raise_expectation_error(sym, expected_received_count, actual_received_count, *args)
__raise "#{intro} expected :#{sym}#{arg_message(*args)} #{count_message(expected_received_count)}, but received it #{count_message(actual_received_count)}"
end
def raise_out_of_order_error(sym)
__raise "#{intro} received :#{sym} out of order"
end
def raise_block_failed_error(sym, detail)
__raise "#{intro} received :#{sym} but passed block failed with: #{detail}"
end
def raise_missing_block_error(args_to_yield)
__raise "#{intro} asked to yield |#{arg_list(*args_to_yield)}| but no block was passed"
end
def raise_wrong_arity_error(args_to_yield, arity)
__raise "#{intro} yielded |#{arg_list(*args_to_yield)}| to block with arity of #{arity}"
end
private
def intro
@name ? "Mock '#{@name}'" : @target.inspect
end
def __raise(message)
message = opts[:message] unless opts[:message].nil?
Kernel::raise(Spec::Mocks::MockExpectationError, message)
end
def arg_message(*args)
" with " + format_args(*args)
end
def format_args(*args)
return "(no args)" if args.empty? || args == [:no_args]
return "(any args)" if args == [:any_args]
"(" + arg_list(*args) + ")"
end
def arg_list(*args)
args.collect do |arg|
arg.respond_to?(:description) ? arg.description : arg.inspect
end.join(", ")
end
def count_message(count)
return "at least #{pretty_print(count.abs)}" if count < 0
return pretty_print(count)
end
def pretty_print(count)
return "once" if count == 1
return "twice" if count == 2
return "#{count} times"
end
end
end
end

View file

@ -0,0 +1,10 @@
module Spec
module Mocks
class MockExpectationError < StandardError
end
class AmbiguousReturnError < StandardError
end
end
end

View file

@ -0,0 +1 @@
require 'spec/mocks/extensions/object'

View file

@ -0,0 +1,3 @@
class Object
include Spec::Mocks::Methods
end

View file

@ -0,0 +1,15 @@
# Require everything except the global extensions of class and object. This
# supports wrapping rspec's mocking functionality without invading every
# object in the system.
require 'spec/mocks/methods'
require 'spec/mocks/argument_constraint_matchers'
require 'spec/mocks/spec_methods'
require 'spec/mocks/proxy'
require 'spec/mocks/mock'
require 'spec/mocks/argument_expectation'
require 'spec/mocks/message_expectation'
require 'spec/mocks/order_group'
require 'spec/mocks/errors'
require 'spec/mocks/error_generator'
require 'spec/mocks/space'

View file

@ -0,0 +1,290 @@
module Spec
module Mocks
class BaseExpectation
attr_reader :sym
def initialize(error_generator, expectation_ordering, expected_from, sym, method_block, expected_received_count=1, opts={})
@error_generator = error_generator
@error_generator.opts = opts
@expected_from = expected_from
@sym = sym
@method_block = method_block
@return_block = nil
@actual_received_count = 0
@expected_received_count = expected_received_count
@args_expectation = ArgumentExpectation.new([AnyArgsConstraint.new])
@consecutive = false
@exception_to_raise = nil
@symbol_to_throw = nil
@order_group = expectation_ordering
@at_least = nil
@at_most = nil
@args_to_yield = []
end
def expected_args
@args_expectation.args
end
def and_return(*values, &return_block)
Kernel::raise AmbiguousReturnError unless @method_block.nil?
case values.size
when 0 then value = nil
when 1 then value = values[0]
else
value = values
@consecutive = true
@expected_received_count = values.size if !ignoring_args? &&
@expected_received_count < values.size
end
@return_block = block_given? ? return_block : lambda { value }
# Ruby 1.9 - see where this is used below
@ignore_args = !block_given?
end
# :call-seq:
# and_raise()
# and_raise(Exception) #any exception class
# and_raise(exception) #any exception object
#
# == Warning
#
# When you pass an exception class, the MessageExpectation will
# raise an instance of it, creating it with +new+. If the exception
# class initializer requires any parameters, you must pass in an
# instance and not the class.
def and_raise(exception=Exception)
@exception_to_raise = exception
end
def and_throw(symbol)
@symbol_to_throw = symbol
end
def and_yield(*args)
@args_to_yield << args
self
end
def matches(sym, args)
@sym == sym and @args_expectation.check_args(args)
end
def invoke(args, block)
if @expected_received_count == 0
@actual_received_count += 1
@error_generator.raise_expectation_error @sym, @expected_received_count, @actual_received_count, *args
end
@order_group.handle_order_constraint self
begin
Kernel::raise @exception_to_raise unless @exception_to_raise.nil?
Kernel::throw @symbol_to_throw unless @symbol_to_throw.nil?
if !@method_block.nil?
default_return_val = invoke_method_block(args)
elsif @args_to_yield.size > 0
default_return_val = invoke_with_yield(block)
else
default_return_val = nil
end
if @consecutive
return invoke_consecutive_return_block(args, block)
elsif @return_block
return invoke_return_block(args, block)
else
return default_return_val
end
ensure
@actual_received_count += 1
end
end
protected
def invoke_method_block(args)
begin
@method_block.call(*args)
rescue => detail
@error_generator.raise_block_failed_error @sym, detail.message
end
end
def invoke_with_yield(block)
if block.nil?
@error_generator.raise_missing_block_error @args_to_yield
end
value = nil
@args_to_yield.each do |args_to_yield_this_time|
if block.arity > -1 && args_to_yield_this_time.length != block.arity
@error_generator.raise_wrong_arity_error args_to_yield_this_time, block.arity
end
value = block.call(*args_to_yield_this_time)
end
value
end
def invoke_consecutive_return_block(args, block)
args << block unless block.nil?
value = @return_block.call(*args)
index = [@actual_received_count, value.size-1].min
value[index]
end
def invoke_return_block(args, block)
args << block unless block.nil?
# Ruby 1.9 - when we set @return_block to return values
# regardless of arguments, any arguments will result in
# a "wrong number of arguments" error
if @ignore_args
@return_block.call()
else
@return_block.call(*args)
end
end
end
class MessageExpectation < BaseExpectation
def matches_name_but_not_args(sym, args)
@sym == sym and not @args_expectation.check_args(args)
end
def verify_messages_received
return if expected_messages_received?
generate_error
rescue Spec::Mocks::MockExpectationError => error
error.backtrace.insert(0, @expected_from)
Kernel::raise error
end
def expected_messages_received?
ignoring_args? || matches_exact_count? ||
matches_at_least_count? || matches_at_most_count?
end
def ignoring_args?
@expected_received_count == :any
end
def matches_at_least_count?
@at_least && @actual_received_count >= @expected_received_count
end
def matches_at_most_count?
@at_most && @actual_received_count <= @expected_received_count
end
def matches_exact_count?
@expected_received_count == @actual_received_count
end
def similar_messages
@similar_messages ||= []
end
def advise(args, block)
similar_messages << args
end
def generate_error
if similar_messages.empty?
@error_generator.raise_expectation_error(@sym, @expected_received_count, @actual_received_count, *@args_expectation.args)
else
@error_generator.raise_unexpected_message_args_error(self, *@similar_messages.first)
end
end
def with(*args, &block)
@args_expectation = ArgumentExpectation.new(args, &block)
self
end
def exactly(n)
set_expected_received_count :exactly, n
self
end
def at_least(n)
set_expected_received_count :at_least, n
self
end
def at_most(n)
set_expected_received_count :at_most, n
self
end
def times(&block)
@method_block = block if block
self
end
def any_number_of_times(&block)
@method_block = block if block
@expected_received_count = :any
self
end
def never
@expected_received_count = 0
self
end
def once(&block)
@method_block = block if block
@expected_received_count = 1
self
end
def twice(&block)
@method_block = block if block
@expected_received_count = 2
self
end
def ordered(&block)
@method_block = block if block
@order_group.register(self)
@ordered = true
self
end
def negative_expectation_for?(sym)
return false
end
protected
def set_expected_received_count(relativity, n)
@at_least = (relativity == :at_least)
@at_most = (relativity == :at_most)
@expected_received_count = case n
when Numeric
n
when :once
1
when :twice
2
end
end
end
class NegativeMessageExpectation < MessageExpectation
def initialize(message, expectation_ordering, expected_from, sym, method_block)
super(message, expectation_ordering, expected_from, sym, method_block, 0)
end
def negative_expectation_for?(sym)
return @sym == sym
end
end
end
end

View file

@ -0,0 +1,39 @@
module Spec
module Mocks
module Methods
def should_receive(sym, opts={}, &block)
__mock_proxy.add_message_expectation(opts[:expected_from] || caller(1)[0], sym.to_sym, opts, &block)
end
def should_not_receive(sym, &block)
__mock_proxy.add_negative_message_expectation(caller(1)[0], sym.to_sym, &block)
end
def stub!(sym, opts={})
__mock_proxy.add_stub(caller(1)[0], sym.to_sym, opts)
end
def received_message?(sym, *args, &block) #:nodoc:
__mock_proxy.received_message?(sym.to_sym, *args, &block)
end
def rspec_verify #:nodoc:
__mock_proxy.verify
end
def rspec_reset #:nodoc:
__mock_proxy.reset
end
private
def __mock_proxy
if Mock === self
@mock_proxy ||= Proxy.new(self, @name, @options)
else
@mock_proxy ||= Proxy.new(self, self.class.name)
end
end
end
end
end

View file

@ -0,0 +1,56 @@
module Spec
module Mocks
class Mock
include Methods
# Creates a new mock with a +name+ (that will be used in error messages
# only) == Options:
# * <tt>:null_object</tt> - if true, the mock object acts as a forgiving
# null object allowing any message to be sent to it.
def initialize(name, stubs_and_options={})
@name = name
@options = parse_options(stubs_and_options)
assign_stubs(stubs_and_options)
end
# This allows for comparing the mock to other objects that proxy such as
# ActiveRecords belongs_to proxy objects By making the other object run
# the comparison, we're sure the call gets delegated to the proxy target
# This is an unfortunate side effect from ActiveRecord, but this should
# be safe unless the RHS redefines == in a nonsensical manner
def ==(other)
other == __mock_proxy
end
def method_missing(sym, *args, &block)
__mock_proxy.instance_eval {@messages_received << [sym, args, block]}
begin
return self if __mock_proxy.null_object?
super(sym, *args, &block)
rescue NameError
__mock_proxy.raise_unexpected_message_error sym, *args
end
end
def inspect
"#<#{self.class}:#{sprintf '0x%x', self.object_id} @name=#{@name.inspect}>"
end
def to_s
inspect.gsub('<','[').gsub('>',']')
end
private
def parse_options(options)
options.has_key?(:null_object) ? {:null_object => options.delete(:null_object)} : {}
end
def assign_stubs(stubs)
stubs.each_pair do |message, response|
stub!(message).and_return(response)
end
end
end
end
end

View file

@ -0,0 +1,29 @@
module Spec
module Mocks
class OrderGroup
def initialize error_generator
@error_generator = error_generator
@ordering = Array.new
end
def register(expectation)
@ordering << expectation
end
def ready_for?(expectation)
return @ordering.first == expectation
end
def consume
@ordering.shift
end
def handle_order_constraint expectation
return unless @ordering.include? expectation
return consume if ready_for?(expectation)
@error_generator.raise_out_of_order_error expectation.sym
end
end
end
end

View file

@ -0,0 +1,184 @@
module Spec
module Mocks
class Proxy
DEFAULT_OPTIONS = {
:null_object => false,
}
def initialize(target, name, options={})
@target = target
@name = name
@error_generator = ErrorGenerator.new target, name
@expectation_ordering = OrderGroup.new @error_generator
@expectations = []
@messages_received = []
@stubs = []
@proxied_methods = []
@options = options ? DEFAULT_OPTIONS.dup.merge(options) : DEFAULT_OPTIONS
end
def null_object?
@options[:null_object]
end
def add_message_expectation(expected_from, sym, opts={}, &block)
__add sym
@expectations << MessageExpectation.new(@error_generator, @expectation_ordering, expected_from, sym, block_given? ? block : nil, 1, opts)
@expectations.last
end
def add_negative_message_expectation(expected_from, sym, &block)
__add sym
@expectations << NegativeMessageExpectation.new(@error_generator, @expectation_ordering, expected_from, sym, block_given? ? block : nil)
@expectations.last
end
def add_stub(expected_from, sym, opts={})
__add sym
@stubs.unshift MessageExpectation.new(@error_generator, @expectation_ordering, expected_from, sym, nil, :any, opts)
@stubs.first
end
def verify #:nodoc:
verify_expectations
ensure
reset
end
def reset
clear_expectations
clear_stubs
reset_proxied_methods
clear_proxied_methods
end
def received_message?(sym, *args, &block)
@messages_received.any? {|array| array == [sym, args, block]}
end
def has_negative_expectation?(sym)
@expectations.detect {|expectation| expectation.negative_expectation_for?(sym)}
end
def message_received(sym, *args, &block)
if expectation = find_matching_expectation(sym, *args)
expectation.invoke(args, block)
elsif (stub = find_matching_method_stub(sym, *args))
if expectation = find_almost_matching_expectation(sym, *args)
expectation.advise(args, block) unless expectation.expected_messages_received?
end
stub.invoke([], block)
elsif expectation = find_almost_matching_expectation(sym, *args)
expectation.advise(args, block) if null_object? unless expectation.expected_messages_received?
raise_unexpected_message_args_error(expectation, *args) unless (has_negative_expectation?(sym) or null_object?)
else
@target.send :method_missing, sym, *args, &block
end
end
def raise_unexpected_message_args_error(expectation, *args)
@error_generator.raise_unexpected_message_args_error expectation, *args
end
def raise_unexpected_message_error(sym, *args)
@error_generator.raise_unexpected_message_error sym, *args
end
private
def __add(sym)
$rspec_mocks.add(@target) unless $rspec_mocks.nil?
define_expected_method(sym)
end
def define_expected_method(sym)
visibility_string = "#{visibility(sym)} :#{sym}"
if target_responds_to?(sym) && !target_metaclass.method_defined?(munge(sym))
munged_sym = munge(sym)
target_metaclass.instance_eval do
alias_method munged_sym, sym if method_defined?(sym.to_s)
end
@proxied_methods << sym
end
target_metaclass.class_eval(<<-EOF, __FILE__, __LINE__)
def #{sym}(*args, &block)
__mock_proxy.message_received :#{sym}, *args, &block
end
#{visibility_string}
EOF
end
def target_responds_to?(sym)
return @target.send(munge(:respond_to?),sym) if @already_proxied_respond_to
return @already_proxied_respond_to = true if sym == :respond_to?
return @target.respond_to?(sym)
end
def visibility(sym)
if Mock === @target
'public'
elsif target_metaclass.private_method_defined?(sym)
'private'
elsif target_metaclass.protected_method_defined?(sym)
'protected'
else
'public'
end
end
def munge(sym)
"proxied_by_rspec__#{sym.to_s}".to_sym
end
def clear_expectations
@expectations.clear
end
def clear_stubs
@stubs.clear
end
def clear_proxied_methods
@proxied_methods.clear
end
def target_metaclass
class << @target; self; end
end
def verify_expectations
@expectations.each do |expectation|
expectation.verify_messages_received
end
end
def reset_proxied_methods
@proxied_methods.each do |sym|
munged_sym = munge(sym)
target_metaclass.instance_eval do
if method_defined?(munged_sym.to_s)
alias_method sym, munged_sym
undef_method munged_sym
else
undef_method sym
end
end
end
end
def find_matching_expectation(sym, *args)
@expectations.find {|expectation| expectation.matches(sym, args)}
end
def find_almost_matching_expectation(sym, *args)
@expectations.find {|expectation| expectation.matches_name_but_not_args(sym, args)}
end
def find_matching_method_stub(sym, *args)
@stubs.find {|stub| stub.matches(sym, args)}
end
end
end
end

View file

@ -0,0 +1,28 @@
module Spec
module Mocks
class Space
def add(obj)
mocks << obj unless mocks.detect {|m| m.equal? obj}
end
def verify_all
mocks.each do |mock|
mock.rspec_verify
end
end
def reset_all
mocks.each do |mock|
mock.rspec_reset
end
mocks.clear
end
private
def mocks
@mocks ||= []
end
end
end
end

View file

@ -0,0 +1,38 @@
module Spec
module Mocks
module ExampleMethods
include Spec::Mocks::ArgumentConstraintMatchers
# Shortcut for creating an instance of Spec::Mocks::Mock.
#
# +name+ is used for failure reporting, so you should use the
# role that the mock is playing in the example.
#
# +stubs_and_options+ lets you assign options and stub values
# at the same time. The only option available is :null_object.
# Anything else is treated as a stub value.
#
# == Examples
#
# stub_thing = mock("thing", :a => "A")
# stub_thing.a == "A" => true
#
# stub_person = stub("thing", :name => "Joe", :email => "joe@domain.com")
# stub_person.name => "Joe"
# stub_person.email => "joe@domain.com"
def mock(name, stubs_and_options={})
Spec::Mocks::Mock.new(name, stubs_and_options)
end
alias :stub :mock
# Shortcut for creating a mock object that will return itself in response
# to any message it receives that it hasn't been explicitly instructed
# to respond to.
def stub_everything(name = 'stub')
mock(name, :null_object => true)
end
end
end
end