Skip to content
216 changes: 215 additions & 1 deletion spec/std/spec/expectations_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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!" }
Expand Down Expand Up @@ -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
55 changes: 43 additions & 12 deletions src/spec/expectations.cr
Original file line number Diff line number Diff line change
Expand Up @@ -410,32 +410,63 @@ 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
end

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

Expand Down
Loading