Skip to content
This repository has been archived by the owner on Nov 30, 2024. It is now read-only.

Commit

Permalink
Merge pull request #393 from rspec/support-composable-matchers
Browse files Browse the repository at this point in the history
Support composable matchers
  • Loading branch information
myronmarston committed Jan 2, 2014
2 parents bc292d8 + b0fde19 commit 7736337
Show file tree
Hide file tree
Showing 59 changed files with 3,144 additions and 716 deletions.
1 change: 1 addition & 0 deletions .yardopts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
--load ./yard/alias_matcher.rb
--exclude features
--no-private
--markup markdown
Expand Down
18 changes: 18 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,24 @@ Enhancements:
Simply chain them off of any existing matcher to create an expression
like `expect(alphabet).to start_with("a").and end_with("z")`.
(Eloy Espinaco)
* Add `contain_exactly` as a less ambiguous version of `match_array`.
Note that it expects the expected array to be splatted as
individual args: `expect(array).to contain_exactly(1, 2)` is
the same as `expect(array).to match_array([1, 2])`. (Myron Marston)
* Update `contain_exactly`/`match_array` so that it can match against
other non-array collections (such as a `Set`). (Myron Marston)
* Update built-in matchers so that they can accept matchers as arguments
to allow you to compose matchers in arbitrary ways. (Myron Marston)
* Add `RSpec::Matchers::Composable` mixin that can be used to make
a custom matcher composable as well. Note that custom matchers
defined via `RSpec::Matchers.define` already have this. (Myron
Marston)
* Define noun-phrase aliases for built-in matchers, which can be
used when creating composed matcher expressions that read better
and provide better failure messages. (Myron Marston)
* Add `RSpec::Machers.alias_matcher` so users can define their own
matcher aliases. The `description` of the matcher will reflect the
alternate matcher name. (Myron Marston)

Breaking Changes for 3.0.0:

Expand Down
4 changes: 2 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ end

### deps for rdoc.info
group :documentation do
gem 'yard', '0.8.0', :require => false
gem 'redcarpet', '2.1.1', :platform => :mri
gem 'yard', '0.8.7.3', :require => false
gem 'redcarpet', '2.1.1', :platform => :mri
gem 'github-markup', '0.7.2'
end

Expand Down
85 changes: 78 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ expect(actual).to be >= expected
expect(actual).to be <= expected
expect(actual).to be < expected
expect(actual).to be_within(delta).of(expected)
expect(array).to match_array(expected)
```

### Regular expressions
Expand Down Expand Up @@ -152,21 +151,27 @@ expect(1..10).to cover(3)
expect(actual).to include(expected)
expect(actual).to start_with(expected)
expect(actual).to end_with(expected)

expect(actual).to contain_exactly(individual, items)
# ...which is the same as:
expect(actual).to match_array(expected_array)
```

#### Examples

```ruby
expect([1,2,3]).to include(1)
expect([1,2,3]).to include(1, 2)
expect([1,2,3]).to start_with(1)
expect([1,2,3]).to start_with(1,2)
expect([1,2,3]).to end_with(3)
expect([1,2,3]).to end_with(2,3)
expect([1, 2, 3]).to include(1)
expect([1, 2, 3]).to include(1, 2)
expect([1, 2, 3]).to start_with(1)
expect([1, 2, 3]).to start_with(1, 2)
expect([1, 2, 3]).to end_with(3)
expect([1, 2, 3]).to end_with(2, 3)
expect({:a => 'b'}).to include(:a => 'b')
expect("this string").to include("is str")
expect("this string").to start_with("this")
expect("this string").to end_with("ring")
expect([1, 2, 3]).to contain_exactly(2, 3, 1)
expect([1, 2, 3]).to match_array([3, 2, 1])
```

## `should` syntax
Expand All @@ -180,6 +185,72 @@ actual.should be > 3
[1, 2, 3].should_not include 4
```

## Compound Matcher Expressions

You can also create compound matcher expressions using `and` or `or`:

``` ruby
expect(alphabet).to start_with("a").and end_with("z")
expect(stoplight.color).to eq("red").or eq("green").or eq("yellow")
```

## Composing Matchers

Many of the built-in matchers are designed to take matchers as
arguments, to allow you to flexibly specify only the essential
aspects of an object or data structure. In addition, all of the
built-in matchers have one or more aliases that provide better
phrasing for when they are used as arguments to another matcher.

### Examples

```ruby
expect { k += 1.05 }.to change { k }.by( a_value_within(0.1).of(1.0) )

