diff --git a/spec/std/spec/expectations_spec.cr b/spec/std/spec/expectations_spec.cr index 0831bca226ca..2e7e9d8df57f 100644 --- a/spec/std/spec/expectations_spec.cr +++ b/spec/std/spec/expectations_spec.cr @@ -12,6 +12,16 @@ private record NoObjectId, to_unsafe : Int32 do end end +private class ExceptionWithOverriddenToS < Exception + def initialize(message : String, @to_s : String) + super(message) + end + + def to_s + @to_s + end +end + describe "expectations" do describe "accept a custom failure message" do it { 1.should be < 3, "custom message!" } @@ -169,8 +179,212 @@ describe "expectations" do end describe "expect_raises" do - it "pass if raises MyError" do + it "passes if expected message equals actual message and expected class equals actual class" do expect_raises(Exception, "Ops") { raise Exception.new("Ops") } end + + it "passes if expected message equals actual message and expected class is an ancestor of actual class" do + expect_raises(Exception, "Ops") { raise ArgumentError.new("Ops") } + end + + it "passes if expected message is a substring of actual message and expected class equals actual class" do + expect_raises(Exception, "Ops") { raise Exception.new("Black Ops") } + end + + it "passes if expected message is a substring of actual message and expected class is an ancestor of actual class" do + expect_raises(Exception, "Ops") { raise ArgumentError.new("Black Ops") } + end + + it "passes if expected regex matches actual message and expected class equals actual class" do + expect_raises(Exception, /Ops/) { raise Exception.new("Black Ops") } + end + + it "passes if expected regex matches actual message and expected class is an ancestor of actual class" do + expect_raises(Exception, /Ops/) { raise ArgumentError.new("Black Ops") } + end + + it "passes if given no message expectation and expected class equals actual class" do + expect_raises(Exception) { raise Exception.new("Ops") } + end + + it "passes if given no message expectation and expected class is an ancestor of actual class" do + expect_raises(Exception) { raise ArgumentError.new("Ops") } + end + + it "passes if given no message expectation, actual message is nil and expected class equals actual class" do + expect_raises(Exception) { raise Exception.new(nil) } + end + + it "passes if given no message expectation, actual message is nil and expected class is an ancestor of actual class" do + expect_raises(Exception) { raise ArgumentError.new(nil) } + end + + it "fails if expected message does not equal actual message and expected class equals actual class" do + expect_raises(Exception, "Ops") { raise Exception.new("Hm") } + rescue Spec::AssertionFailed + # success + else + fail "expected Spec::AssertionFailed but nothing was raised" + end + + it "fails if given expected message, actual message is nil and expected class equals actual class" do + expect_raises(Exception, "Ops") { raise Exception.new(nil) } + rescue Spec::AssertionFailed + # success + else + fail "expected Spec::AssertionFailed but nothing was raised" + end + + it "fails if expected regex does not match actual message and expected class equals actual class" do + expect_raises(Exception, /Ops/) { raise Exception.new("Hm") } + rescue Spec::AssertionFailed + # success + else + fail "expected Spec::AssertionFailed but nothing was raised" + end + + it "fails if given expected regex, actual message is nil and expected class equals actual class" do + expect_raises(Exception, /Ops/) { raise Exception.new(nil) } + rescue Spec::AssertionFailed + # success + else + fail "expected Spec::AssertionFailed but nothing was raised" + end + + it "fails if given no message expectation and expected class does not equal and is not an ancestor of actual class" do + expect_raises(IndexError) { raise ArgumentError.new("Ops") } + rescue Spec::AssertionFailed + # success + else + fail "expected Spec::AssertionFailed but nothing was raised" + end + + it "fails if given no message expectation, actual message is nil and expected class does not equal and is not an ancestor of actual class" do + expect_raises(IndexError) { raise ArgumentError.new(nil) } + rescue Spec::AssertionFailed + # success + else + fail "expected Spec::AssertionFailed but nothing was raised" + end + + it "fails if nothing was raised" do + expect_raises(IndexError) { raise ArgumentError.new("Ops") } + rescue Spec::AssertionFailed + # success + else + fail "expected Spec::AssertionFailed but nothing was raised" + end + + it "uses the exception's #to_s output to match a given String" do + expect_raises(Exception, "Hm") { raise ExceptionWithOverriddenToS.new("Ops", to_s: "Hm") } + end + + it "uses the exception's #to_s output to match a given Regex" do + expect_raises(Exception, /Hm/) { raise ExceptionWithOverriddenToS.new("Ops", to_s: "Hm") } + end + + describe "failure message format" do + context "given string to compare with message" do + it "contains expected exception, actual exception and backtrace" do + expect_raises(Exception, "digits should be non-negative") do + raise IndexError.new("Index out of bounds") + end + rescue e : Spec::AssertionFailed + # don't check backtrace items because they are platform specific + e.message.as(String).should contain(<<-MESSAGE) + Expected Exception with message containing: "digits should be non-negative" + got IndexError with message: "Index out of bounds" + Backtrace: + MESSAGE + else + fail "expected Spec::AssertionFailed but nothing is raised" + end + + it "contains expected class, actual exception and backtrace when expected class does not match actual class" do + expect_raises(ArgumentError, "digits should be non-negative") do + raise IndexError.new("Index out of bounds") + end + rescue e : Spec::AssertionFailed + # don't check backtrace items because they are platform specific + e.message.as(String).should contain(<<-MESSAGE) + Expected ArgumentError + got IndexError with message: "Index out of bounds" + Backtrace: + MESSAGE + else + fail "expected Spec::AssertionFailed but nothing is raised" + end + + it "escapes expected and actual messages in the same way" do + expect_raises(Exception, %q(a\tb\nc)) do + raise %q(a\tb\nc).inspect + end + rescue e : Spec::AssertionFailed + e.message.as(String).should contain("Expected Exception with message containing: #{%q(a\tb\nc).inspect}") + e.message.as(String).should contain("got Exception with message: #{%q(a\tb\nc).inspect.inspect}") + else + fail "expected Spec::AssertionFailed but nothing is raised" + end + end + + context "given regex to match a message" do + it "contains expected exception, actual exception and backtrace" do + expect_raises(Exception, /digits should be non-negative/) do + raise IndexError.new("Index out of bounds") + end + rescue e : Spec::AssertionFailed + # don't check backtrace items because they are platform specific + e.message.as(String).should contain(<<-MESSAGE) + Expected Exception with message matching: /digits should be non-negative/ + got IndexError with message: "Index out of bounds" + Backtrace: + MESSAGE + else + fail "expected Spec::AssertionFailed but nothing is raised" + end + + it "contains expected class, actual exception and backtrace when expected class does not match actual class" do + expect_raises(ArgumentError, /digits should be non-negative/) do + raise IndexError.new("Index out of bounds") + end + rescue e : Spec::AssertionFailed + # don't check backtrace items because they are platform specific + e.message.as(String).should contain(<<-MESSAGE) + Expected ArgumentError + got IndexError with message: "Index out of bounds" + Backtrace: + MESSAGE + else + fail "expected Spec::AssertionFailed but nothing is raised" + end + end + + context "given nil to allow any message" do + it "contains expected class, actual exception and backtrace when expected class does not match actual class" do + expect_raises(ArgumentError, nil) do + raise IndexError.new("Index out of bounds") + end + rescue e : Spec::AssertionFailed + # don't check backtrace items because they are platform specific + e.message.as(String).should contain(<<-MESSAGE) + Expected ArgumentError + got IndexError with message: "Index out of bounds" + Backtrace: + MESSAGE + else + fail "expected Spec::AssertionFailed but nothing is raised" + end + end + + context "nothing was raises" do + it "contains expected class" do + expect_raises(IndexError) { } + rescue e : Spec::AssertionFailed + e.message.as(String).should contain("Expected IndexError but nothing was raised") + else + fail "expected Spec::AssertionFailed but nothing was raised" + end + end + end end end diff --git a/src/spec/expectations.cr b/src/spec/expectations.cr index 158c82413fc5..79cad0680417 100644 --- a/src/spec/expectations.cr +++ b/src/spec/expectations.cr @@ -410,19 +410,17 @@ module Spec raise ex end - ex_to_s = ex.to_s + exception_as_string = ex.to_s case message when Regex - unless (ex_to_s =~ message) - backtrace = ex.backtrace.join('\n') { |f| " # #{f}" } - fail "Expected #{klass} with message matching #{message.pretty_inspect}, " \ - "got #<#{ex.class}: #{ex_to_s}> with backtrace:\n#{backtrace}", file, line + unless exception_as_string =~ message + expectation_failed_message = build_expectation_failed_message(klass, message, ex, exception_as_string) + fail expectation_failed_message, file, line end when String - unless ex_to_s.includes?(message) - backtrace = ex.backtrace.join('\n') { |f| " # #{f}" } - fail "Expected #{klass} with #{message.pretty_inspect}, got #<#{ex.class}: " \ - "#{ex_to_s}> with backtrace:\n#{backtrace}", file, line + unless exception_as_string.includes?(message) + expectation_failed_message = build_expectation_failed_message(klass, message, ex, exception_as_string) + fail expectation_failed_message, file, line end when Nil # No need to check the message @@ -430,12 +428,45 @@ module Spec ex rescue ex - backtrace = ex.backtrace.join('\n') { |f| " # #{f}" } - fail "Expected #{klass}, got #<#{ex.class}: #{ex}> with backtrace:\n" \ - "#{backtrace}", file, line + expectation_failed_message = build_expectation_failed_message(klass, ex) + fail expectation_failed_message, file, line else fail "Expected #{klass} but nothing was raised", file, line end + + private def build_expectation_failed_message(klass : Class, message : String, exception : Exception, exception_as_string : String) + backtrace = " # #{exception.backtrace.join("\n # ")}" + + <<-MESSAGE + Expected #{klass} with message containing: #{message.inspect} + got #{exception.class} with message: #{exception_as_string.inspect} + Backtrace: + #{backtrace} + MESSAGE + end + + private def build_expectation_failed_message(klass : Class, message : Regex, exception : Exception, exception_as_string : String) + backtrace = " # #{exception.backtrace.join("\n # ")}" + + <<-MESSAGE + Expected #{klass} with message matching: #{message.inspect} + got #{exception.class} with message: #{exception_as_string.inspect} + Backtrace: + #{backtrace} + MESSAGE + end + + private def build_expectation_failed_message(klass : Class, exception : Exception) + exception_as_string = exception.to_s + backtrace = " # #{exception.backtrace.join("\n # ")}" + + <<-MESSAGE + Expected #{klass} + got #{exception.class} with message: #{exception_as_string.inspect} + Backtrace: + #{backtrace} + MESSAGE + end {% end %} end