From 1bf592a143223c1bcac795e7a1be26fac1104c82 Mon Sep 17 00:00:00 2001 From: Erik Michaels-Ober Date: Fri, 3 Jan 2014 22:02:39 +0100 Subject: [PATCH 1/6] Add RSpec::Matchers::BuiltIn::BeBetween Ruby's #between? instance method is included in the Comparable module. Any object that mixes in Comparable and responds to the <=> operator will also respond to #between?, including the following core classes: * Numeric (Fixnum, Bignum, Integer, Float, Complex, and Rational) * String * Symbol * Time ...as well as a few lesser-used ones (e.g. File::Stat, Gem::Version). BeBetween defines a matcher that takes exactly two arguments--a minimum and a maximum value--mimicking the interface of Comparable.html#between? and ensuring backward compatibility. Before this patch, using be_between on Comparable objects worked but produced the following failure message: expected between?(1, 10) to return true, got false After this patch, it will produce this message, instead: expected 5 to be between 1 and 10 I believe this is a much more informative failure message, as it reveals the actual value, which is often a variable, in addition to displaying the expectation in plainer English. --- lib/rspec/matchers.rb | 12 ++++ lib/rspec/matchers/built_in.rb | 1 + lib/rspec/matchers/built_in/be_between.rb | 20 +++++++ .../matchers/built_in/be_between_spec.rb | 57 +++++++++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 lib/rspec/matchers/built_in/be_between.rb create mode 100644 spec/rspec/matchers/built_in/be_between_spec.rb diff --git a/lib/rspec/matchers.rb b/lib/rspec/matchers.rb index 858df1306..ca8b71eec 100644 --- a/lib/rspec/matchers.rb +++ b/lib/rspec/matchers.rb @@ -317,6 +317,18 @@ def be_a_kind_of(expected) alias_method :be_kind_of, :be_a_kind_of alias_matcher :a_kind_of, :be_a_kind_of + # Passes if actual.between?(min, max). Works with any Comparable object, + # including String, Symbol, Time, or Numeric (Fixnum, Bignum, Integer, + # Float, Complex, and Rational). + # + # @example + # + # expect(5).to be_between(1, 10) + # expect(11).not_to be_between(1, 10) + def be_between(min, max) + BuiltIn::BeBetween.new(min, max) + end + # Passes if actual == expected +/- delta # # @example diff --git a/lib/rspec/matchers/built_in.rb b/lib/rspec/matchers/built_in.rb index 4e67fada3..2845c2be1 100644 --- a/lib/rspec/matchers/built_in.rb +++ b/lib/rspec/matchers/built_in.rb @@ -4,6 +4,7 @@ module RSpec module Matchers module BuiltIn autoload :BeAnInstanceOf, 'rspec/matchers/built_in/be_instance_of' + autoload :BeBetween, 'rspec/matchers/built_in/be_between' autoload :Be, 'rspec/matchers/built_in/be' autoload :BeTruthy, 'rspec/matchers/built_in/be' autoload :BeFalsey, 'rspec/matchers/built_in/be' diff --git a/lib/rspec/matchers/built_in/be_between.rb b/lib/rspec/matchers/built_in/be_between.rb new file mode 100644 index 000000000..025c3377b --- /dev/null +++ b/lib/rspec/matchers/built_in/be_between.rb @@ -0,0 +1,20 @@ +module RSpec + module Matchers + module BuiltIn + class BeBetween < BaseMatcher + def initialize(min, max) + @min, @max = min, max + end + + def matches?(actual) + @actual = actual + @actual.between?(@min, @max) + end + + def description + "be between #{@min.inspect} and #{@max.inspect}" + end + end + end + end +end diff --git a/spec/rspec/matchers/built_in/be_between_spec.rb b/spec/rspec/matchers/built_in/be_between_spec.rb new file mode 100644 index 000000000..c211e080e --- /dev/null +++ b/spec/rspec/matchers/built_in/be_between_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe "expect(...).to be_between(min, max)" do + it_behaves_like "an RSpec matcher", :valid_value => (5), :invalid_value => (11) do + let(:matcher) { be_between(1, 10) } + end + + it "passes if target is between min and max" do + expect(5).to be_between(1, 10) + end + + it "fails if target is not between min and max" do + expect { + # It does not go to 11 + expect(11).to be_between(1, 10) + }.to fail_with("expected 11 to be between 1 and 10") + end + + it 'works with strings' do + expect("baz").to be_between("bar", "foo") + expect { + expect("foo").to be_between("bar", "baz") + }.to fail_with("expected \"foo\" to be between \"bar\" and \"baz\"") + end + + it 'works with other Comparable objects' do + class SizeMatters + include Comparable + attr :str + def <=>(other) + str.size <=> other.str.size + end + def initialize(str) + @str = str + end + def inspect + @str + end + end + expect(SizeMatters.new("--")).to be_between(SizeMatters.new("-"), SizeMatters.new("---")) + expect { + expect(SizeMatters.new("---")).to be_between(SizeMatters.new("-"), SizeMatters.new("--")) + }.to fail_with("expected --- to be between - and --") + end +end + +describe "expect(...).not_to be_between(min, max)" do + it "passes if target is not between min and max" do + expect(11).not_to be_between(1, 10) + end + + it "fails if target is between min and max" do + expect { + expect(5).not_to be_between(1, 10) + }.to fail_with("expected 5 not to be between 1 and 10") + end +end From 596dc0adefcddd2d2b7cb03265963c225dd41376 Mon Sep 17 00:00:00 2001 From: Erik Michaels-Ober Date: Fri, 3 Jan 2014 22:36:07 +0100 Subject: [PATCH 2/6] Alphabetize/organize built-in matchers --- lib/rspec/matchers/built_in.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/rspec/matchers/built_in.rb b/lib/rspec/matchers/built_in.rb index 2845c2be1..81af6a685 100644 --- a/lib/rspec/matchers/built_in.rb +++ b/lib/rspec/matchers/built_in.rb @@ -3,15 +3,15 @@ module RSpec module Matchers module BuiltIn + autoload :BeAKindOf, 'rspec/matchers/built_in/be_kind_of' autoload :BeAnInstanceOf, 'rspec/matchers/built_in/be_instance_of' autoload :BeBetween, 'rspec/matchers/built_in/be_between' autoload :Be, 'rspec/matchers/built_in/be' - autoload :BeTruthy, 'rspec/matchers/built_in/be' + autoload :BeComparedTo, 'rspec/matchers/built_in/be' autoload :BeFalsey, 'rspec/matchers/built_in/be' autoload :BeNil, 'rspec/matchers/built_in/be' - autoload :BeComparedTo, 'rspec/matchers/built_in/be' autoload :BePredicate, 'rspec/matchers/built_in/be' - autoload :BeAKindOf, 'rspec/matchers/built_in/be_kind_of' + autoload :BeTruthy, 'rspec/matchers/built_in/be' autoload :BeWithin, 'rspec/matchers/built_in/be_within' autoload :Change, 'rspec/matchers/built_in/change' autoload :Compound, 'rspec/matchers/built_in/compound' @@ -34,11 +34,9 @@ module BuiltIn autoload :StartWith, 'rspec/matchers/built_in/start_and_end_with' autoload :ThrowSymbol, 'rspec/matchers/built_in/throw_symbol' autoload :YieldControl, 'rspec/matchers/built_in/yield' + autoload :YieldSuccessiveArgs, 'rspec/matchers/built_in/yield' autoload :YieldWithArgs, 'rspec/matchers/built_in/yield' autoload :YieldWithNoArgs, 'rspec/matchers/built_in/yield' - autoload :YieldSuccessiveArgs, 'rspec/matchers/built_in/yield' end end end - - From 0ef2bd57c30f1ffc282e4878ac8957c0903bb43d Mon Sep 17 00:00:00 2001 From: Erik Michaels-Ober Date: Fri, 3 Jan 2014 23:01:18 +0100 Subject: [PATCH 3/6] Define custom predicate method to test description generation --- .../rspec/matchers/description_generation_spec.rb | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/spec/rspec/matchers/description_generation_spec.rb b/spec/rspec/matchers/description_generation_spec.rb index 928ad0921..e1ed9a823 100644 --- a/spec/rspec/matchers/description_generation_spec.rb +++ b/spec/rspec/matchers/description_generation_spec.rb @@ -45,11 +45,22 @@ expect(RSpec::Matchers.generated_description).to eq "should be > 3" end - it "expect(...).to be predicate arg1, arg2 and arg3" do - expect(5.0).to be_between(0,10) + it "expect(...).to be between min and max" do + expect(10).to be_between(0, 10) expect(RSpec::Matchers.generated_description).to eq "should be between 0 and 10" end + it "expect(...).to be predicate arg1, arg2 and arg3" do + class Parent; end + class Child < Parent + def child_of?(*parents) + parents.all? { |parent| self.is_a?(parent) } + end + end + expect(Child.new).to be_a_child_of(Parent, Object) + expect(RSpec::Matchers.generated_description).to eq "should be a child of Parent and Object" + end + it "expect(...).to equal" do expected = "expected" expect(expected).to equal(expected) From cf2d0a5f4734cfb4ba13a716712cfc2e9ac4c427 Mon Sep 17 00:00:00 2001 From: Erik Michaels-Ober Date: Sat, 4 Jan 2014 01:48:03 +0100 Subject: [PATCH 4/6] Note that between is inclusive of min and max values and test edge cases --- lib/rspec/matchers.rb | 2 ++ lib/rspec/matchers/built_in/be_between.rb | 2 +- spec/rspec/matchers/built_in/be_between_spec.rb | 14 +++++++------- spec/rspec/matchers/description_generation_spec.rb | 2 +- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/rspec/matchers.rb b/lib/rspec/matchers.rb index ca8b71eec..24cc199f2 100644 --- a/lib/rspec/matchers.rb +++ b/lib/rspec/matchers.rb @@ -321,6 +321,8 @@ def be_a_kind_of(expected) # including String, Symbol, Time, or Numeric (Fixnum, Bignum, Integer, # Float, Complex, and Rational). # + # @note Inclusive of both min and max values. + # # @example # # expect(5).to be_between(1, 10) diff --git a/lib/rspec/matchers/built_in/be_between.rb b/lib/rspec/matchers/built_in/be_between.rb index 025c3377b..908eb0a65 100644 --- a/lib/rspec/matchers/built_in/be_between.rb +++ b/lib/rspec/matchers/built_in/be_between.rb @@ -12,7 +12,7 @@ def matches?(actual) end def description - "be between #{@min.inspect} and #{@max.inspect}" + "be between #{@min.inspect} and #{@max.inspect} (inclusive)" end end end diff --git a/spec/rspec/matchers/built_in/be_between_spec.rb b/spec/rspec/matchers/built_in/be_between_spec.rb index c211e080e..15eb954cd 100644 --- a/spec/rspec/matchers/built_in/be_between_spec.rb +++ b/spec/rspec/matchers/built_in/be_between_spec.rb @@ -1,26 +1,26 @@ require 'spec_helper' describe "expect(...).to be_between(min, max)" do - it_behaves_like "an RSpec matcher", :valid_value => (5), :invalid_value => (11) do + it_behaves_like "an RSpec matcher", :valid_value => (10), :invalid_value => (11) do let(:matcher) { be_between(1, 10) } end it "passes if target is between min and max" do - expect(5).to be_between(1, 10) + expect(10).to be_between(1, 10) end it "fails if target is not between min and max" do expect { # It does not go to 11 expect(11).to be_between(1, 10) - }.to fail_with("expected 11 to be between 1 and 10") + }.to fail_with("expected 11 to be between 1 and 10 (inclusive)") end it 'works with strings' do expect("baz").to be_between("bar", "foo") expect { expect("foo").to be_between("bar", "baz") - }.to fail_with("expected \"foo\" to be between \"bar\" and \"baz\"") + }.to fail_with("expected \"foo\" to be between \"bar\" and \"baz\" (inclusive)") end it 'works with other Comparable objects' do @@ -40,7 +40,7 @@ def inspect expect(SizeMatters.new("--")).to be_between(SizeMatters.new("-"), SizeMatters.new("---")) expect { expect(SizeMatters.new("---")).to be_between(SizeMatters.new("-"), SizeMatters.new("--")) - }.to fail_with("expected --- to be between - and --") + }.to fail_with("expected --- to be between - and -- (inclusive)") end end @@ -51,7 +51,7 @@ def inspect it "fails if target is between min and max" do expect { - expect(5).not_to be_between(1, 10) - }.to fail_with("expected 5 not to be between 1 and 10") + expect(10).not_to be_between(1, 10) + }.to fail_with("expected 10 not to be between 1 and 10 (inclusive)") end end diff --git a/spec/rspec/matchers/description_generation_spec.rb b/spec/rspec/matchers/description_generation_spec.rb index e1ed9a823..5a4f973af 100644 --- a/spec/rspec/matchers/description_generation_spec.rb +++ b/spec/rspec/matchers/description_generation_spec.rb @@ -47,7 +47,7 @@ it "expect(...).to be between min and max" do expect(10).to be_between(0, 10) - expect(RSpec::Matchers.generated_description).to eq "should be between 0 and 10" + expect(RSpec::Matchers.generated_description).to eq "should be between 0 and 10 (inclusive)" end it "expect(...).to be predicate arg1, arg2 and arg3" do From 308ccce5a00e910d03ad69c23d3a2acd59051eba Mon Sep 17 00:00:00 2001 From: Erik Michaels-Ober Date: Fri, 3 Jan 2014 22:48:46 +0100 Subject: [PATCH 5/6] Make BeBetween composable --- lib/rspec/matchers.rb | 1 + lib/rspec/matchers/built_in/be_between.rb | 22 ++++++++++++++++++- lib/rspec/matchers/built_in/be_within.rb | 5 ++--- spec/rspec/matchers/aliases_spec.rb | 8 +++++++ .../matchers/built_in/be_between_spec.rb | 17 ++++++++++++++ 5 files changed, 49 insertions(+), 4 deletions(-) diff --git a/lib/rspec/matchers.rb b/lib/rspec/matchers.rb index 24cc199f2..ddc19d751 100644 --- a/lib/rspec/matchers.rb +++ b/lib/rspec/matchers.rb @@ -330,6 +330,7 @@ def be_a_kind_of(expected) def be_between(min, max) BuiltIn::BeBetween.new(min, max) end + alias_matcher :a_value_between, :be_between # Passes if actual == expected +/- delta # diff --git a/lib/rspec/matchers/built_in/be_between.rb b/lib/rspec/matchers/built_in/be_between.rb index 908eb0a65..7a566317a 100644 --- a/lib/rspec/matchers/built_in/be_between.rb +++ b/lib/rspec/matchers/built_in/be_between.rb @@ -2,18 +2,38 @@ module RSpec module Matchers module BuiltIn class BeBetween < BaseMatcher + include Composable + def initialize(min, max) @min, @max = min, max end def matches?(actual) @actual = actual - @actual.between?(@min, @max) + comparable? and @actual.between?(@min, @max) + end + + def failure_message + "expected #{@actual.inspect} to #{description}#{not_comparable_clause}" + end + + def failure_message_when_negated + "expected #{@actual.inspect} not to #{description}" end def description "be between #{@min.inspect} and #{@max.inspect} (inclusive)" end + + private + + def comparable? + @actual.respond_to?(:between?) + end + + def not_comparable_clause + ", but #{@actual.inspect} does not respond to `between?`" unless comparable? + end end end end diff --git a/lib/rspec/matchers/built_in/be_within.rb b/lib/rspec/matchers/built_in/be_within.rb index 1bb6cbf0b..9e38da700 100644 --- a/lib/rspec/matchers/built_in/be_within.rb +++ b/lib/rspec/matchers/built_in/be_within.rb @@ -33,7 +33,7 @@ def failure_message end def failure_message_when_negated - "expected #{@actual} not to #{description}" + "expected #{@actual.inspect} not to #{description}" end def description @@ -51,8 +51,7 @@ def needs_expected end def not_numeric_clause - return "" if numeric? - ", but it could not be treated as a numeric value" + ", but it could not be treated as a numeric value" unless numeric? end end end diff --git a/spec/rspec/matchers/aliases_spec.rb b/spec/rspec/matchers/aliases_spec.rb index baee13cf4..133c4efd6 100644 --- a/spec/rspec/matchers/aliases_spec.rb +++ b/spec/rspec/matchers/aliases_spec.rb @@ -80,6 +80,14 @@ module RSpec ).with_description("a kind of Integer") end + specify do + expect( + a_value_between(1, 10) + ).to be_aliased_to( + be_between(1, 10) + ).with_description("a value between 1 and 10 (inclusive)") + end + specify do expect( a_value_within(0.1).of(3) diff --git a/spec/rspec/matchers/built_in/be_between_spec.rb b/spec/rspec/matchers/built_in/be_between_spec.rb index 15eb954cd..8c707d59a 100644 --- a/spec/rspec/matchers/built_in/be_between_spec.rb +++ b/spec/rspec/matchers/built_in/be_between_spec.rb @@ -55,3 +55,20 @@ def inspect }.to fail_with("expected 10 not to be between 1 and 10 (inclusive)") end end + +describe "composing with other matchers" do + it "passes when the matchers both match" do + expect([0.1, 2]).to include(a_value_between(2, 4), an_instance_of(Float)) + end + + it "provides a description" do + description = include(a_value_between(2, 4), an_instance_of(Float)).description + expect(description).to eq("include (a value between 2 and 4 (inclusive)) and (an instance of Float)") + end + + it "fails with a clear error message when the matchers do not match" do + expect { + expect([0.1, 1]).to include(a_value_between(2, 4), an_instance_of(Float)) + }.to fail_with("expected [0.1, 1] to include (a value between 2 and 4 (inclusive)) and (an instance of Float)") + end +end From 7bbe1517d875246a12c1b7a79a59c15ea592f336 Mon Sep 17 00:00:00 2001 From: Erik Michaels-Ober Date: Sat, 4 Jan 2014 04:40:42 +0100 Subject: [PATCH 6/6] Test against JRuby and MRI at head --- .travis.yml | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 737794604..a26555e3d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,10 @@ -script: "script/test_all" +before_install: + - gem update bundler + - bundle --version + - gem update --system 2.1.11 + - gem --version bundler_args: "--standalone --binstubs --without documentation" +script: "script/test_all" rvm: - 1.8.7 - 1.9.2 @@ -8,10 +13,13 @@ rvm: - 2.1.0 - jruby-18mode - jruby-19mode - - ree + - jruby-head - rbx -before_install: - - gem update bundler - - bundle --version - - gem update --system 2.1.11 - - gem --version + - ree + - ruby-head +matrix: + allow_failures: + - rvm: jruby-head + - rvm: ruby-head + fast_finish: true +