diff --git a/CHANGELOG.md b/CHANGELOG.md index 40813e7..9a79e5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +### Added +- Added ability to cast types using the return value from expect statements with a type matcher. + ## [0.11.5] - 2022-12-18 ### Added - Added support for mock modules and types that include mocked modules. diff --git a/spec/features/expect_type_spec.cr b/spec/features/expect_type_spec.cr new file mode 100644 index 0000000..6991a8a --- /dev/null +++ b/spec/features/expect_type_spec.cr @@ -0,0 +1,36 @@ +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) + 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 diff --git a/src/spectator/expectation.cr b/src/spectator/expectation.cr index 780e299..79d8473 100644 --- a/src/spectator/expectation.cr +++ b/src/spectator/expectation.cr @@ -114,6 +114,21 @@ module Spectator report(match_data, message) 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 to(matcher : Matchers::TypeMatcher(U), message = nil) forall U + match_data = matcher.match(@expression) + value = @expression.value + if report(match_data, message) + return value if value.is_a?(U) + + raise "Spectator bug: expected value should have cast to #{U}" + else + raise TypeCastError.new("#{@expression.label} is expected to be a #{U}, but was actually #{value.class}") + end + end + # Asserts that a method is not called before the example completes. @[AlwaysInline] def to_not(stub : Stub, message = nil) : Nil @@ -136,6 +151,36 @@ module Spectator report(match_data, message) 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 to_not(matcher : Matchers::TypeMatcher(U), message = nil) forall U + match_data = matcher.negated_match(@expression) + value = @expression.value + if report(match_data, message) + return value unless value.is_a?(U) + + raise "Spectator bug: expected value should not be #{U}" + else + raise TypeCastError.new("#{@expression.label} is not expected to be a #{U}, but was actually #{value.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 to_not(matcher : Matchers::NilMatcher, message = nil) + match_data = matcher.negated_match(@expression) + if report(match_data, message) + value = @expression.value + return value unless value.nil? + + raise "Spectator bug: expected value should not be nil" + else + raise NilAssertionError.new("#{@expression.label} is not expected to be nil.") + end + end + # :ditto: @[AlwaysInline] def not_to(matcher, message = nil) : Nil