Implement remaining assertion macros

Move "should" methods.
This commit is contained in:
Michael Miller 2021-01-10 11:09:28 -07:00
parent 096c31d7f5
commit 4ed8c4a573
No known key found for this signature in database
GPG key ID: F9A0C5C65B162436
2 changed files with 147 additions and 197 deletions

View file

@ -6,20 +6,45 @@ require "../source"
module Spectator::DSL module Spectator::DSL
# Methods and macros for asserting that conditions are met. # Methods and macros for asserting that conditions are met.
module Assertions module Assertions
# Immediately fail the current test.
# A reason can be specified with *message*.
def fail(message = "Example failed", *, _file = __FILE__, _line = __LINE__)
raise AssertionFailed.new(Source.new(_file, _line), message)
end
# Checks that the specified condition is true. # Checks that the specified condition is true.
# Raises `AssertionFailed` if *condition* is false. # Raises `AssertionFailed` if *condition* is false.
# The *message* is passed to the exception. # The *message* is passed to the exception.
#
# ```
# assert(value == 42, "That's not the answer to everything.")
# ```
def assert(condition, message, *, _file = __FILE__, _line = __LINE__) def assert(condition, message, *, _file = __FILE__, _line = __LINE__)
raise AssertionFailed.new(Source.new(_file, _line), message) unless condition fail(message, _file: _file, _line: _line) unless condition
end end
# Checks that the specified condition is true. # Checks that the specified condition is true.
# Raises `AssertionFailed` if *condition* is false. # Raises `AssertionFailed` if *condition* is false.
# The message of the exception is the *condition*. # The message of the exception is the *condition*.
#
# ```
# assert(value == 42)
# ```
macro assert(condition) macro assert(condition)
assert({{condition}}, {{condition.stringify}}, _file: {{condition.filename}}, _line: {{condition.line_number}}) assert({{condition}}, {{condition.stringify}}, _file: {{condition.filename}}, _line: {{condition.line_number}})
end end
# Starts an expectation.
# This should be followed up with `Assertion::Target#to` or `Assertion::Target#to_not`.
# The value passed in will be checked to see if it satisfies the conditions of the specified matcher.
#
# This macro should be used like so:
# ```
# expect(actual).to eq(expected)
# ```
#
# Where the actual value is returned by the system under test,
# and the expected value is what the actual value should be to satisfy the condition.
macro expect(actual) macro expect(actual)
%actual = begin %actual = begin
{{actual}} {{actual}}
@ -29,213 +54,120 @@ module Spectator::DSL
%source = ::Spectator::Source.new({{actual.filename}}, {{actual.line_number}}) %source = ::Spectator::Source.new({{actual.filename}}, {{actual.line_number}})
::Spectator::Assertion::Target.new(%expression, %source) ::Spectator::Assertion::Target.new(%expression, %source)
end end
end
# Starts an expectation. # Starts an expectation.
# This should be followed up with `Spectator::Expectations::ExpectationPartial#to` # This should be followed up with `Assertion::Target#to` or `Assertion::Target#not_to`.
# or `Spectator::Expectations::ExpectationPartial#to_not`. # The value passed in will be checked to see if it satisfies the conditions of the specified matcher.
# The value passed in will be checked #
# to see if it satisfies the conditions specified. # This macro should be used like so:
# # ```
# This method should be used like so: # expect { raise "foo" }.to raise_error
# ``` # ```
# expect(actual).to eq(expected) #
# ``` # The block of code is passed along for validation to the matchers.
# Where the actual value is returned by the system-under-test, #
# and the expected value is what the actual value should be to satisfy the condition. # The short, one argument syntax used for passing methods to blocks can be used.
macro expect(actual, _source_file = __FILE__, _source_line = __LINE__) # So instead of doing this:
%test_value = ::Spectator::TestValue.new({{actual}}, {{actual.stringify}}) # ```
%source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) # expect(subject.size).to eq(5)
::Spectator::Expectations::ExpectationPartial.new(%test_value, %source) # ```
end #
# The following syntax can be used instead:
# Starts an expectation on a block of code. # ```
# This should be followed up with `Spectator::Expectations::ExpectationPartial#to` # expect(&.size).to eq(5)
# or `Spectator::Expectations::ExpectationPartial#to_not`. # ```
# The block passed in, or its return value, will be checked #
# to see if it satisfies the conditions specified. # The method passed will always be evaluated on the subject.
# #
# This method should be used like so: # TECHNICAL NOTE:
# ``` # This macro uses an ugly hack to detect the short-hand syntax.
# expect { raise "foo" }.to raise_error #
# ```
# The block of code is passed along for validation to the matchers.
#
# The short, one argument syntax used for passing methods to blocks can be used.
# So instead of doing this:
# ```
# expect(subject.size).to eq(5)
# ```
# The following syntax can be used instead:
# ```
# expect(&.size).to eq(5)
# ```
# The method passed will always be evaluated on the subject.
macro expect(_source_file = __FILE__, _source_line = __LINE__, &block)
{% if block.is_a?(Nop) %}
{% raise "Argument or block must be provided to expect" %}
{% end %}
# Check if the short-hand method syntax is used.
# This is a hack, since macros don't get this as a "literal" or something similar.
# The Crystal compiler will translate: # The Crystal compiler will translate:
# ``` # ```
# &.foo # &.foo
# ``` # ```
# to: #
# effectively to:
# ``` # ```
# { |__arg0| __arg0.foo } # { |__arg0| __arg0.foo }
# ``` # ```
# The hack used here is to check if it looks like a compiler-generated block. macro expect(&block)
{% if block.args.size == 1 && block.args[0] =~ /^__arg\d+$/ && block.body.is_a?(Call) && block.body.id =~ /^__arg\d+\./ %} {% if block.args.size == 1 && block.args[0] =~ /^__arg\d+$/ && block.body.is_a?(Call) && block.body.id =~ /^__arg\d+\./ %}
# Extract the method name to make it clear to the user what is tested. {% method_name = block.body.id.split('.')[1..-1].join('.') %}
# The raw block can't be used because it's not clear to the user. %block = ::Spectator::Block.new({{"#" + method_name}}) do
{% method_name = block.body.id.split('.')[1..-1].join('.') %} subject.{{method_name.id}}
%proc = ->{ subject.{{method_name.id}} } end
%test_block = ::Spectator::TestBlock.create(%proc, {{"#" + method_name}}) {% elsif block.args.empty? %}
{% elsif block.args.empty? %} %block = ::Spectator::Block.new({{"`" + block.body.stringify + "`"}}) {{block}}
# In this case, it looks like the short-hand method syntax wasn't used. {% else %}
# Capture the block as a proc and pass along. {% raise "Unexpected block arguments in 'expect' call" %}
%proc = ->{{block}} {% end %}
%test_block = ::Spectator::TestBlock.create(%proc, {{"`" + block.body.stringify + "`"}})
{% else %}
{% raise "Unexpected block arguments in expect call" %}
{% end %}
%source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) %source = ::Spectator::Source.new({{block.filename}}, {{block.line_number}})
::Spectator::Expectations::ExpectationPartial.new(%test_block, %source) ::Spectator::Assertion::Target.new(%block, %source)
end end
# Starts an expectation. # Short-hand for expecting something of the subject.
# This should be followed up with `Spectator::Expectations::ExpectationPartial#to` #
# or `Spectator::Expectations::ExpectationPartial#to_not`. # These two are functionally equivalent:
# The value passed in will be checked # ```
# to see if it satisfies the conditions specified. # expect(subject).to eq("foo")
# # is_expected.to eq("foo")
# This method is identical to `#expect`, # ```
# but is grammatically correct for the one-liner syntax. macro is_expected
# It can be used like so: expect(subject)
# ``` end
# it expects(actual).to eq(expected)
# ```
# Where the actual value is returned by the system-under-test,
# and the expected value is what the actual value should be to satisfy the condition.
macro expects(actual)
expect({{actual}})
end
# Starts an expectation on a block of code. # Short-hand form of `#is_expected` that can be used for one-liner syntax.
# This should be followed up with `Spectator::Expectations::ExpectationPartial#to` #
# or `Spectator::Expectations::ExpectationPartial#to_not`. # For instance:
# The block passed in, or its return value, will be checked # ```
# to see if it satisfies the conditions specified. # it "is 42" do
# # expect(subject).to eq(42)
# This method is identical to `#expect`, # end
# but is grammatically correct for the one-liner syntax. # ```
# It can be used like so: #
# ``` # Can be shortened to:
# it expects { 5 / 0 }.to raise_error # ```
# ``` # it { is(42) }
# The block of code is passed along for validation to the matchers. # ```
# #
# The short, one argument syntax used for passing methods to blocks can be used. # These three are functionally equivalent:
# So instead of doing this: # ```
# ``` # expect(subject).to eq("foo")
# it expects(subject.size).to eq(5) # is_expected.to eq("foo")
# ``` # is("foo")
# The following syntax can be used instead: # ```
# ``` #
# it expects(&.size).to eq(5) # See also: `#is_not`
# ``` macro is(expected)
# The method passed will always be evaluated on the subject. expect(subject).to(eq({{expected}}))
macro expects(&block) end
expect {{block}}
end
# Short-hand for expecting something of the subject. # Short-hand, negated form of `#is_expected` that can be used for one-liner syntax.
# These two are functionally equivalent: #
# ``` # For instance:
# expect(subject).to eq("foo") # ```
# is_expected.to eq("foo") # it "is not 42" do
# ``` # expect(subject).to_not eq(42)
macro is_expected # end
expect(subject) # ```
end #
# Can be shortened to:
# Short-hand form of `#is_expected` that can be used for one-liner syntax. # ```
# For instance: # it { is_not(42) }
# ``` # ```
# it "is 42" do #
# expect(subject).to eq(42) # These three are functionally equivalent:
# end # ```
# ``` # expect(subject).not_to eq("foo")
# Can be shortened to: # is_expected.not_to eq("foo")
# ``` # is_not("foo")
# it is(42) # ```
# ``` #
# # See also: `#is`
# These three are functionally equivalent: macro is_not(expected)
# ``` expect(subject).not_to(eq({{expected}}))
# expect(subject).to eq("foo") end
# is_expected.to eq("foo")
# is("foo")
# ```
#
# See also: `#is_not`
macro is(expected)
is_expected.to eq({{expected}})
end
# Short-hand, negated form of `#is_expected` that can be used for one-liner syntax.
# For instance:
# ```
# it "is not 42" do
# expect(subject).to_not eq(42)
# end
# ```
# Can be shortened to:
# ```
# it is_not(42)
# ```
#
# These three are functionally equivalent:
# ```
# expect(subject).to_not eq("foo")
# is_expected.to_not eq("foo")
# is_not("foo")
# ```
#
# See also: `#is`
macro is_not(expected)
is_expected.to_not eq({{expected}})
end
macro should(matcher)
is_expected.to({{matcher}})
end
macro should_not(matcher)
is_expected.to_not({{matcher}})
end
macro should_eventually(matcher)
is_expected.to_eventually({{matcher}})
end
macro should_never(matcher)
is_expected.to_never({{matcher}})
end
# Immediately fail the current test.
# A reason can be passed,
# which is reported in the output.
def fail(reason : String)
raise ExampleFailed.new(reason)
end
# :ditto:
@[AlwaysInline]
def fail
fail("Example failed")
end end
end end

View file

@ -64,3 +64,21 @@ struct Proc(*T, R)
::Spectator::Expectations::BlockExpectationPartial.new(actual, source).to_not(matcher) ::Spectator::Expectations::BlockExpectationPartial.new(actual, source).to_not(matcher)
end end
end end
module Spectator::DSL::Assertions
macro should(matcher)
expect(subject).to({{matcher}})
end
macro should_not(matcher)
expect(subject).to_not({{matcher}})
end
macro should_eventually(matcher)
expect(subject).to_eventually({{matcher}})
end
macro should_never(matcher)
expect(subject).to_never({{matcher}})
end
end