expect { s = "barn" }.to change { s }
.from( a_string_matching(/foo/) )
.to( a_string_matching(/bar/) )

expect(["barn", 2.45]).to contain_exactly(
a_value_within(0.1).of(2.5),
a_string_starting_with("bar")
)

expect(["barn", "food", 2.45]).to end_with(
a_string_matching("foo"),
a_value > 2
)

expect(["barn", 2.45]).to include( a_string_starting_with("bar") )

expect(:a => "food", :b => "good").to include(:a => a_string_matching(/foo/))

hash = {
:a => {
:b => ["foo", 5],
:c => { :d => 2.05 }
}
}

expect(hash).to match(
:a => {
:b => a_collection_containing_exactly(
a_string_starting_with("f"),
an_instance_of(Fixnum)
),
:c => { :d => (a_value < 3) }
}
)

expect { |probe|
[1, 2, 3].each(&probe)
}.to yield_successive_args(
a_number_that_is_odd,
a_number_that_is_even,
a_number_that_is_odd
)
```

See [detailed information on the `should` syntax and its usage.](https://github.com/rspec/rspec-expectations/blob/master/Should.md)

## Also see
Expand Down
22 changes: 0 additions & 22 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,11 @@ require 'rspec/expectations/version'
require 'cucumber/rake/task'
Cucumber::Rake::Task.new(:cucumber)

task :cleanup_rcov_files do
rm_rf 'coverage.data'
end

desc "Run all examples"
RSpec::Core::RakeTask.new(:spec) do |t|
t.ruby_opts = %w[-w]
end

if RUBY_VERSION.to_f == 1.8
namespace :rcov do
desc "Run all examples using rcov"
RSpec::Core::RakeTask.new :spec => :cleanup_rcov_files do |t|
t.rcov = true
t.rcov_opts = %[-Ilib -Ispec --exclude "gems/*,features"]
t.rcov_opts << %[--text-report --sort coverage --no-html --aggregate coverage.data]
end
desc "Run cucumber features using rcov"
Cucumber::Rake::Task.new :cucumber => :cleanup_rcov_files do |t|
t.cucumber_opts = %w{--format progress}
t.rcov = true
t.rcov_opts = %[-Ilib -Ispec --exclude "gems/*,features"]
t.rcov_opts << %[--text-report --sort coverage --aggregate coverage.data]
end
end
end

desc "Push docs/cukes to relishapp using the relish-client-gem"
task :relish, :version do |t, args|
raise "rake relish[VERSION]" unless args[:version]
Expand Down
113 changes: 113 additions & 0 deletions benchmarks/match_array/failing_with_distinct_items.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
$LOAD_PATH.unshift "./lib"
require 'benchmark'
require 'rspec/expectations'
require 'securerandom'

extend RSpec::Matchers

sizes = [10, 100, 1000, 2000, 4000]

puts "rspec-expectations #{RSpec::Expectations::Version::STRING} -- #{RUBY_ENGINE}/#{RUBY_VERSION}"

puts
puts "Failing `match_array` expectation with lists of distinct strings having 1 unmatched pair"
puts

Benchmark.benchmark do |bm|
sizes.each do |size|
actual = Array.new(size) { SecureRandom.uuid }

expecteds = Array.new(3) do
array = actual.shuffle
# replace one entry with a different value
array[rand(array.length)] = SecureRandom.uuid
array
end

expecteds.each do |expected|
bm.report("#{size.to_s.rjust(5)} items") do
begin
expect(actual).to match_array(expected)
rescue RSpec::Expectations::ExpectationNotMetError
else
raise "did not fail but should have"
end
end
end
end
end

__END__

Before new composable matchers algo:

10 items 0.000000 0.000000 0.000000 ( 0.000813)
10 items 0.000000 0.000000 0.000000 ( 0.000099)
10 items 0.000000 0.000000 0.000000 ( 0.000127)
100 items 0.000000 0.000000 0.000000 ( 0.000707)
100 items 0.000000 0.000000 0.000000 ( 0.000612)
100 items 0.000000 0.000000 0.000000 ( 0.000600)
1000 items 0.040000 0.000000 0.040000 ( 0.038679)
1000 items 0.040000 0.000000 0.040000 ( 0.041379)
1000 items 0.040000 0.000000 0.040000 ( 0.036680)
2000 items 0.130000 0.000000 0.130000 ( 0.131681)
2000 items 0.120000 0.000000 0.120000 ( 0.123664)
2000 items 0.130000 0.000000 0.130000 ( 0.128799)
4000 items 0.490000 0.000000 0.490000 ( 0.489446)
4000 items 0.510000 0.000000 0.510000 ( 0.511915)
4000 items 0.480000 0.010000 0.490000 ( 0.477616)

After:

10 items 0.000000 0.000000 0.000000 ( 0.001382)
10 items 0.000000 0.000000 0.000000 ( 0.000156)
10 items 0.000000 0.000000 0.000000 ( 0.000161)
100 items 0.010000 0.000000 0.010000 ( 0.005052)
100 items 0.000000 0.000000 0.000000 ( 0.004991)
100 items 0.010000 0.000000 0.010000 ( 0.004984)
1000 items 0.470000 0.000000 0.470000 ( 0.470043)
1000 items 0.500000 0.000000 0.500000 ( 0.499316)
1000 items 0.490000 0.000000 0.490000 ( 0.488582)
2000 items 1.910000 0.000000 1.910000 ( 1.917279)
2000 items 1.930000 0.010000 1.940000 ( 1.931002)
2000 items 1.920000 0.000000 1.920000 ( 1.928989)
4000 items 7.860000 0.010000 7.870000 ( 7.881995)
4000 items 7.980000 0.010000 7.990000 ( 8.003643)
4000 items 8.000000 0.010000 8.010000 ( 8.031382)

With "smaller subproblem" optimization: (about 25% slower)

10 items 0.010000 0.000000 0.010000 ( 0.001331)
10 items 0.000000 0.000000 0.000000 ( 0.000175)
10 items 0.000000 0.000000 0.000000 ( 0.000165)
100 items 0.000000 0.000000 0.000000 ( 0.006137)
100 items 0.010000 0.000000 0.010000 ( 0.005880)
100 items 0.000000 0.000000 0.000000 ( 0.005950)
1000 items 0.630000 0.000000 0.630000 ( 0.634294)
1000 items 0.620000 0.000000 0.620000 ( 0.622427)
1000 items 0.640000 0.000000 0.640000 ( 0.641505)
2000 items 2.420000 0.000000 2.420000 ( 2.419876)
2000 items 2.430000 0.000000 2.430000 ( 2.442544)
2000 items 2.380000 0.010000 2.390000 ( 2.385106)
4000 items 9.780000 0.010000 9.790000 ( 9.811499)
4000 items 9.670000 0.010000 9.680000 ( 9.688799)
4000 items 9.710000 0.010000 9.720000 ( 9.743054)

With "implement `values_match?` ourselves" optimization: (more than twice as fast!)

10 items 0.000000 0.000000 0.000000 ( 0.001189)
10 items 0.000000 0.000000 0.000000 ( 0.000149)
10 items 0.000000 0.000000 0.000000 ( 0.000130)
100 items 0.000000 0.000000 0.000000 ( 0.002927)
100 items 0.000000 0.000000 0.000000 ( 0.002856)
100 items 0.010000 0.000000 0.010000 ( 0.003028)
1000 items 0.250000 0.000000 0.250000 ( 0.245146)
1000 items 0.240000 0.000000 0.240000 ( 0.246291)
1000 items 0.320000 0.000000 0.320000 ( 0.315192)
2000 items 1.120000 0.000000 1.120000 ( 1.128162)
2000 items 1.030000 0.000000 1.030000 ( 1.034982)
2000 items 1.060000 0.000000 1.060000 ( 1.063870)
4000 items 4.530000 0.000000 4.530000 ( 4.556346)
4000 items 4.400000 0.010000 4.410000 ( 4.414447)
4000 items 4.410000 0.000000 4.410000 ( 4.417440)

Loading

0 comments on commit 7736337

Please sign in to comment.