Adjust evaluation order of change matcher expressions

Handles reference types better and matches RSpec more closely.
This commit is contained in:
Michael Miller 2022-05-14 23:30:15 -06:00
parent 739629ef9d
commit 0704fd2a48
No known key found for this signature in database
GPG key ID: AC78B32D30CE34A2
5 changed files with 73 additions and 66 deletions

View file

@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
### Changed ### Changed
- Forward example procsy `to_s` to underlying example. [#70](https://gitlab.com/arctic-fox/spectator/-/issues/70) - Forward example procsy `to_s` to underlying example. [#70](https://gitlab.com/arctic-fox/spectator/-/issues/70)
- Adjust evaluation order of `change` matcher expressions.
## [0.10.5] - 2022-01-27 ## [0.10.5] - 2022-01-27
### Fixed ### Fixed

View file

@ -34,16 +34,14 @@ Spectator.describe "Explicit Subject" do
subject { @@element_list.pop } subject { @@element_list.pop }
skip "is memoized across calls (i.e. the block is invoked once)", it "is memoized across calls (i.e. the block is invoked once)" do
reason: "RSpec calls the \"actual\" block after the \"change block\"." do
expect do expect do
3.times { subject } 3.times { subject }
end.to change { @@element_list }.from([1, 2, 3]).to([1, 2]) end.to change { @@element_list }.from([1, 2, 3]).to([1, 2])
expect(subject).to eq(3) expect(subject).to eq(3)
end end
skip "is not memoized across examples", it "is not memoized across examples" do
reason: "RSpec calls the \"actual\" block after the \"change block\"." do
expect { subject }.to change { @@element_list }.from([1, 2]).to([1]) expect { subject }.to change { @@element_list }.from([1, 2]).to([1])
expect(subject).to eq(2) expect(subject).to eq(2)
end end

View file

@ -27,26 +27,32 @@ module Spectator::Matchers
# Actually performs the test against the expression. # Actually performs the test against the expression.
def match(actual : Expression(T)) : MatchData forall T def match(actual : Expression(T)) : MatchData forall T
before, after = change(actual) before = expression.value
before_inspect = before.inspect
if expected_before == before if expected_before == before
if before == after actual.value # Trigger block that might cause a change.
FailedMatchData.new(match_data_description(actual), "#{actual.label} did not change #{expression.label}", after = expression.value
before: before.inspect, after_inspect = after.inspect
after: after.inspect
) if expected_after == after
elsif expected_after == after
SuccessfulMatchData.new(match_data_description(actual)) SuccessfulMatchData.new(match_data_description(actual))
elsif before == after
FailedMatchData.new(match_data_description(actual), "#{actual.label} did not change #{expression.label}",
before: before_inspect,
after: after_inspect
)
else else
FailedMatchData.new(match_data_description(actual), "#{actual.label} did not change #{expression.label} to #{expected_after.inspect}", FailedMatchData.new(match_data_description(actual), "#{actual.label} did not change #{expression.label} to #{expected_after.inspect}",
before: before.inspect, before: before_inspect,
after: after.inspect, after: after_inspect,
expected: expected_after.inspect expected: expected_after.inspect
) )
end end
else else
FailedMatchData.new(match_data_description(actual), "#{expression.label} was not initially #{expected_before.inspect}", FailedMatchData.new(match_data_description(actual), "#{expression.label} was not initially #{expected_before.inspect}",
expected: expected_before.inspect, expected: expected_before.inspect,
actual: before.inspect, actual: before_inspect,
) )
end end
end end
@ -54,12 +60,18 @@ module Spectator::Matchers
# Performs the test against the expression, but inverted. # Performs the test against the expression, but inverted.
# A successful match with `#match` should normally fail for this method, and vice-versa. # A successful match with `#match` should normally fail for this method, and vice-versa.
def negated_match(actual : Expression(T)) : MatchData forall T def negated_match(actual : Expression(T)) : MatchData forall T
before, after = change(actual) before = expression.value
before_inspect = before.inspect
if expected_before == before if expected_before == before
actual.value # Trigger block that might cause a change.
after = expression.value
after_inspect = after.inspect
if expected_after == after if expected_after == after
FailedMatchData.new(match_data_description(actual), "#{actual.label} changed #{expression.label} from #{expected_before.inspect} to #{expected_after.inspect}", FailedMatchData.new(match_data_description(actual), "#{actual.label} changed #{expression.label} from #{expected_before.inspect} to #{expected_after.inspect}",
before: before.inspect, before: before_inspect,
after: after.inspect after: after_inspect
) )
else else
SuccessfulMatchData.new(match_data_description(actual)) SuccessfulMatchData.new(match_data_description(actual))
@ -67,18 +79,9 @@ module Spectator::Matchers
else else
FailedMatchData.new(match_data_description(actual), "#{expression.label} was not initially #{expected_before.inspect}", FailedMatchData.new(match_data_description(actual), "#{expression.label} was not initially #{expected_before.inspect}",
expected: expected_before.inspect, expected: expected_before.inspect,
actual: before.inspect, actual: before_inspect,
) )
end end
end end
# Performs the change and reports the before and after values.
private def change(actual)
before = expression.value # Retrieve the expression's initial value.
actual.value # Invoke action that might change the expression's value.
after = expression.value # Retrieve the expression's value again.
{before, after}
end
end end
end end

View file

@ -25,16 +25,24 @@ module Spectator::Matchers
# Actually performs the test against the expression. # Actually performs the test against the expression.
def match(actual : Expression(T)) : MatchData forall T def match(actual : Expression(T)) : MatchData forall T
before, after = change(actual) before = expression.value
before_inspect = before.inspect
if expected != before if expected != before
FailedMatchData.new(match_data_description(actual), "#{expression.label} was not initially #{expected}", return FailedMatchData.new(match_data_description(actual), "#{expression.label} was not initially #{expected}",
expected: expected.inspect, expected: expected.inspect,
actual: before.inspect, actual: before_inspect,
) )
elsif before == after end
actual.value # Trigger block that might change the expression.
after = expression.value
after_inspect = after.inspect
if expected == after
FailedMatchData.new(match_data_description(actual), "#{actual.label} did not change #{expression.label} from #{expected}", FailedMatchData.new(match_data_description(actual), "#{actual.label} did not change #{expression.label} from #{expected}",
before: before.inspect, before: before_inspect,
after: after.inspect, after: after_inspect,
expected: "Not #{expected.inspect}" expected: "Not #{expected.inspect}"
) )
else else
@ -45,18 +53,26 @@ module Spectator::Matchers
# Performs the test against the expression, but inverted. # Performs the test against the expression, but inverted.
# A successful match with `#match` should normally fail for this method, and vice-versa. # A successful match with `#match` should normally fail for this method, and vice-versa.
def negated_match(actual : Expression(T)) : MatchData forall T def negated_match(actual : Expression(T)) : MatchData forall T
before, after = change(actual) before = expression.value
before_inspect = before.inspect
if expected != before if expected != before
FailedMatchData.new(match_data_description(actual), "#{expression.label} was not initially #{expected}", return FailedMatchData.new(match_data_description(actual), "#{expression.label} was not initially #{expected}",
expected: expected.inspect, expected: expected.inspect,
actual: before.inspect actual: before_inspect
) )
elsif before == after end
actual.value # Trigger block that might change the expression.
after = expression.value
after_inspect = after.inspect
if expected == after
SuccessfulMatchData.new(match_data_description(actual)) SuccessfulMatchData.new(match_data_description(actual))
else else
FailedMatchData.new(match_data_description(actual), "#{actual.label} changed #{expression.label} from #{expected}", FailedMatchData.new(match_data_description(actual), "#{actual.label} changed #{expression.label} from #{expected}",
before: before.inspect, before: before_inspect,
after: after.inspect, after: after_inspect,
expected: expected.inspect expected: expected.inspect
) )
end end
@ -71,14 +87,5 @@ module Spectator::Matchers
def by(amount) def by(amount)
ChangeExactMatcher.new(@expression, @expected, @expected + value) ChangeExactMatcher.new(@expression, @expected, @expected + value)
end end
# Performs the change and reports the before and after values.
private def change(actual)
before = expression.value # Retrieve the expression's initial value.
actual.value # Invoke action that might change the expression's value.
after = expression.value # Retrieve the expression's value again.
{before, after}
end
end end
end end

View file

@ -25,19 +25,26 @@ module Spectator::Matchers
# Actually performs the test against the expression. # Actually performs the test against the expression.
def match(actual : Expression(T)) : MatchData forall T def match(actual : Expression(T)) : MatchData forall T
before, after = change(actual) before = expression.value
if before == after before_inspect = before.inspect
FailedMatchData.new(match_data_description(actual), "#{actual.label} did not change #{expression.label}",
before: before.inspect, if expected == before
after: after.inspect, return FailedMatchData.new(match_data_description(actual), "#{expression.label} was already #{expected}",
expected: expected.inspect before: before_inspect,
expected: "Not #{expected.inspect}"
) )
elsif expected == after end
actual.value # Trigger block that could change the expression.
after = expression.value
after_inspect = after.inspect
if expected == after
SuccessfulMatchData.new(match_data_description(actual)) SuccessfulMatchData.new(match_data_description(actual))
else else
FailedMatchData.new(match_data_description(actual), "#{actual.label} did not change #{expression.label} to #{expected}", FailedMatchData.new(match_data_description(actual), "#{actual.label} did not change #{expression.label} to #{expected}",
before: before.inspect, before: before_inspect,
after: after.inspect, after: after_inspect,
expected: expected.inspect expected: expected.inspect
) )
end end
@ -65,14 +72,5 @@ module Spectator::Matchers
def by(amount) def by(amount)
ChangeExactMatcher.new(@expression, @expected - amount, @expected) ChangeExactMatcher.new(@expression, @expected - amount, @expected)
end end
# Performs the change and reports the before and after values.
private def change(actual)
before = expression.value # Retrieve the expression's initial value.
actual.value # Invoke action that might change the expression's value.
after = expression.value # Retrieve the expression's value again.
{before, after}
end
end end
end end