From 590d81979eeef0eb0c852d0651c9a989681c8a5b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 6 Jan 2020 22:04:05 -0700 Subject: [PATCH 1/8] Workaround typing issues --- src/spectator/matchers/end_with_matcher.cr | 24 +++++++++++--------- src/spectator/matchers/start_with_matcher.cr | 6 +++-- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/spectator/matchers/end_with_matcher.cr b/src/spectator/matchers/end_with_matcher.cr index 4d77a36..f555153 100644 --- a/src/spectator/matchers/end_with_matcher.cr +++ b/src/spectator/matchers/end_with_matcher.cr @@ -23,7 +23,8 @@ module Spectator::Matchers # Actually performs the test against the expression. def match(actual : TestExpression(T)) : MatchData forall T - if (value = actual.value).responds_to?(:ends_with?) + value = actual.value + if value.is_a?(String) || value.responds_to?(:ends_with?) match_ends_with(value, actual.label) else match_last(value, actual.label) @@ -33,10 +34,11 @@ module Spectator::Matchers # Performs the test against the expression, but inverted. # A successful match with `#match` should normally fail for this method, and vice-versa. def negated_match(actual : TestExpression(T)) : MatchData forall T - if actual.value.responds_to?(:ends_with?) - negated_match_ends_with(actual) + value = actual.value + if value.is_a?(String) || value.responds_to?(:ends_with?) + negated_match_ends_with(value, actual.label) else - negated_match_last(actual) + negated_match_last(value, actual.label) end end @@ -72,11 +74,11 @@ module Spectator::Matchers # Checks whether the actual value does not end with the expected value. # This method expects (and uses) the `#ends_with?` method on the value. - private def negated_match_ends_with(actual) - if actual.value.ends_with?(expected.value) - FailedMatchData.new(description, "#{actual.label} ends with #{expected.label} (using #ends_with?)", + private def negated_match_ends_with(actual_value, actual_label) + if actual_value.ends_with?(expected.value) + FailedMatchData.new(description, "#{actual_label} ends with #{expected.label} (using #ends_with?)", expected: expected.value.inspect, - actual: actual.value.inspect + actual: actual_value.inspect ) else SuccessfulMatchData.new(description) @@ -85,12 +87,12 @@ module Spectator::Matchers # Checks whether the last element of the value is not the expected value. # This method expects that the actual value is a set (enumerable). - private def negated_match_last(actual) - list = actual.value.to_a + private def negated_match_last(actual_value, actual_label) + list = actual_value.to_a last = list.last if expected.value === last - FailedMatchData.new(description, "#{actual.label} ends with #{expected.label} (using expected === last)", + FailedMatchData.new(description, "#{actual_label} ends with #{expected.label} (using expected === last)", expected: expected.value.inspect, actual: last.inspect, list: list.inspect diff --git a/src/spectator/matchers/start_with_matcher.cr b/src/spectator/matchers/start_with_matcher.cr index aaa8846..02060e7 100644 --- a/src/spectator/matchers/start_with_matcher.cr +++ b/src/spectator/matchers/start_with_matcher.cr @@ -22,7 +22,8 @@ module Spectator::Matchers # Actually performs the test against the expression. def match(actual : TestExpression(T)) : MatchData forall T - if (value = actual.value).responds_to?(:starts_with?) + value = actual.value + if value.is_a?(String) || value.responds_to?(:starts_with?) match_starts_with(value, actual.label) else match_first(value, actual.label) @@ -32,7 +33,8 @@ module Spectator::Matchers # Performs the test against the expression, but inverted. # A successful match with `#match` should normally fail for this method, and vice-versa. def negated_match(actual : TestExpression(T)) : MatchData forall T - if (value = actual.value).responds_to?(:starts_with?) + value = actual.value + if value.is_a?(String) || value.responds_to?(:starts_with?) negated_match_starts_with(value, actual.label) else negated_match_first(value, actual.label) From 5fa6b5d5494070a3497102490da144ec962f050b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 6 Jan 2020 22:11:36 -0700 Subject: [PATCH 2/8] Fix negation expectation text --- src/spectator/matchers/end_with_matcher.cr | 4 ++-- src/spectator/matchers/start_with_matcher.cr | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/spectator/matchers/end_with_matcher.cr b/src/spectator/matchers/end_with_matcher.cr index f555153..319c559 100644 --- a/src/spectator/matchers/end_with_matcher.cr +++ b/src/spectator/matchers/end_with_matcher.cr @@ -77,7 +77,7 @@ module Spectator::Matchers private def negated_match_ends_with(actual_value, actual_label) if actual_value.ends_with?(expected.value) FailedMatchData.new(description, "#{actual_label} ends with #{expected.label} (using #ends_with?)", - expected: expected.value.inspect, + expected: "Not #{expected.value.inspect}", actual: actual_value.inspect ) else @@ -93,7 +93,7 @@ module Spectator::Matchers if expected.value === last FailedMatchData.new(description, "#{actual_label} ends with #{expected.label} (using expected === last)", - expected: expected.value.inspect, + expected: "Not #{expected.value.inspect}", actual: last.inspect, list: list.inspect ) diff --git a/src/spectator/matchers/start_with_matcher.cr b/src/spectator/matchers/start_with_matcher.cr index 02060e7..b459bb4 100644 --- a/src/spectator/matchers/start_with_matcher.cr +++ b/src/spectator/matchers/start_with_matcher.cr @@ -76,7 +76,7 @@ module Spectator::Matchers private def negated_match_starts_with(actual_value, actual_label) if actual_value.starts_with?(expected.value) FailedMatchData.new(description, "#{actual_label} starts with #{expected.label} (using #starts_with?)", - expected: expected.value.inspect, + expected: "Not #{expected.value.inspect}", actual: actual_value.inspect ) else @@ -92,7 +92,7 @@ module Spectator::Matchers if expected.value === first FailedMatchData.new(description, "#{actual_label} starts with #{expected.label} (using expected === first)", - expected: expected.value.inspect, + expected: "Not #{expected.value.inspect}", actual: first.inspect, list: list.inspect ) From f23141b3e164020dc8c7f4406c7e5a840773ae4c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 6 Jan 2020 22:19:09 -0700 Subject: [PATCH 3/8] Add RSpec `start_with` and `end_with` matchers specs --- .../expectations/end_with_matcher_spec.cr | 33 +++++++++++++++++++ .../expectations/start_with_matcher_spec.cr | 33 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 spec/rspec/expectations/end_with_matcher_spec.cr create mode 100644 spec/rspec/expectations/start_with_matcher_spec.cr diff --git a/spec/rspec/expectations/end_with_matcher_spec.cr b/spec/rspec/expectations/end_with_matcher_spec.cr new file mode 100644 index 0000000..c19720f --- /dev/null +++ b/spec/rspec/expectations/end_with_matcher_spec.cr @@ -0,0 +1,33 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/end-with-matcher +# and modified to fit Spectator and Crystal. +Spectator.describe "`end_with` matcher" do + context "string usage" do + describe "this string" do + it { is_expected.to end_with "string" } + it { is_expected.not_to end_with "stringy" } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to end_with "string" } + xit { is_expected.to end_with "stringy" } + end + end + + context "array usage" do + describe [0, 1, 2, 3, 4] do + it { is_expected.to end_with 4 } + # TODO: Add support for multiple items at the end of an array. + # it { is_expected.to end_with 3, 4 } + it { is_expected.not_to end_with 3 } + # it { is_expected.not_to end_with 0, 1, 2, 3, 4, 5 } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to end_with 4 } + xit { is_expected.to end_with 3 } + end + end +end diff --git a/spec/rspec/expectations/start_with_matcher_spec.cr b/spec/rspec/expectations/start_with_matcher_spec.cr new file mode 100644 index 0000000..c2942cc --- /dev/null +++ b/spec/rspec/expectations/start_with_matcher_spec.cr @@ -0,0 +1,33 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/start-with-matcher +# and modified to fit Spectator and Crystal. +Spectator.describe "`start_with` matcher" do + context "with a string" do + describe "this string" do + it { is_expected.to start_with "this" } + it { is_expected.not_to start_with "that" } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to start_with "this" } + xit { is_expected.to start_with "that" } + end + end + + context "with an array" do + describe [0, 1, 2, 3, 4] do + it { is_expected.to start_with 0 } + # TODO: Add support for multiple items at the beginning of an array. + # it { is_expected.to start_with(0, 1) } + it { is_expected.not_to start_with(2) } + # it { is_expected.not_to start_with(0, 1, 2, 3, 4, 5) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to start_with 0 } + xit { is_expected.to start_with 3 } + end + end +end From 6ad861365c73ac4616b1f98316d80b960b0e3fbc Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 6 Jan 2020 22:33:52 -0700 Subject: [PATCH 4/8] Add RSpec `have_attributes` matcher spec --- .../have_attributes_matcher_spec.cr | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 spec/rspec/expectations/have_attributes_matcher_spec.cr diff --git a/spec/rspec/expectations/have_attributes_matcher_spec.cr b/spec/rspec/expectations/have_attributes_matcher_spec.cr new file mode 100644 index 0000000..dfa273b --- /dev/null +++ b/spec/rspec/expectations/have_attributes_matcher_spec.cr @@ -0,0 +1,37 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/have-attributes-matcher +# and modified to fit Spectator and Crystal. +Spectator.describe "`have_attributes` matcher" do + context "basic usage" do + # Use `record` instead of `Struct.new`. + record Person, name : String, age : Int32 + + describe Person.new("Jim", 32) do + # Changed some syntax for Ruby hashes to Crystal named tuples. + + # Spectator doesn't support helper matchers like `a_string_starting_with` and `a_value <`. + # But maybe in the future it will. + it { is_expected.to have_attributes(name: "Jim") } + # it { is_expected.to have_attributes(name: a_string_starting_with("J") ) } + it { is_expected.to have_attributes(age: 32) } + # it { is_expected.to have_attributes(age: (a_value > 30) ) } + it { is_expected.to have_attributes(name: "Jim", age: 32) } + # it { is_expected.to have_attributes(name: a_string_starting_with("J"), age: (a_value > 30) ) } + it { is_expected.not_to have_attributes(name: "Bob") } + it { is_expected.not_to have_attributes(age: 10) } + # it { is_expected.not_to have_attributes(age: (a_value < 30) ) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.to have_attributes(name: "Bob") } + xit { is_expected.to have_attributes(name: 10) } + + # fails if any of the attributes don't match + xit { is_expected.to have_attributes(name: "Bob", age: 32) } + xit { is_expected.to have_attributes(name: "Jim", age: 10) } + xit { is_expected.to have_attributes(name: "Bob", age: 10) } + end + end +end From b43351120197dde0fd95c711d525b590b7573c2c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 6 Jan 2020 22:51:47 -0700 Subject: [PATCH 5/8] Add RSpec `include/contain` matcher spec --- .../expectations/contain_matcher_spec.cr | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 spec/rspec/expectations/contain_matcher_spec.cr diff --git a/spec/rspec/expectations/contain_matcher_spec.cr b/spec/rspec/expectations/contain_matcher_spec.cr new file mode 100644 index 0000000..a75897c --- /dev/null +++ b/spec/rspec/expectations/contain_matcher_spec.cr @@ -0,0 +1,99 @@ +require "../../spec_helper" + +# In Ruby, this is the `include` matcher. +# However, `include` is a reserved keyword in Crystal. +# So instead, it is `contain` in Spectator. +Spectator.describe "`contain` matcher" do + context "array usage" do + describe [1, 3, 7] do + it { is_expected.to contain(1) } + it { is_expected.to contain(3) } + it { is_expected.to contain(7) } + it { is_expected.to contain(1, 7) } + it { is_expected.to contain(1, 3, 7) } + + # Utility matcher method `a_kind_of` is not supported. + # it { is_expected.to contain(a_kind_of(Int)) } + + # TODO: Compound matchers aren't supported. + # it { is_expected.to contain(be_odd.and be < 10) } + + # TODO: Fix behavior and cleanup output. + # This syntax is allowed, but produces a wrong result and bad output. + xit { is_expected.to contain(be_odd) } + xit { is_expected.not_to contain(be_even) } + + it { is_expected.not_to contain(17) } + it { is_expected.not_to contain(43, 100) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.to contain(4) } + xit { is_expected.to contain(be_even) } + xit { is_expected.not_to contain(1) } + xit { is_expected.not_to contain(3) } + xit { is_expected.not_to contain(7) } + xit { is_expected.not_to contain(1, 3, 7) } + + # both of these should fail since it contains 1 but not 9 + xit { is_expected.to contain(1, 9) } + xit { is_expected.not_to contain(1, 9) } + end + end + + context "string usage" do + describe "a string" do + it { is_expected.to contain("str") } + it { is_expected.to contain("a", "str", "ng") } + it { is_expected.not_to contain("foo") } + it { is_expected.not_to contain("foo", "bar") } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.to contain("foo") } + xit { is_expected.not_to contain("str") } + xit { is_expected.to contain("str", "foo") } + xit { is_expected.not_to contain("str", "foo") } + end + end + + context "hash usage" do + # A hash can't be described inline here for some reason. + # So it is placed in the subject instead. + describe ":a => 7, :b => 5" do + subject { {:a => 7, :b => 5} } + + # Hash syntax is changed here from `:a => 7` to `a: 7`. + xit { is_expected.to contain(:a) } + xit { is_expected.to contain(:b, :a) } + + # TODO: This hash-like syntax isn't supported. + # it { is_expected.to contain(a: 7) } + # it { is_expected.to contain(b: 5, a: 7) } + xit { is_expected.not_to contain(:c) } + xit { is_expected.not_to contain(:c, :d) } + # it { is_expected.not_to contain(d: 2) } + # it { is_expected.not_to contain(a: 5) } + # it { is_expected.not_to contain(b: 7, a: 5) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to contain(:a) } + xit { is_expected.not_to contain(:b, :a) } + # it { is_expected.not_to contain(a: 7) } + # it { is_expected.not_to contain(a: 7, b: 5) } + xit { is_expected.to contain(:c) } + xit { is_expected.to contain(:c, :d) } + # it { is_expected.to contain(d: 2) } + # it { is_expected.to contain(a: 5) } + # it { is_expected.to contain(a: 5, b: 7) } + + # Mixed cases--the hash contains one but not the other. + # All 4 of these cases should fail. + xit { is_expected.to contain(:a, :d) } + xit { is_expected.not_to contain(:a, :d) } + # it { is_expected.to contain(a: 7, d: 3) } + # it { is_expected.not_to contain(a: 7, d: 3) } + end + end +end From f11b548f4e79a1cf283d99b57b3345ca169fa7aa Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 6 Jan 2020 23:01:45 -0700 Subject: [PATCH 6/8] Mimic RSpec behavior of `match` matcher The code: expect(/foo/).to match("food") would normally evaluate: "food" === /foo/ which is false. However, in RSpec, this expectation is true. --- src/spectator/matchers/case_matcher.cr | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/spectator/matchers/case_matcher.cr b/src/spectator/matchers/case_matcher.cr index e7f0891..fcb5c23 100644 --- a/src/spectator/matchers/case_matcher.cr +++ b/src/spectator/matchers/case_matcher.cr @@ -16,6 +16,12 @@ module Spectator::Matchers expected.value === actual.value end + # Overload that takes a regex so that the operands are flipped. + # This mimics RSpec's behavior. + private def match?(actual : TestExpression(Regex)) : Bool forall T + actual.value === expected.value + end + # Message displayed when the matcher isn't satisifed. # # This is only called when `#match?` returns false. From 6a0a73ca7674cf0519b1a9d81aee3f0dc8ead880 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 6 Jan 2020 23:05:31 -0700 Subject: [PATCH 7/8] Add RSpec `match` matcher spec --- spec/rspec/expectations/match_matcher_spec.cr | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 spec/rspec/expectations/match_matcher_spec.cr diff --git a/spec/rspec/expectations/match_matcher_spec.cr b/spec/rspec/expectations/match_matcher_spec.cr new file mode 100644 index 0000000..28b4f72 --- /dev/null +++ b/spec/rspec/expectations/match_matcher_spec.cr @@ -0,0 +1,30 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/match-matcher +# and modified to fit Spectator and Crystal. +Spectator.describe "`match` matcher" do + context "string usage" do + describe "a string" do + it { is_expected.to match(/str/) } + it { is_expected.not_to match(/foo/) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to match(/str/) } + xit { is_expected.to match(/foo/) } + end + end + + context "regular expression usage" do + describe /foo/ do + it { is_expected.to match("food") } + it { is_expected.not_to match("drinks") } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to match("food") } + xit { is_expected.to match("drinks") } + end + end +end From d5c5a82395127a675c8244b17b3ed19f2b2bf438 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 6 Jan 2020 23:46:38 -0700 Subject: [PATCH 8/8] Add with_message modifier to raise_error matcher --- src/spectator/matchers/exception_matcher.cr | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/spectator/matchers/exception_matcher.cr b/src/spectator/matchers/exception_matcher.cr index f94c3f2..82d67ab 100644 --- a/src/spectator/matchers/exception_matcher.cr +++ b/src/spectator/matchers/exception_matcher.cr @@ -90,6 +90,11 @@ module Spectator::Matchers end end + def with_message(message : T) forall T + value = TestValue.new(message) + ExceptionMatcher(ExceptionType, T).new(value) + end + # Runs a block of code and returns the exception it threw. # If no exception was thrown, *nil* is returned. private def capture_exception