282 lines
11 KiB
Crystal
282 lines
11 KiB
Crystal
require "json"
|
|
require "./expression"
|
|
require "./location"
|
|
|
|
module Spectator
|
|
# Result of evaluating a matcher on a target.
|
|
# Contains information about the match,
|
|
# such as whether it was successful and a description of the operation.
|
|
struct Expectation
|
|
# Location of the expectation in source code.
|
|
# This can be nil if the location can't be captured,
|
|
# for instance using the *should* syntax or dynamically created expectations.
|
|
getter! location : Location
|
|
|
|
# Indicates whether the expectation was met.
|
|
def satisfied?
|
|
@match_data.matched?
|
|
end
|
|
|
|
# Indicates whether the expectation was not met.
|
|
def failed?
|
|
!satisfied?
|
|
end
|
|
|
|
# If nil, then the match was successful.
|
|
def failure_message?
|
|
return unless match_data = @match_data.as?(Matchers::FailedMatchData)
|
|
|
|
case message = @message
|
|
when String then message
|
|
when Proc(String) then @message = message.call # Cache result of call.
|
|
else match_data.failure_message
|
|
end
|
|
end
|
|
|
|
# Description of why the match failed.
|
|
def failure_message
|
|
failure_message?.not_nil!
|
|
end
|
|
|
|
# Additional information about the match, useful for debug.
|
|
# If nil, then the match was successful.
|
|
def values?
|
|
@match_data.as?(Matchers::FailedMatchData).try(&.values)
|
|
end
|
|
|
|
# Additional information about the match, useful for debug.
|
|
def values
|
|
values?.not_nil!
|
|
end
|
|
|
|
def description
|
|
@match_data.description
|
|
end
|
|
|
|
# Creates the expectation.
|
|
# The *match_data* comes from the result of calling `Matcher#match`.
|
|
# The *location* is the location of the expectation in source code, if available.
|
|
# A custom *message* can be used in case of a failure.
|
|
def initialize(@match_data : Matchers::MatchData, @location : Location? = nil,
|
|
@message : String? | Proc(String) = nil)
|
|
end
|
|
|
|
# Creates the JSON representation of the expectation.
|
|
def to_json(json : JSON::Builder)
|
|
json.object do
|
|
if location = @location
|
|
json.field("file_path", location.path)
|
|
json.field("line_number", location.line)
|
|
end
|
|
json.field("satisfied", satisfied?)
|
|
if (failed = @match_data.as?(Matchers::FailedMatchData))
|
|
failed_to_json(failed, json)
|
|
end
|
|
end
|
|
end
|
|
|
|
# Adds failure information to a JSON structure.
|
|
private def failed_to_json(failed : Matchers::FailedMatchData, json : JSON::Builder)
|
|
json.field("failure", failed.failure_message)
|
|
json.field("values") do
|
|
json.object do
|
|
failed.values.each do |pair|
|
|
json.field(pair.first, pair.last)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
# Stores part of an expectation.
|
|
# This covers the actual value (or block) being inspected and its location.
|
|
# This is the type returned by an `expect` block in the DSL.
|
|
# It is not intended to be used directly, but instead by chaining methods.
|
|
# Typically `#to` and `#not_to` are used.
|
|
struct Target(T)
|
|
# Creates the expectation target.
|
|
# The *expression* is the actual value being tested and its label.
|
|
# The *location* is the location of where this expectation was defined.
|
|
def initialize(@expression : Expression(T), @location : Location)
|
|
end
|
|
|
|
# Asserts that a method is called some point before the example completes.
|
|
@[AlwaysInline]
|
|
def to(stub : Stub, message = nil) : Nil
|
|
{% raise "The syntax `expect(...).to receive(...)` requires the expression passed to `expect` be stubbable (a mock or double)" unless T < ::Spectator::Stubbable || T < ::Spectator::StubbedType %}
|
|
|
|
to_eventually(stub, message)
|
|
end
|
|
|
|
# Asserts that some criteria defined by the matcher is satisfied.
|
|
# Allows a custom message to be used.
|
|
def to(matcher, message = nil) : Nil
|
|
match_data = matcher.match(@expression)
|
|
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
|
|
{% raise "The syntax `expect(...).to_not receive(...)` requires the expression passed to `expect` be stubbable (a mock or double)" unless T < ::Spectator::Stubbable || T < ::Spectator::StubbedType %}
|
|
|
|
to_never(stub, message)
|
|
end
|
|
|
|
# :ditto:
|
|
@[AlwaysInline]
|
|
def not_to(stub : Stub, message = nil) : Nil
|
|
to_not(stub, message)
|
|
end
|
|
|
|
# Asserts that some criteria defined by the matcher is not satisfied.
|
|
# This is effectively the opposite of `#to`.
|
|
# Allows a custom message to be used.
|
|
def to_not(matcher, message = nil) : Nil
|
|
match_data = matcher.negated_match(@expression)
|
|
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
|
|
to_not(matcher, message)
|
|
end
|
|
|
|
# Asserts that a method is called some point before the example completes.
|
|
def to_eventually(stub : Stub, message = nil) : Nil
|
|
{% raise "The syntax `expect(...).to_eventually receive(...)` requires the expression passed to `expect` be stubbable (a mock or double)" unless T < ::Spectator::Stubbable || T < ::Spectator::StubbedType %}
|
|
|
|
stubbable = @expression.value
|
|
unless stubbable._spectator_stub_for_method?(stub.method)
|
|
# Add stub without an argument constraint.
|
|
# Avoids confusing logic like this:
|
|
# ```
|
|
# expect(dbl).to receive(:foo).with(:bar)
|
|
# dbl.foo(:baz)
|
|
# ```
|
|
# Notice that `#foo` is called, but with different arguments.
|
|
# Normally this would raise an error, but that should be prevented.
|
|
unconstrained_stub = stub.with(Arguments.any)
|
|
stubbable._spectator_define_stub(unconstrained_stub)
|
|
end
|
|
|
|
# Apply the stub that is expected to be called.
|
|
stubbable._spectator_define_stub(stub)
|
|
|
|
# Check if the stub was invoked after the test completes.
|
|
matcher = Matchers::ReceiveMatcher.new(stub)
|
|
Harness.current.defer { to(matcher, message) }
|
|
|
|
# Prevent leaking stubs between tests.
|
|
Harness.current.cleanup { stubbable._spectator_remove_stub(stub) }
|
|
end
|
|
|
|
# Asserts that some criteria defined by the matcher is eventually satisfied.
|
|
# The expectation is checked after the example finishes and all hooks have run.
|
|
# Allows a custom message to be used.
|
|
def to_eventually(matcher, message = nil) : Nil
|
|
Harness.current.defer { to(matcher, message) }
|
|
end
|
|
|
|
# Asserts that a method is not called before the example completes.
|
|
def to_never(stub : Stub, message = nil) : Nil
|
|
{% raise "The syntax `expect(...).to_never receive(...)` requires the expression passed to `expect` be stubbable (a mock or double)" unless T < ::Spectator::Stubbable || T < ::Spectator::StubbedType %}
|
|
|
|
stubbable = @expression.value
|
|
unless stubbable._spectator_stub_for_method?(stub.method)
|
|
# Add stub without an argument constraint.
|
|
# Avoids confusing logic like this:
|
|
# ```
|
|
# expect(dbl).to receive(:foo).with(:bar)
|
|
# dbl.foo(:baz)
|
|
# ```
|
|
# Notice that `#foo` is called, but with different arguments.
|
|
# Normally this would raise an error, but that should be prevented.
|
|
unconstrained_stub = stub.with(Arguments.any)
|
|
stubbable._spectator_define_stub(unconstrained_stub)
|
|
end
|
|
|
|
# Apply the stub that could be called in case it is.
|
|
stubbable._spectator_define_stub(stub)
|
|
|
|
# Check if the stub was invoked after the test completes.
|
|
matcher = Matchers::ReceiveMatcher.new(stub)
|
|
Harness.current.defer { to_not(matcher, message) }
|
|
|
|
# Prevent leaking stubs between tests.
|
|
Harness.current.cleanup { stubbable._spectator_remove_stub(stub) }
|
|
end
|
|
|
|
# :ditto:
|
|
@[AlwaysInline]
|
|
def never_to(stub : Stub, message = nil) : Nil
|
|
to_never(stub, message)
|
|
end
|
|
|
|
# Asserts that some criteria defined by the matcher is never satisfied.
|
|
# The expectation is checked after the example finishes and all hooks have run.
|
|
# Allows a custom message to be used.
|
|
def to_never(matcher, message = nil) : Nil
|
|
Harness.current.defer { to_not(matcher, message) }
|
|
end
|
|
|
|
# :ditto:
|
|
@[AlwaysInline]
|
|
def never_to(matcher, message = nil) : Nil
|
|
to_never(matcher, message)
|
|
end
|
|
|
|
# Reports an expectation to the current harness.
|
|
private def report(match_data : Matchers::MatchData, message : String? | Proc(String) = nil)
|
|
expectation = Expectation.new(match_data, @location, message)
|
|
Harness.current.report(expectation)
|
|
end
|
|
end
|
|
end
|
|
end
|