diff --git a/CHANGELOG.md b/CHANGELOG.md index ddcdc86..72d1db9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- Added ability to cast types using the return value from expect statements with a type matcher. +- Added ability to cast types using the return value from expect/should statements with a type matcher. ### Fixed - Fix invalid syntax (unterminated call) when recording calls to stubs with an un-named splat. diff --git a/spec/features/expect_type_spec.cr b/spec/features/expect_type_spec.cr index 6991a8a..6ed9949 100644 --- a/spec/features/expect_type_spec.cr +++ b/spec/features/expect_type_spec.cr @@ -1,36 +1,70 @@ require "../spec_helper" -Spectator.describe "Expect Type" do - it "ensures a type is cast" do - value = 42.as(String | Int32) - expect(value).to be_a(String | Int32) - expect(value).to compile_as(String | Int32) - value = expect(value).to be_a(Int32) - expect(value).to eq(42) - expect(value).to be_a(Int32) - expect(value).to compile_as(Int32) - expect(value).to_not respond_to(:downcase) +Spectator.describe "Expect Type", :smoke do + context "with expect syntax" do + it "ensures a type is cast" do + value = 42.as(String | Int32) + expect(value).to be_a(String | Int32) + expect(value).to compile_as(String | Int32) + value = expect(value).to be_a(Int32) + expect(value).to eq(42) + expect(value).to be_a(Int32) + expect(value).to compile_as(Int32) + expect(value).to_not respond_to(:downcase) + end + + it "ensures a type is not nil" do + value = 42.as(Int32?) + expect(value).to be_a(Int32?) + expect(value).to compile_as(Int32?) + value = expect(value).to_not be_nil + expect(value).to eq(42) + expect(value).to be_a(Int32) + expect(value).to compile_as(Int32) + expect { value.not_nil! }.to_not raise_error(NilAssertionError) + end + + it "removes types from a union" do + value = 42.as(String | Int32) + expect(value).to be_a(String | Int32) + expect(value).to compile_as(String | Int32) + value = expect(value).to_not be_a(String) + expect(value).to eq(42) + expect(value).to be_a(Int32) + expect(value).to compile_as(Int32) + expect(value).to_not respond_to(:downcase) + end end - it "ensures a type is not nil" do - value = 42.as(Int32?) - expect(value).to be_a(Int32?) - expect(value).to compile_as(Int32?) - value = expect(value).to_not be_nil - expect(value).to eq(42) - expect(value).to be_a(Int32) - expect(value).to compile_as(Int32) - expect { value.not_nil! }.to_not raise_error(NilAssertionError) - end + context "with should syntax" do + it "ensures a type is cast" do + value = 42.as(String | Int32) + value.should be_a(String | Int32) + value = value.should be_a(Int32) + value.should eq(42) + value.should be_a(Int32) + value.should compile_as(Int32) + value.should_not respond_to(:downcase) + end - it "removes types from a union" do - value = 42.as(String | Int32) - expect(value).to be_a(String | Int32) - expect(value).to compile_as(String | Int32) - value = expect(value).to_not be_a(String) - expect(value).to eq(42) - expect(value).to be_a(Int32) - expect(value).to compile_as(Int32) - expect(value).to_not respond_to(:downcase) + it "ensures a type is not nil" do + value = 42.as(Int32?) + value.should be_a(Int32?) + value = value.should_not be_nil + value.should eq(42) + value.should be_a(Int32) + value.should compile_as(Int32) + expect { value.not_nil! }.to_not raise_error(NilAssertionError) + end + + it "removes types from a union" do + value = 42.as(String | Int32) + value.should be_a(String | Int32) + value = value.should_not be_a(String) + value.should eq(42) + value.should be_a(Int32) + value.should compile_as(Int32) + value.should_not respond_to(:downcase) + end end end diff --git a/src/spectator/should.cr b/src/spectator/should.cr index d444c00..f0fe075 100644 --- a/src/spectator/should.cr +++ b/src/spectator/should.cr @@ -30,6 +30,23 @@ class Object ::Spectator::Harness.current.report(expectation) end + # Asserts that some criteria defined by the matcher is satisfied. + # Allows a custom message to be used. + # Returns the expected value cast as the expected type, if the matcher is satisfied. + def should(matcher : ::Spectator::Matchers::TypeMatcher(U), message = nil, *, _file = __FILE__, _line = __LINE__) forall U + actual = ::Spectator::Value.new(self) + location = ::Spectator::Location.new(_file, _line) + match_data = matcher.match(actual) + expectation = ::Spectator::Expectation.new(match_data, location, message) + if ::Spectator::Harness.current.report(expectation) + return self if self.is_a?(U) + + raise "Spectator bug: expected value should have cast to #{U}" + else + raise TypeCastError.new("Expected value should be a #{U}, but was actually #{self.class}") + end + end + # Works the same as `#should` except the condition is inverted. # When `#should` succeeds, this method will fail, and vice-versa. def should_not(matcher, message = nil, *, _file = __FILE__, _line = __LINE__) @@ -40,6 +57,40 @@ class Object ::Spectator::Harness.current.report(expectation) end + # Asserts that some criteria defined by the matcher is not satisfied. + # Allows a custom message to be used. + # Returns the expected value cast without the unexpected type, if the matcher is satisfied. + def should_not(matcher : ::Spectator::Matchers::TypeMatcher(U), message = nil, *, _file = __FILE__, _line = __LINE__) forall U + actual = ::Spectator::Value.new(self) + location = ::Spectator::Location.new(_file, _line) + match_data = matcher.negated_match(actual) + expectation = ::Spectator::Expectation.new(match_data, location, message) + if ::Spectator::Harness.current.report(expectation) + return self unless self.is_a?(U) + + raise "Spectator bug: expected value should not be #{U}" + else + raise TypeCastError.new("Expected value is not expected to be a #{U}, but was actually #{self.class}") + end + end + + # Asserts that some criteria defined by the matcher is not satisfied. + # Allows a custom message to be used. + # Returns the expected value cast as a non-nillable type, if the matcher is satisfied. + def should_not(matcher : ::Spectator::Matchers::NilMatcher, message = nil, *, _file = __FILE__, _line = __LINE__) + actual = ::Spectator::Value.new(self) + location = ::Spectator::Location.new(_file, _line) + match_data = matcher.negated_match(actual) + expectation = ::Spectator::Expectation.new(match_data, location, message) + if ::Spectator::Harness.current.report(expectation) + return self unless self.nil? + + raise "Spectator bug: expected value should not be nil" + else + raise NilAssertionError.new("Expected value should not be nil.") + end + end + # Works the same as `#should` except that the condition check is postponed. # The expectation is checked after the example finishes and all hooks have run. def should_eventually(matcher, message = nil, *, _file = __FILE__, _line = __LINE__)