Merge branch 'master' into mocks-and-doubles

This commit is contained in:
Michael Miller 2020-02-23 12:00:37 -07:00
commit 86729c6745
70 changed files with 1799 additions and 209 deletions

View file

@ -15,6 +15,9 @@ before_script:
spec:
script:
- crystal spec --error-on-warnings
style:
script:
- bin/ameba
- crystal tool format --check

5
.guardian.yml Normal file
View file

@ -0,0 +1,5 @@
files: ./**/*.cr
run: time crystal spec --error-trace
---
files: ./shard.yml
run: shards

View file

@ -1,16 +1,16 @@
name: spectator
version: 0.9.1
version: 0.9.9
description: |
A feature-rich spec testing framework for Crystal with similarities to RSpec.
authors:
- Michael Miller <icy.arctic.fox@gmail.com>
crystal: 0.31.0
crystal: 0.33.0
license: MIT
development_dependencies:
ameba:
github: crystal-ameba/ameba
version: ~> 0.10
version: ~> 0.11.0

View file

@ -0,0 +1,32 @@
require "../spec_helper"
Spectator.describe "eq matcher" do
it "is true for equal values" do
expect(42).to eq(42)
end
it "is false for inequal values" do
expect(42).to_not eq(24)
end
it "is true for identical references" do
string = "foobar"
expect(string).to eq(string)
end
it "is false for different references" do
string1 = "foo"
string2 = "bar"
expect(string1).to_not eq(string2)
end
double(:fake) do
stub instance.==(other) { true }
end
it "uses the == operator" do
dbl = double(:fake)
expect(42).to eq(dbl)
expect(dbl).to have_received(:==).with(42).once
end
end

View file

@ -0,0 +1,112 @@
require "../../spec_helper"
# Examples taken from:
# https://relishapp.com/rspec/rspec-core/v/3-8/docs/hooks/before-and-after-hooks
# and modified to fit Spectator and Crystal.
Spectator.describe "`before` and `after` hooks" do
context "Define `before_each` block" do
class Thing
def widgets
@widgets ||= [] of Symbol # Must specify array element type.
end
end
describe Thing do
before_each do
@thing = Thing.new
end
describe "initialize in before_each" do
it "has 0 widgets" do
widgets = @thing.as(Thing).widgets # Must cast since compile type is Thing?
expect(widgets.size).to eq(0) # Use size instead of count.
end
it "can accept new widgets" do
widgets = @thing.as(Thing).widgets # Must cast since compile type is Thing?
widgets << :foo
end
it "does not share state across examples" do
widgets = @thing.as(Thing).widgets # Must cast since compile type is Thing?
expect(widgets.size).to eq(0) # Use size instead of count.
end
end
end
end
context "Define `before_all` block in example group" do
class Thing
def widgets
@widgets ||= [] of Symbol # Must specify array element type.
end
end
describe Thing do
# Moved before_all into the same example group.
# Unlike Ruby, inherited class variables don't share the same value.
# See: https://crystal-lang.org/reference/syntax_and_semantics/class_variables.html
describe "initialized in before_all" do
@@thing : Thing?
before_all do
@@thing = Thing.new # Must use class variables.
end
it "has 0 widgets" do
widgets = @@thing.as(Thing).widgets # Must cast since compile type is Thing?
expect(widgets.size).to eq(0) # Use size instead of count.
end
it "can accept new widgets" do
widgets = @@thing.as(Thing).widgets # Must cast since compile type is Thing?
widgets << :foo
end
it "shares state across examples" do
widgets = @@thing.as(Thing).widgets # Must cast since compile type is Thing?
expect(widgets.size).to eq(1) # Use size instead of count.
end
end
end
end
context "Failure in `before_each` block" do
# TODO
end
context "Failure in `after_each` block" do
# TODO
end
context "Define `before` and `after` blocks in configuration" do
# TODO
end
context "`before`/`after` blocks are run in order" do
# Examples changed from using puts to appending to an array.
describe "before and after callbacks" do
@@order = [] of Symbol
before_all do
@@order << :before_all
end
before_each do
@@order << :before_each
end
after_each do
@@order << :after_each
end
after_all do
@@order << :after_all
end
it "gets run in order" do
expect(@@order).to_eventually eq(%i[before_all before_each after_each after_all])
end
end
end
end

View file

@ -0,0 +1,140 @@
require "../../spec_helper"
# Examples taken from:
# https://relishapp.com/rspec/rspec-core/v/3-8/docs/subject/explicit-subject
# and modified to fit Spectator and Crystal.
Spectator.describe "Explicit Subject" do
context "A `subject` can be defined and used in the top level group scope" do
describe Array(Int32) do # TODO: Multiple arguments to describe/context.
subject { [1, 2, 3] }
it "has the prescribed elements" do
expect(subject).to eq([1, 2, 3])
end
end
end
context "The `subject` define in an outer group is available to inner groups" do
describe Array(Int32) do
subject { [1, 2, 3] }
describe "has some elements" do
it "which are the prescribed elements" do
expect(subject).to eq([1, 2, 3])
end
end
end
end
context "The `subject` is memoized within an example but not across examples" do
describe Array(Int32) do
# Changed to class variable to get around compiler error/crash.
# Unhandled exception: Negative argument (ArgumentError)
@@element_list = [1, 2, 3]
subject { @@element_list.pop }
# TODO: RSpec calls the "actual" block after the "change block".
xit "is memoized across calls (i.e. the block is invoked once)" do
expect do
3.times { subject }
end.to change { @@element_list }.from([1, 2, 3]).to([1, 2])
expect(subject).to eq(3)
end
# TODO: RSpec calls the "actual" block after the "change block".
xit "is not memoized across examples" do
expect { subject }.to change { @@element_list }.from([1, 2]).to([1])
expect(subject).to eq(2)
end
end
end
context "The `subject` is available in `before` blocks" do
describe Array(Int32) do # TODO: Multiple arguments to describe/context.
subject { [] of Int32 }
before_each { subject.push(1, 2, 3) }
it "has the prescribed elements" do
expect(subject).to eq([1, 2, 3])
end
end
end
context "Helper methods can be invoked from a `subject` definition block" do
describe Array(Int32) do # TODO: Multiple arguments to describe/context.
def prepared_array
[1, 2, 3]
end
subject { prepared_array }
it "has the prescribed elements" do
expect(subject).to eq([1, 2, 3])
end
end
end
context "Use the `subject!` bang method to call the definition block before the example" do
describe "eager loading with subject!" do
subject! { element_list.push(99) }
let(:element_list) { [1, 2, 3] }
it "calls the definition block before the example" do
element_list.push(5)
expect(element_list).to eq([1, 2, 3, 99, 5])
end
end
end
context "Use `subject(:name)` to define a memoized helper method" do
# Globals not supported, using class variable instead.
@@count = 0
describe "named subject" do
subject(:global_count) { @@count += 1 }
it "is memoized across calls (i.e. the block is invoked once)" do
expect do
2.times { global_count }
end.not_to change { global_count }.from(1)
end
it "is not cached across examples" do
expect(global_count).to eq(2)
end
it "is still available using the subject method" do
expect(subject).to eq(3)
end
it "works with the one-liner syntax" do
is_expected.to eq(4)
end
it "the subject and named helpers return the same object" do
expect(global_count).to be(subject)
end
it "is set to the block return value (i.e. the global $count)" do
expect(global_count).to be(@@count)
end
end
end
context "Use `subject!(:name)` to define a helper method called before the example" do
describe "eager loading using a named subject!" do
subject!(:updated_list) { element_list.push(99) }
let(:element_list) { [1, 2, 3] }
it "calls the definition block before the example" do
element_list.push(5)
expect(element_list).to eq([1, 2, 3, 99, 5])
expect(updated_list).to be(element_list)
end
end
end
end

View file

@ -0,0 +1,29 @@
require "../../spec_helper"
Spectator.describe "Arbitrary helper methods" do
context "Use a method define in the same group" do
describe "an example" do
def help
:available
end
it "has access to methods define in its group" do
expect(help).to be(:available)
end
end
end
context "Use a method defined in a parent group" do
describe "an example" do
def help
:available
end
describe "in a nested group" do
it "has access to methods defined in its parent group" do
expect(help).to be(:available)
end
end
end
end
end

View file

@ -0,0 +1,43 @@
require "../../spec_helper"
# Examples taken from:
# https://relishapp.com/rspec/rspec-core/v/3-8/docs/subject/implicitly-defined-subject
# and modified to fit Spectator and Crystal.
Spectator.describe "Implicitly defined subject" do
context "`subject` exposed in top-level group" do
describe Array(String) do
it "should be empty when first created" do
expect(subject).to be_empty
end
end
end
context "`subject` in a nested group" do
describe Array(String) do
describe "when first created" do
it "should be empty" do
expect(subject).to be_empty
end
end
end
end
context "`subject` in a nested group with a different class (innermost wins)" do
class ArrayWithOneElement < Array(String)
def initialize(*_args)
super
unshift "first element"
end
end
describe Array(String) do
describe ArrayWithOneElement do
context "referenced as subject" do
it "contains one element" do
expect(subject).to contain("first element")
end
end
end
end
end
end

View file

@ -0,0 +1,45 @@
require "../../spec_helper"
# Examples taken from:
# https://relishapp.com/rspec/rspec-core/v/3-8/docs/helper-methods/let-and-let
# and modified to fit Spectator and Crystal.
Spectator.describe "Let and let!" do
context "Use `let` to define memoized helper method" do
# Globals aren't supported, use class variables instead.
@@count = 0
describe "let" do
let(:count) { @@count += 1 }
it "memoizes thte value" do
expect(count).to eq(1)
expect(count).to eq(1)
end
it "is not cached across examples" do
expect(count).to eq(2)
end
end
end
context "Use `let!` to define a memoized helper method that is called in a `before` hook" do
# Globals aren't supported, use class variables instead.
@@count = 0
describe "let!" do
# Use class variable here.
@@invocation_order = [] of Symbol
let!(:count) do
@@invocation_order << :let!
@@count += 1
end
it "calls the helper method in a before hook" do
@@invocation_order << :example
expect(@@invocation_order).to eq([:let!, :example])
expect(count).to eq(1)
end
end
end
end

View file

@ -0,0 +1,31 @@
require "../../spec_helper"
# Examples taken from:
# https://relishapp.com/rspec/rspec-core/v/3-8/docs/subject/one-liner-syntax
# and modified to fit Spectator and Crystal.
Spectator.describe "One-liner syntax" do
context "Implicit subject" do
describe Array(Int32) do
# Rather than:
# it "should be empty" do
# subject.should be_empty
# end
it { should be_empty }
# or
it { is_expected.to be_empty }
end
end
context "Explicit subject" do
describe Array(Int32) do
describe "with 3 items" do
subject { [1, 2, 3] }
it { should_not be_empty }
# or
it { is_expected.not_to be_empty }
end
end
end
end

View file

@ -0,0 +1,37 @@
require "../../spec_helper"
# Examples taken from:
# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/all-matcher
# and modified to fit Spectator and Crystal.
Spectator.describe "`all` matcher" do
context "array usage" do
describe [1, 3, 5] do
it { is_expected.to all(be_odd) }
it { is_expected.to all(be_an(Int32)) } # Changed to Int32 to satisfy compiler.
it { is_expected.to all(be < 10) }
# deliberate failures
it_fails { is_expected.to all(be_even) }
it_fails { is_expected.to all(be_a(String)) }
it_fails { is_expected.to all(be > 2) }
end
end
context "compound matcher usage" do
# Changed `include` to `contain` to match our own.
# `include` is a keyword and can't be used as a method name in Crystal.
# TODO: Add support for compound matchers.
describe ["anything", "everything", "something"] do
xit { is_expected.to all(be_a(String)) } # .and contain("thing") ) }
xit { is_expected.to all(be_a(String)) } # .and end_with("g") ) }
xit { is_expected.to all(start_with("s")) } # .or contain("y") ) }
# deliberate failures
# TODO: Add support for compound matchers.
xit { is_expected.to all(contain("foo")) } # .and contain("bar") ) }
xit { is_expected.to all(be_a(String)) } # .and start_with("a") ) }
xit { is_expected.to all(start_with("a")) } # .or contain("z") ) }
end
end
end

View file

@ -0,0 +1,17 @@
require "../../spec_helper"
Spectator.describe "`be_between` matcher" do
context "basic usage" do
describe 7 do
it { is_expected.to be_between(1, 10) }
it { is_expected.to be_between(0.2, 27.1) }
it { is_expected.not_to be_between(1.5, 4) }
it { is_expected.not_to be_between(8, 9) }
# boundaries check
it { is_expected.to be_between(0, 7) }
it { is_expected.to be_between(7, 10) }
it { is_expected.not_to (be_between(0, 7).exclusive) }
end
end
end

View file

@ -0,0 +1,66 @@
require "../../spec_helper"
# Examples taken from:
# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/be-matchers
# and modified to fit Spectator and Crystal.
Spectator.describe "`be` matchers" do
context "be_truthy matcher" do
specify { expect(true).to be_truthy }
specify { expect(7).to be_truthy }
specify { expect("foo").to be_truthy }
specify { expect(nil).not_to be_truthy }
specify { expect(false).not_to be_truthy }
# deliberate failures
specify_fails { expect(true).not_to be_truthy }
specify_fails { expect(7).not_to be_truthy }
specify_fails { expect("foo").not_to be_truthy }
specify_fails { expect(nil).to be_truthy }
specify_fails { expect(false).to be_truthy }
end
context "be_falsey matcher" do
specify { expect(nil).to be_falsey }
specify { expect(false).to be_falsey }
specify { expect(true).not_to be_falsey }
specify { expect(7).not_to be_falsey }
specify { expect("foo").not_to be_falsey }
# deliberate failures
specify_fails { expect(nil).not_to be_falsey }
specify_fails { expect(false).not_to be_falsey }
specify_fails { expect(true).to be_falsey }
specify_fails { expect(7).to be_falsey }
specify_fails { expect("foo").to be_falsey }
end
context "be_nil matcher" do
specify { expect(nil).to be_nil }
specify { expect(false).not_to be_nil }
specify { expect(true).not_to be_nil }
specify { expect(7).not_to be_nil }
specify { expect("foo").not_to be_nil }
# deliberate failures
specify_fails { expect(nil).not_to be_nil }
specify_fails { expect(false).to be_nil }
specify_fails { expect(true).to be_nil }
specify_fails { expect(7).to be_nil }
specify_fails { expect("foo").to be_nil }
end
context "be matcher" do
specify { expect(true).to be }
specify { expect(7).to be }
specify { expect("foo").to be }
specify { expect(nil).not_to be }
specify { expect(false).not_to be }
# deliberate failures
specify_fails { expect(true).not_to be }
specify_fails { expect(7).not_to be }
specify_fails { expect("foo").not_to be }
specify_fails { expect(nil).to be }
specify_fails { expect(false).to be }
end
end

View file

@ -0,0 +1,24 @@
require "../../spec_helper"
# Examples taken from:
# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/be-within-matcher
# and modified to fit Spectator and Crystal.
Spectator.describe "`be_within` matcher" do
context "basic usage" do
describe 27.5 do
it { is_expected.to be_within(0.5).of(27.9) }
it { is_expected.to be_within(0.5).of(28.0) }
it { is_expected.to be_within(0.5).of(27.1) }
it { is_expected.to be_within(0.5).of(27.0) }
it { is_expected.not_to be_within(0.5).of(28.1) }
it { is_expected.not_to be_within(0.5).of(26.9) }
# deliberate failures
it_fails { is_expected.not_to be_within(0.5).of(28) }
it_fails { is_expected.not_to be_within(0.5).of(27) }
it_fails { is_expected.to be_within(0.5).of(28.1) }
it_fails { is_expected.to be_within(0.5).of(26.9) }
end
end
end

View file

@ -0,0 +1,47 @@
require "../../spec_helper"
# Examples taken from:
# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/change-matcher
# and modified to fit Spectator and Crystal.
Spectator.describe "`change` matcher" do
# Modified this example type to work in Crystal.
module Counter
extend self
@@count = 0
def increment
@@count += 1
end
def count
@@count
end
end
context "expect change" do
describe "Counter#increment" do # TODO: Allow multiple arguments to context/describe.
it "should increment the count" do
expect { Counter.increment }.to change { Counter.count }.from(0).to(1)
end
# deliberate failure
it_fails "should increment the count by 2" do
expect { Counter.increment }.to change { Counter.count }.by(2)
end
end
end
context "expect no change" do
describe "Counter#increment" do # TODO: Allow multiple arguments to context/describe.
# deliberate failures
it_fails "should not increment the count by 1 (using not_to)" do
expect { Counter.increment }.not_to change { Counter.count }
end
it_fails "should not increment the count by 1 (using to_not)" do
expect { Counter.increment }.to_not change { Counter.count }
end
end
end
end

View file

@ -0,0 +1,44 @@
require "../../spec_helper"
# Examples taken from:
# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/comparison-matchers
# and modified to fit Spectator and Crystal.
Spectator.describe "Comparison matchers" do
context "numeric operator matchers" do
describe 18 do
it { is_expected.to be < 20 }
it { is_expected.to be > 15 }
it { is_expected.to be <= 19 }
it { is_expected.to be >= 17 }
# deliberate failures
it_fails { is_expected.to be < 15 }
it_fails { is_expected.to be > 20 }
it_fails { is_expected.to be <= 17 }
it_fails { is_expected.to be >= 19 }
# it { is_expected.to be < 'a' } # Removed because Crystal doesn't support Int32#<(Char)
end
describe 'a' do
it { is_expected.to be < 'b' }
# deliberate failures
# it { is_expected.to be < 18 } # Removed because Crystal doesn't support Char#<(Int32)
end
end
context "string operator matchers" do
describe "Strawberry" do
it { is_expected.to be < "Tomato" }
it { is_expected.to be > "Apple" }
it { is_expected.to be <= "Turnip" }
it { is_expected.to be >= "Banana" }
# deliberate failures
it_fails { is_expected.to be < "Cranberry" }
it_fails { is_expected.to be > "Zuchini" }
it_fails { is_expected.to be <= "Potato" }
it_fails { is_expected.to be >= "Tomato" }
end
end
end

View file

@ -0,0 +1,30 @@
require "../../spec_helper"
# Examples taken from:
# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/contain-exactly-matcher
# and modified to fit Spectator and Crystal.
Spectator.describe "`contain_exactly` matcher" do
context "Array is expected to contain every value" do
describe [1, 2, 3] do
it { is_expected.to contain_exactly(1, 2, 3) }
it { is_expected.to contain_exactly(1, 3, 2) }
it { is_expected.to contain_exactly(2, 1, 3) }
it { is_expected.to contain_exactly(2, 3, 1) }
it { is_expected.to contain_exactly(3, 1, 2) }
it { is_expected.to contain_exactly(3, 2, 1) }
# deliberate failures
it_fails { is_expected.to contain_exactly(1, 2, 1) }
end
end
context "Array is not expected to contain every value" do
describe [1, 2, 3] do
it { is_expected.to_not contain_exactly(1, 2, 3, 4) }
it { is_expected.to_not contain_exactly(1, 2) }
# deliberate failures
it_fails { is_expected.to_not contain_exactly(1, 3, 2) }
end
end
end

View file

@ -0,0 +1,96 @@
require "../../spec_helper"
# In Ruby, this is the `include` matcher.
# However, `include` is a reserved keyword in Crystal.
# So instead, it is `contain` in Spectator.
Spectator.describe "`contain` matcher" do
context "array usage" do
describe [1, 3, 7] do
it { is_expected.to contain(1) }
it { is_expected.to contain(3) }
it { is_expected.to contain(7) }
it { is_expected.to contain(1, 7) }
it { is_expected.to contain(1, 3, 7) }
# Utility matcher method `a_kind_of` is not supported.
# it { is_expected.to contain(a_kind_of(Int)) }
# TODO: Compound matchers aren't supported.
# it { is_expected.to contain(be_odd.and be < 10) }
# TODO: Fix behavior and cleanup output.
# This syntax is allowed, but produces a wrong result and bad output.
xit { is_expected.to contain(be_odd) }
xit { is_expected.not_to contain(be_even) }
it { is_expected.not_to contain(17) }
it { is_expected.not_to contain(43, 100) }
# deliberate failures
it_fails { is_expected.to contain(4) }
it_fails { is_expected.to contain(be_even) }
it_fails { is_expected.not_to contain(1) }
it_fails { is_expected.not_to contain(3) }
it_fails { is_expected.not_to contain(7) }
it_fails { is_expected.not_to contain(1, 3, 7) }
# both of these should fail since it contains 1 but not 9
it_fails { is_expected.to contain(1, 9) }
it_fails { is_expected.not_to contain(1, 9) }
end
end
context "string usage" do
describe "a string" do
it { is_expected.to contain("str") }
it { is_expected.to contain("a", "str", "ng") }
it { is_expected.not_to contain("foo") }
it { is_expected.not_to contain("foo", "bar") }
# deliberate failures
it_fails { is_expected.to contain("foo") }
it_fails { is_expected.not_to contain("str") }
it_fails { is_expected.to contain("str", "foo") }
it_fails { is_expected.not_to contain("str", "foo") }
end
end
context "hash usage" do
# A hash can't be described inline here for some reason.
# So it is placed in the subject instead.
describe ":a => 7, :b => 5" do
subject { {:a => 7, :b => 5} }
# Hash syntax is changed here from `:a => 7` to `a: 7`.
# it { is_expected.to contain(:a) }
# it { is_expected.to contain(:b, :a) }
# TODO: This hash-like syntax isn't supported.
# it { is_expected.to contain(a: 7) }
# it { is_expected.to contain(b: 5, a: 7) }
# it { is_expected.not_to contain(:c) }
# it { is_expected.not_to contain(:c, :d) }
# it { is_expected.not_to contain(d: 2) }
# it { is_expected.not_to contain(a: 5) }
# it { is_expected.not_to contain(b: 7, a: 5) }
# deliberate failures
# it { is_expected.not_to contain(:a) }
# it { is_expected.not_to contain(:b, :a) }
# it { is_expected.not_to contain(a: 7) }
# it { is_expected.not_to contain(a: 7, b: 5) }
# it { is_expected.to contain(:c) }
# it { is_expected.to contain(:c, :d) }
# it { is_expected.to contain(d: 2) }
# it { is_expected.to contain(a: 5) }
# it { is_expected.to contain(a: 5, b: 7) }
# Mixed cases--the hash contains one but not the other.
# All 4 of these cases should fail.
# it { is_expected.to contain(:a, :d) }
# it { is_expected.not_to contain(:a, :d) }
# it { is_expected.to contain(a: 7, d: 3) }
# it { is_expected.not_to contain(a: 7, d: 3) }
end
end
end

View file

@ -0,0 +1,29 @@
require "../../spec_helper"
# Examples taken from:
# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/cover-matcher
# and modified to fit Spectator and Crystal.
Spectator.describe "`cover` matcher" do
context "range usage" do
describe (1..10) do
it { is_expected.to cover(4) }
it { is_expected.to cover(6) }
it { is_expected.to cover(8) }
it { is_expected.to cover(4, 6) }
it { is_expected.to cover(4, 6, 8) }
it { is_expected.not_to cover(11) }
it { is_expected.not_to cover(11, 12) }
# deliberate failures
it_fails { is_expected.to cover(11) }
it_fails { is_expected.not_to cover(4) }
it_fails { is_expected.not_to cover(6) }
it_fails { is_expected.not_to cover(8) }
it_fails { is_expected.not_to cover(4, 6, 8) }
# both of these should fail since it covers 5 but not 11
it_fails { is_expected.to cover(5, 11) }
it_fails { is_expected.not_to cover(5, 11) }
end
end
end

View file

@ -0,0 +1,31 @@
require "../../spec_helper"
# Examples taken from:
# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/end-with-matcher
# and modified to fit Spectator and Crystal.
Spectator.describe "`end_with` matcher" do
context "string usage" do
describe "this string" do
it { is_expected.to end_with "string" }
it { is_expected.not_to end_with "stringy" }
# deliberate failures
it_fails { is_expected.not_to end_with "string" }
it_fails { is_expected.to end_with "stringy" }
end
end
context "array usage" do
describe [0, 1, 2, 3, 4] do
it { is_expected.to end_with 4 }
# TODO: Add support for multiple items at the end of an array.
# it { is_expected.to end_with 3, 4 }
it { is_expected.not_to end_with 3 }
# it { is_expected.not_to end_with 0, 1, 2, 3, 4, 5 }
# deliberate failures
it_fails { is_expected.not_to end_with 4 }
it_fails { is_expected.to end_with 3 }
end
end
end

View file

@ -0,0 +1,64 @@
require "../../spec_helper"
# Examples taken from:
# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/equality-matchers
# and modified to fit Spectator and Crystal.
Spectator.describe "Equality matchers" do
context "compare using eq (==)" do
describe "a string" do
it "is equal to another string of the same value" do
expect("this string").to eq("this string")
end
it "is not equal to another string of a different value" do
expect("this string").not_to eq("a different string")
end
end
describe "an integer" do
it "is equal to a float for the same value" do
expect(5).to eq(5.0)
end
end
end
context "compare using ==" do
describe "a string" do
it "is equal to another string of the same value" do
expect("this string").to be == "this string"
end
it "is not equal to another string of a different value" do
expect("this string").not_to be == "a different string"
end
end
describe "an integer" do
it "is equal to a float of the same value" do
expect(5).to be == 5.0
end
end
end
# There are no #eql? and #equal? methods in Crystal, so these tests are skipped.
context "compare using be (same?)" do
it "is equal to itself" do
string = "this string"
expect(string).to be(string)
end
it "is not equal to another reference of the same value" do
# Strings with identical contents are the same reference in Crystal.
# This test is modified to reflect that.
# expect("this string").not_to be("this string")
box1 = Box.new("this string")
box2 = Box.new("this string")
expect(box1).not_to be(box2)
end
it "is not equal to another string of a different value" do
expect("this string").not_to be("a different string")
end
end
end

View file

@ -0,0 +1,36 @@
require "../../spec_helper"
# Examples taken from:
# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/have-attributes-matcher
# and modified to fit Spectator and Crystal.
Spectator.describe "`have_attributes` matcher" do
context "basic usage" do
# Use `record` instead of `Struct.new`.
record Person, name : String, age : Int32
describe Person.new("Jim", 32) do
# Changed some syntax for Ruby hashes to Crystal named tuples.
# Spectator doesn't support helper matchers like `a_string_starting_with` and `a_value <`.
# But maybe in the future it will.
it { is_expected.to have_attributes(name: "Jim") }
# it { is_expected.to have_attributes(name: a_string_starting_with("J") ) }
it { is_expected.to have_attributes(age: 32) }
# it { is_expected.to have_attributes(age: (a_value > 30) ) }
it { is_expected.to have_attributes(name: "Jim", age: 32) }
# it { is_expected.to have_attributes(name: a_string_starting_with("J"), age: (a_value > 30) ) }
it { is_expected.not_to have_attributes(name: "Bob") }
it { is_expected.not_to have_attributes(age: 10) }
# it { is_expected.not_to have_attributes(age: (a_value < 30) ) }
# deliberate failures
it_fails { is_expected.to have_attributes(name: "Bob") }
it_fails { is_expected.to have_attributes(name: 10) }
# fails if any of the attributes don't match
it_fails { is_expected.to have_attributes(name: "Bob", age: 32) }
it_fails { is_expected.to have_attributes(name: "Jim", age: 10) }
it_fails { is_expected.to have_attributes(name: "Bob", age: 10) }
end
end
end

View file

@ -0,0 +1,28 @@
require "../../spec_helper"
# Examples taken from:
# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/match-matcher
# and modified to fit Spectator and Crystal.
Spectator.describe "`match` matcher" do
context "string usage" do
describe "a string" do
it { is_expected.to match(/str/) }
it { is_expected.not_to match(/foo/) }
# deliberate failures
it_fails { is_expected.not_to match(/str/) }
it_fails { is_expected.to match(/foo/) }
end
end
context "regular expression usage" do
describe /foo/ do
it { is_expected.to match("food") }
it { is_expected.not_to match("drinks") }
# deliberate failures
it_fails { is_expected.not_to match("food") }
it_fails { is_expected.to match("drinks") }
end
end
end

View file

@ -0,0 +1,81 @@
require "../../spec_helper"
# Examples taken from:
# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/predicate-matchers
# and modified to fit Spectator and Crystal.
Spectator.describe "Predicate matchers" do
context "should be_zero (based on Int#zero?)" do
describe 0 do
it { is_expected.to be_zero }
end
describe 7 do
# deliberate failure
it_fails { is_expected.to be_zero }
end
end
context "should_not be_empty (based on Array#empty?)" do
describe [1, 2, 3] do
it { is_expected.not_to be_empty }
end
describe [] of Int32 do
# deliberate failure
it_fails { is_expected.not_to be_empty }
end
end
context "should have_key (based on Hash#has_key?)" do
describe Hash do
subject { {:foo => 7} }
it { is_expected.to have_key(:foo) }
# deliberate failure
it_fails { is_expected.to have_key(:bar) }
end
end
context "should_not have_all_string_keys (based on custom #has_all_string_keys? method)" do
class ::Hash(K, V)
def has_all_string_keys?
keys.all? { |k| String === k }
end
end
describe Hash do
context "with symbol keys" do
subject { {:foo => 7, :bar => 5} }
it { is_expected.not_to have_all_string_keys }
end
context "with string keys" do
subject { {"foo" => 7, "bar" => 5} }
# deliberate failure
it_fails { is_expected.not_to have_all_string_keys }
end
end
end
context "matcher arguments are passed on to the predicate method" do
struct ::Int
def multiple_of?(x)
(self % x).zero?
end
end
describe 12 do
it { is_expected.to be_multiple_of(3) }
it { is_expected.not_to be_multiple_of(7) }
# deliberate failures
it_fails { is_expected.not_to be_multiple_of(4) }
it_fails { is_expected.to be_multiple_of(5) }
end
end
# The examples using private methods cause a compilation error in Crystal, and can't be used here.
end

View file

@ -0,0 +1,94 @@
require "../../spec_helper"
# Examples taken from:
# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/raise-error-matcher
# and modified to fit Spectator and Crystal.
Spectator.describe "`raise_error` matcher" do
context "expect any error" do
# This example originally calls a non-existent method.
# That isn't allowed in Crystal.
# The example has been changed to just raise a runtime error.
describe "dividing by zero" do
it "raises" do
expect { 42 // 0 }.to raise_error
end
end
end
context "expect specific error" do
# Again, can't even compile if a method doesn't exist.
# So using a different exception here.
describe "dividing by zero" do
it "raises" do
expect { 42 // 0 }.to raise_error(DivisionByZeroError)
end
end
end
# The following examples are changed slightly.
# `raise Type.new(message)` is the syntax in Crystal,
# whereas it is `raise Type, message` in Ruby.
# Additionally, `StandardError` doesn't exist in Crystal,
# so `Exception` is used instead.
context "match message with a string" do
describe "matching error message with string" do
it "matches the error message" do
expect { raise Exception.new("this message exactly") }
.to raise_error("this message exactly")
end
end
end
context "match message with a regexp" do
describe "matching error message with regex" do
it "matches the error message" do
expect { raise Exception.new("my message") }
.to raise_error(/my mess/)
end
end
end
context "matching message with `with_message`" do
describe "matching error message with regex" do
it "matches the error message" do
expect { raise Exception.new("my message") }
.to raise_error.with_message(/my mess/)
end
end
end
context "match class + message with string" do
describe "matching error message with string" do
it "matches the error message" do
expect { raise Exception.new("this message exactly") }
.to raise_error(Exception, "this message exactly")
end
end
end
context "match class + message with regexp" do
describe "matching error message with regex" do
it "matches the error message" do
expect { raise Exception.new("my message") }
.to raise_error(Exception, /my mess/)
end
end
end
# TODO: Support passing a block to `raise_error` matcher.
# context "set expectations on error object passed to block" do
# it "raises DivisionByZeroError" do
# expect { 42 // 0 }.to raise_error do |error|
# expect(error).to be_a(DivisionByZeroError)
# end
# end
# end
context "expect no error at all" do
describe "#to_s" do
it "does not raise" do
expect { 42.to_s }.not_to raise_error
end
end
end
end

View file

@ -0,0 +1,28 @@
require "../../spec_helper"
# Examples taken from:
# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/respond-to-matcher
# and modified to fit Spectator and Crystal.
Spectator.describe "`respond_to` matcher" do
context "basic usage" do
describe "a string" do
it { is_expected.to respond_to(:size) } # It's size in Crystal, not length.
it { is_expected.to respond_to(:hash, :class, :to_s) }
it { is_expected.not_to respond_to(:to_model) }
it { is_expected.not_to respond_to(:compact, :flatten) }
# deliberate failures
it_fails { is_expected.to respond_to(:to_model) }
it_fails { is_expected.to respond_to(:compact, :flatten) }
it_fails { is_expected.not_to respond_to(:size) }
it_fails { is_expected.not_to respond_to(:hash, :class, :to_s) }
# mixed examples--String responds to :length but not :flatten
# both specs should fail
it_fails { is_expected.to respond_to(:size, :flatten) }
it_fails { is_expected.not_to respond_to(:size, :flatten) }
end
end
# Spectator doesn't support argument matching with respond_to.
end

View file

@ -0,0 +1,31 @@
require "../../spec_helper"
# Examples taken from:
# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/start-with-matcher
# and modified to fit Spectator and Crystal.
Spectator.describe "`start_with` matcher" do
context "with a string" do
describe "this string" do
it { is_expected.to start_with "this" }
it { is_expected.not_to start_with "that" }
# deliberate failures
it_fails { is_expected.not_to start_with "this" }
it_fails { is_expected.to start_with "that" }
end
end
context "with an array" do
describe [0, 1, 2, 3, 4] do
it { is_expected.to start_with 0 }
# TODO: Add support for multiple items at the beginning of an array.
# it { is_expected.to start_with(0, 1) }
it { is_expected.not_to start_with(2) }
# it { is_expected.not_to start_with(0, 1, 2, 3, 4, 5) }
# deliberate failures
it_fails { is_expected.not_to start_with 0 }
it_fails { is_expected.to start_with 3 }
end
end
end

View file

@ -0,0 +1,100 @@
require "../../spec_helper"
# Examples taken from:
# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/type-matchers
# and modified to fit Spectator and Crystal.
Spectator.describe "Type matchers" do
context "be_(a_)kind_of matcher" do
# The docs use Float as an example.
# This doesn't work with the Crystal compiler,
# so a custom hierarchy is used instead.
# "Error: can't use Number as generic type argument yet, use a more specific type"
module MyModule; end
class Base; end
class Derived < Base
include MyModule
end
describe Derived do
# the actual class
it { is_expected.to be_kind_of(Derived) }
it { is_expected.to be_a_kind_of(Derived) }
it { is_expected.to be_a(Derived) }
# the superclass
it { is_expected.to be_kind_of(Base) }
it { is_expected.to be_a_kind_of(Base) }
it { is_expected.to be_an(Base) }
# an included module
it { is_expected.to be_kind_of(MyModule) }
it { is_expected.to be_a_kind_of(MyModule) }
it { is_expected.to be_a(MyModule) }
# negative passing case
it { is_expected.not_to be_kind_of(String) }
it { is_expected.not_to be_a_kind_of(String) }
it { is_expected.not_to be_a(String) }
# deliberate failures
it_fails { is_expected.not_to be_kind_of(Derived) }
it_fails { is_expected.not_to be_a_kind_of(Derived) }
it_fails { is_expected.not_to be_a(Derived) }
it_fails { is_expected.not_to be_kind_of(Base) }
it_fails { is_expected.not_to be_a_kind_of(Base) }
it_fails { is_expected.not_to be_an(Base) }
it_fails { is_expected.not_to be_kind_of(MyModule) }
it_fails { is_expected.not_to be_a_kind_of(MyModule) }
it_fails { is_expected.not_to be_a(MyModule) }
it_fails { is_expected.to be_kind_of(String) }
it_fails { is_expected.to be_a_kind_of(String) }
it_fails { is_expected.to be_a(String) }
end
context "be_(an_)instance_of matcher" do
# The docs use Float as an example.
# This doesn't work with the Crystal compiler,
# so a custom hierarchy is used instead.
# "Error: can't use Number as generic type argument yet, use a more specific type"
module MyModule; end
class Base; end
class Derived < Base
include MyModule
end
describe Derived do
# the actual class
it { is_expected.to be_instance_of(Derived) }
it { is_expected.to be_an_instance_of(Derived) }
# the superclass
it { is_expected.not_to be_instance_of(Base) }
it { is_expected.not_to be_an_instance_of(Base) }
# an included module
it { is_expected.not_to be_instance_of(MyModule) }
it { is_expected.not_to be_an_instance_of(MyModule) }
# another class with no relation to the subject's hierarchy
it { is_expected.not_to be_instance_of(String) }
it { is_expected.not_to be_an_instance_of(String) }
# deliberate failures
it_fails { is_expected.not_to be_instance_of(Derived) }
it_fails { is_expected.not_to be_an_instance_of(Derived) }
it_fails { is_expected.to be_instance_of(Base) }
it_fails { is_expected.to be_an_instance_of(Base) }
it_fails { is_expected.to be_instance_of(MyModule) }
it_fails { is_expected.to be_an_instance_of(MyModule) }
it_fails { is_expected.to be_instance_of(String) }
it_fails { is_expected.to be_an_instance_of(String) }
end
end
end
end

View file

@ -1,4 +1,13 @@
require "../src/spectator"
# Prevent Spectator from trying to run tests on its own.
Spectator.autorun = false
macro it_fails(description = nil, &block)
it {{description}} do
expect do
{{block.body}}
end.to raise_error(Spectator::ExampleFailed)
end
end
macro specify_fails(description = nil, &block)
it_fails {{description}} {{block}}
end

23
spec/subject_spec.cr Normal file
View file

@ -0,0 +1,23 @@
require "./spec_helper"
class Base; end
module SomeModule; end
Spectator.describe "Subject" do
subject { Base.new }
context "nested" do
it "inherits the parent explicit subject" do
expect(subject).to be_a(Base)
end
end
context "module" do
describe SomeModule do
it "sets the implicit subject to the module" do
expect(subject).to be(SomeModule)
end
end
end
end

View file

@ -6,7 +6,7 @@ module Spectator
extend self
# Current version of the Spectator library.
VERSION = "0.9.1"
VERSION = "0.9.9"
# Top-level describe method.
# All specs in a file must be wrapped in this call.

View file

@ -184,6 +184,22 @@ module Spectator
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.

View file

@ -3,7 +3,7 @@ require "../spec_builder"
module Spectator
module DSL
macro it(description, _source_file = __FILE__, _source_line = __LINE__, &block)
macro it(description = nil, _source_file = __FILE__, _source_line = __LINE__, &block)
{% if block.is_a?(Nop) %}
{% if description.is_a?(Call) %}
def %run
@ -20,17 +20,17 @@ module Spectator
%source = ::Spectator::Source.new({{_source_file}}, {{_source_line}})
::Spectator::SpecBuilder.add_example(
{{description.is_a?(StringLiteral) ? description : description.stringify}},
{{description.is_a?(StringLiteral) || description.is_a?(NilLiteral) ? description : description.stringify}},
%source,
{{@type.name}}
) { |test| test.as({{@type.name}}).%run }
end
macro specify(description, &block)
macro specify(description = nil, &block)
it({{description}}) {{block}}
end
macro pending(description, _source_file = __FILE__, _source_line = __LINE__, &block)
macro pending(description = nil, _source_file = __FILE__, _source_line = __LINE__, &block)
{% if block.is_a?(Nop) %}
{% if description.is_a?(Call) %}
def %run
@ -47,17 +47,17 @@ module Spectator
%source = ::Spectator::Source.new({{_source_file}}, {{_source_line}})
::Spectator::SpecBuilder.add_pending_example(
{{description.is_a?(StringLiteral) ? description : description.stringify}},
{{description.is_a?(StringLiteral) || description.is_a?(NilLiteral) ? description : description.stringify}},
%source,
{{@type.name}}
) { |test| test.as({{@type.name}}).%run }
end
macro skip(description, &block)
macro skip(description = nil, &block)
pending({{description}}) {{block}}
end
macro xit(description, &block)
macro xit(description = nil, &block)
pending({{description}}) {{block}}
end
end

View file

@ -19,13 +19,21 @@ module Spectator
%source = ::Spectator::Source.new({{_source_file}}, {{_source_line}})
::Spectator::SpecBuilder.start_group({{description}}, %source)
{% if what.is_a?(Path) || what.is_a?(Generic) %}
{% if (what.is_a?(Path) || what.is_a?(Generic)) && (described_type = what.resolve?) %}
macro described_class
{{what}}
{{described_type.name}}
end
def subject(*args)
described_class.new(*args)
subject do
{% if described_type < Reference || described_type < Value %}
described_class.new
{% else %}
described_class
{% end %}
end
{% else %}
def _spectator_implicit_subject(*args)
{{what}}
end
{% end %}

View file

@ -94,6 +94,60 @@ module Spectator
be_a({{expected}})
end
# Indicates that some value should be of a specified type.
# The `Object#is_a?` method is used for this check.
# A type name or type union should be used for *expected*.
# This method is identical to `#be_a`,
# and exists just to improve grammar.
#
# Examples:
# ```
# expect(123).to be_kind_of(Int)
# ```
macro be_kind_of(expected)
be_a({{expected}})
end
# Indicates that some value should be of a specified type.
# The `Object#is_a?` method is used for this check.
# A type name or type union should be used for *expected*.
# This method is identical to `#be_a`,
# and exists just to improve grammar.
#
# Examples:
# ```
# expect(123).to be_a_kind_of(Int)
# ```
macro be_a_kind_of(expected)
be_a({{expected}})
end
# Indicates that some value should be of a specified type.
# The value's runtime class is checked.
# A type name or type union should be used for *expected*.
#
# Examples:
# ```
# expect(123).to be_instance_of(Int32)
# ```
macro be_instance_of(expected)
::Spectator::Matchers::InstanceMatcher({{expected}}).new
end
# Indicates that some value should be of a specified type.
# The value's runtime class is checked.
# A type name or type union should be used for *expected*.
# This method is identical to `#be_an_instance_of`,
# and exists just to improve grammar.
#
# Examples:
# ```
# expect(123).to be_an_instance_of(Int32)
# ```
macro be_an_instance_of(expected)
be_instance_of({{expected}})
end
# Indicates that some value should respond to a method call.
# One or more method names can be provided.
#
@ -288,10 +342,10 @@ module Spectator
# expect(100).to be_between(97, 101).exclusive # 97, 98, 99, or 100 (not 101)
# ```
macro be_between(min, max)
%range = Range.new({{min}}, {{max}}))
%range = Range.new({{min}}, {{max}})
%label = [{{min.stringify}}, {{max.stringify}}].join(" to ")
%test_value = ::Spectator::TestValue.new(%range, %label)
:Spectator::Matchers::RangeMatcher.new(%test_value)
::Spectator::Matchers::RangeMatcher.new(%test_value)
end
# Indicates that some value should be within a delta of an expected value.
@ -400,6 +454,27 @@ module Spectator
::Spectator::Matchers::ContainMatcher.new(%test_value)
end
# Indicates that some range (or collection) should contain another value.
# This is typically used on a `Range` (although any `Enumerable` works).
# The `includes?` method is used.
#
# Examples:
# ```
# expect(1..10).to contain(5)
# expect((1..)).to contain(100)
# expect(..100).to contain(50)
# ```
#
# Additionally, multiple arguments can be specified.
# ```
# expect(1..10).to contain(2, 3)
# expect(..100).to contain(0, 50)
# ```
macro cover(*expected)
%test_value = ::Spectator::TestValue.new({{expected}}, {{expected.splat.stringify}})
::Spectator::Matchers::ContainMatcher.new(%test_value)
end
# Indicates that some value or set should contain another value.
# This is similar to `#contain`, but uses a different method for matching.
# Typically a `String` or `Array` (any `Enumerable` works) is checked against.
@ -466,22 +541,23 @@ module Spectator
have_value({{expected}})
end
# Indicates that some set should contain some values in exact order.
# Indicates that some set should contain some values in any order.
#
# Example:
# ```
# expect([1, 2, 3]).to contain_exactly(1, 2, 3)
# expect([1, 2, 3]).to contain_exactly(3, 2, 1)
# ```
macro contain_exactly(*expected)
%test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}})
::Spectator::Matchers::ArrayMatcher.new(%test_value)
end
# Indicates that some set should contain the same values in exact order as another set.
# Indicates that some set should contain the same values in any order as another set.
# This is the same as `#contain_exactly`, but takes an array as an argument.
#
# Example:
# ```
# expect([1, 2, 3]).to match_array([1, 2, 3])
# expect([1, 2, 3]).to match_array([3, 2, 1])
# ```
macro match_array(expected)
%test_value = ::Spectator::TestValue.new({{expected}}, {{expected.stringify}})
@ -716,8 +792,8 @@ module Spectator
{% raise "Undefined local variable or method '#{call}'" %}
{% end %}
descriptor = { {{method_name}}: Tuple.new({{call.args.splat}}) }
label = String::Builder.new({{method_name.stringify}})
descriptor = { {{method_name}}: ::Tuple.new({{call.args.splat}}) }
label = ::String::Builder.new({{method_name.stringify}})
{% unless call.args.empty? %}
label << '('
{% for arg, index in call.args %}

View file

@ -19,11 +19,18 @@ module Spectator
end
macro let!(name, &block)
# TODO: Doesn't work with late-defined values (let).
@%value = {{yield}}
@%wrapper : ::Spectator::ValueWrapper?
def %wrapper
{{block.body}}
end
before_each do
@%wrapper = ::Spectator::TypedValueWrapper.new(%wrapper)
end
def {{name.id}}
@%value
@%wrapper.as(::Spectator::TypedValueWrapper(typeof(%wrapper))).value
end
end

View file

@ -6,6 +6,9 @@ module Spectator
# Concrete types must implement the `#run_impl` method.
abstract class Example < ExampleComponent
@finished = false
@description : String? = nil
protected setter description
# Indicates whether the example has already been run.
def finished? : Bool
@ -24,10 +27,12 @@ module Spectator
end
def description : String | Symbol
@test_wrapper.description
@description || @test_wrapper.description
end
def symbolic? : Bool
return false unless @test_wrapper.description?
description = @test_wrapper.description
description.starts_with?('#') || description.starts_with?('.')
end

View file

@ -44,6 +44,10 @@ module Spectator::Expectations
values?.not_nil!
end
def description
@match_data.description
end
# Creates the JSON representation of the expectation.
def to_json(json : ::JSON::Builder)
json.object do

View file

@ -46,6 +46,7 @@ module Spectator
# Reports the outcome of an expectation.
# An exception will be raised when a failing result is given.
def report_expectation(expectation : Expectations::Expectation) : Nil
@example.description = expectation.description unless @example.test_wrapper.description?
@reporter.report(expectation)
end

View file

@ -26,7 +26,7 @@ module Spectator::Matchers
match_data = matcher.match(element)
break match_data unless match_data.matched?
end
found || SuccessfulMatchData.new
found || SuccessfulMatchData.new(description)
end
# Negated matching for this matcher is not supported.

View file

@ -5,7 +5,7 @@ require "./unordered_array_matcher"
module Spectator::Matchers
# Matcher for checking that the contents of one array (or similar type)
# has the exact same contents as another and in the same order.
# has the exact same contents as another but may be in any order.
struct ArrayMatcher(ExpectedType) < Matcher
# Expected value and label.
private getter expected
@ -25,15 +25,19 @@ module Spectator::Matchers
def match(actual : TestExpression(T)) : MatchData forall T
actual_elements = actual.value.to_a
expected_elements = expected.value.to_a
index = compare_arrays(expected_elements, actual_elements)
missing, extra = compare_arrays(expected_elements, actual_elements)
case index
when Int # Content differs.
failed_content_mismatch(expected_elements, actual_elements, index, actual.label)
when true # Contents are identical.
SuccessfulMatchData.new
else # Size differs.
failed_size_mismatch(expected_elements, actual_elements, actual.label)
if missing.empty? && extra.empty?
# Contents are identical.
SuccessfulMatchData.new(description)
else
# Content differs.
FailedMatchData.new(description, "#{actual.label} does not contain exactly #{expected.label}",
expected: expected_elements.inspect,
actual: actual_elements.inspect,
missing: missing.empty? ? "None" : missing.inspect,
extra: extra.empty? ? "None" : extra.inspect
)
end
end
@ -42,14 +46,17 @@ module Spectator::Matchers
def negated_match(actual : TestExpression(T)) : MatchData forall T
actual_elements = actual.value.to_a
expected_elements = expected.value.to_a
missing, extra = compare_arrays(expected_elements, actual_elements)
case compare_arrays(expected_elements, actual_elements)
when Int # Contents differ.
SuccessfulMatchData.new
when true # Contents are identical.
failed_content_identical(expected_elements, actual_elements, actual.label)
else # Size differs.
SuccessfulMatchData.new
if missing.empty? && extra.empty?
# Contents are identical.
FailedMatchData.new(description, "#{actual.label} contains exactly #{expected.label}",
expected: "Not #{expected_elements.inspect}",
actual: actual_elements.inspect
)
else
# Content differs.
SuccessfulMatchData.new(description)
end
end
@ -65,49 +72,41 @@ module Spectator::Matchers
UnorderedArrayMatcher.new(expected)
end
# Compares two arrays to determine whether they contain the same elements, and in the same order.
# If the arrays are the same, then `true` is returned.
# If they are different, `false` or an integer is returned.
# `false` is returned when the sizes of the arrays don't match.
# An integer is returned, that is the index of the mismatched elements in the arrays.
# Compares two arrays to determine whether they contain the same elements, but in any order.
# A tuple of two arrays is returned.
# The first array is the missing elements (present in expected, missing in actual).
# The second array array is the extra elements (not present in expected, present in actual).
private def compare_arrays(expected_elements, actual_elements)
if expected_elements.size == actual_elements.size
index = 0
expected_elements.zip(actual_elements) do |expected_element, actual_element|
return index unless expected_element == actual_element
index += 1
# Produce hashes where the array elements are the keys, and the values are the number of occurances.
expected_hash = expected_elements.group_by(&.itself).map { |k, v| {k, v.size} }.to_h
actual_hash = actual_elements.group_by(&.itself).map { |k, v| {k, v.size} }.to_h
{
hash_count_difference(expected_hash, actual_hash),
hash_count_difference(actual_hash, expected_hash),
}
end
# Expects two hashes, with values as counts for keys.
# Produces an array of differences with elements repeated if needed.
private def hash_count_difference(first, second)
# Subtract the number of occurances from the other array.
# A duplicate hash is used here because the original can't be modified,
# since it there's a two-way comparison.
#
# Then reject elements that have zero (or less) occurances.
# Lastly, expand to the correct number of elements.
first.map do |element, count|
if second_count = second[element]?
{element, count - second_count}
else
{element, count}
end
true
else
false
end
end
# Produces match data for a failure when the array sizes differ.
private def failed_size_mismatch(expected_elements, actual_elements, actual_label)
FailedMatchData.new("#{actual_label} does not contain exactly #{expected.label} (size mismatch)",
expected: expected_elements.inspect,
actual: actual_elements.inspect,
"expected size": expected_elements.size.to_s,
"actual size": actual_elements.size.to_s
)
end
# Produces match data for a failure when the array content is mismatched.
private def failed_content_mismatch(expected_elements, actual_elements, index, actual_label)
FailedMatchData.new("#{actual_label} does not contain exactly #{expected.label} (element mismatch)",
expected: expected_elements[index].inspect,
actual: actual_elements[index].inspect,
index: index.to_s
)
end
# Produces match data for a failure when the arrays are identical, but they shouldn't be (negation).
private def failed_content_identical(expected_elements, actual_elements, actual_label)
FailedMatchData.new("#{actual_label} contains exactly #{expected.label}",
expected: "Not #{expected_elements.inspect}",
actual: actual_elements.inspect
)
end.reject do |(_, count)|
count <= 0
end.map do |(element, count)|
Array.new(count, element)
end.flatten
end
end
end

View file

@ -28,9 +28,9 @@ module Spectator::Matchers
def match(actual : TestExpression(T)) : MatchData forall T
snapshot = snapshot_values(actual.value)
if match?(snapshot)
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
else
FailedMatchData.new("#{actual.label} does not have attributes #{expected.label}", **values(snapshot))
FailedMatchData.new(description, "#{actual.label} does not have attributes #{expected.label}", **values(snapshot))
end
end
@ -39,9 +39,9 @@ module Spectator::Matchers
def negated_match(actual : TestExpression(T)) : MatchData forall T
snapshot = snapshot_values(actual.value)
if match?(snapshot)
FailedMatchData.new("#{actual.label} has attributes #{expected.label}", **negated_values(snapshot))
FailedMatchData.new(description, "#{actual.label} has attributes #{expected.label}", **negated_values(snapshot))
else
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
end
end

View file

@ -16,6 +16,12 @@ module Spectator::Matchers
expected.value === actual.value
end
# Overload that takes a regex so that the operands are flipped.
# This mimics RSpec's behavior.
private def match?(actual : TestExpression(Regex)) : Bool forall T
actual.value === expected.value
end
# Message displayed when the matcher isn't satisifed.
#
# This is only called when `#match?` returns false.

View file

@ -30,21 +30,21 @@ module Spectator::Matchers
before, after = change(actual)
if expected_before == before
if before == after
FailedMatchData.new("#{actual.label} did not change #{expression.label}",
FailedMatchData.new(description, "#{actual.label} did not change #{expression.label}",
before: before.inspect,
after: after.inspect
)
elsif expected_after == after
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
else
FailedMatchData.new("#{actual.label} did not change #{expression.label} to #{expected_after.inspect}",
FailedMatchData.new(description, "#{actual.label} did not change #{expression.label} to #{expected_after.inspect}",
before: before.inspect,
after: after.inspect,
expected: expected_after.inspect
)
end
else
FailedMatchData.new("#{expression.label} was not initially #{expected_before.inspect}",
FailedMatchData.new(description, "#{expression.label} was not initially #{expected_before.inspect}",
expected: expected_before.inspect,
actual: before.inspect,
)
@ -57,15 +57,15 @@ module Spectator::Matchers
before, after = change(actual)
if expected_before == before
if expected_after == after
FailedMatchData.new("#{actual.label} changed #{expression.label} from #{expected_before.inspect} to #{expected_after.inspect}",
FailedMatchData.new(description, "#{actual.label} changed #{expression.label} from #{expected_before.inspect} to #{expected_after.inspect}",
before: before.inspect,
after: after.inspect
)
else
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
end
else
FailedMatchData.new("#{expression.label} was not initially #{expected_before.inspect}",
FailedMatchData.new(description, "#{expression.label} was not initially #{expected_before.inspect}",
expected: expected_before.inspect,
actual: before.inspect,
)

View file

@ -27,18 +27,18 @@ module Spectator::Matchers
def match(actual : TestExpression(T)) : MatchData forall T
before, after = change(actual)
if expected != before
FailedMatchData.new("#{expression.label} was not initially #{expected}",
FailedMatchData.new(description, "#{expression.label} was not initially #{expected}",
expected: expected.inspect,
actual: before.inspect,
)
elsif before == after
FailedMatchData.new("#{actual.label} did not change #{expression.label} from #{expected}",
FailedMatchData.new(description, "#{actual.label} did not change #{expression.label} from #{expected}",
before: before.inspect,
after: after.inspect,
expected: "Not #{expected.inspect}"
)
else
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
end
end
@ -47,14 +47,14 @@ module Spectator::Matchers
def negated_match(actual : TestExpression(T)) : MatchData forall T
before, after = change(actual)
if expected != before
FailedMatchData.new("#{expression.label} was not initially #{expected}",
FailedMatchData.new(description, "#{expression.label} was not initially #{expected}",
expected: expected.inspect,
actual: before.inspect
)
elsif before == after
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
else
FailedMatchData.new("#{actual.label} changed #{expression.label} from #{expected}",
FailedMatchData.new(description, "#{actual.label} changed #{expression.label} from #{expected}",
before: before.inspect,
after: after.inspect,
expected: expected.inspect

View file

@ -25,12 +25,12 @@ module Spectator::Matchers
def match(actual : TestExpression(T)) : MatchData forall T
before, after = change(actual)
if before == after
FailedMatchData.new("#{actual.label} did not change #{expression.label}",
FailedMatchData.new(description, "#{actual.label} did not change #{expression.label}",
before: before.inspect,
after: after.inspect
)
else
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
end
end
@ -39,9 +39,9 @@ module Spectator::Matchers
def negated_match(actual : TestExpression(T)) : MatchData forall T
before, after = change(actual)
if before == after
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
else
FailedMatchData.new("#{actual.label} changed #{expression.label}",
FailedMatchData.new(description, "#{actual.label} changed #{expression.label}",
before: before.inspect,
after: after.inspect
)

View file

@ -25,14 +25,14 @@ module Spectator::Matchers
def match(actual : TestExpression(T)) : MatchData forall T
before, after = change(actual)
if before == after
FailedMatchData.new("#{actual.label} did not change #{expression.label}",
FailedMatchData.new(description, "#{actual.label} did not change #{expression.label}",
before: before.inspect,
after: after.inspect
)
elsif @evaluator.call(before, after)
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
else
FailedMatchData.new("#{actual.label} did not change #{expression.label} #{@relativity}",
FailedMatchData.new(description, "#{actual.label} did not change #{expression.label} #{@relativity}",
before: before.inspect,
after: after.inspect
)

View file

@ -27,15 +27,15 @@ module Spectator::Matchers
def match(actual : TestExpression(T)) : MatchData forall T
before, after = change(actual)
if before == after
FailedMatchData.new("#{actual.label} did not change #{expression.label}",
FailedMatchData.new(description, "#{actual.label} did not change #{expression.label}",
before: before.inspect,
after: after.inspect,
expected: expected.inspect
)
elsif expected == after
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
else
FailedMatchData.new("#{actual.label} did not change #{expression.label} to #{expected}",
FailedMatchData.new(description, "#{actual.label} did not change #{expression.label} to #{expected}",
before: before.inspect,
after: after.inspect,
expected: expected.inspect

View file

@ -18,6 +18,13 @@ module Spectator::Matchers
end
end
# If the expectation is negated, then this method is called instead of `#match?`.
private def does_not_match?(actual : TestExpression(T)) : Bool forall T
!expected.value.any? do |item|
actual.value.includes?(item)
end
end
# Message displayed when the matcher isn't satisifed.
#
# This is only called when `#match?` returns false.
@ -25,7 +32,7 @@ module Spectator::Matchers
# The message should typically only contain the test expression labels.
# Actual values should be returned by `#values`.
private def failure_message(actual) : String
"#{actual.label} does not match #{expected.label}"
"#{actual.label} does not contain #{expected.label}"
end
# Message displayed when the matcher isn't satisifed and is negated.

View file

@ -23,7 +23,8 @@ module Spectator::Matchers
# Actually performs the test against the expression.
def match(actual : TestExpression(T)) : MatchData forall T
if (value = actual.value).responds_to?(:ends_with?)
value = actual.value
if value.is_a?(String) || value.responds_to?(:ends_with?)
match_ends_with(value, actual.label)
else
match_last(value, actual.label)
@ -33,10 +34,11 @@ module Spectator::Matchers
# Performs the test against the expression, but inverted.
# A successful match with `#match` should normally fail for this method, and vice-versa.
def negated_match(actual : TestExpression(T)) : MatchData forall T
if actual.value.responds_to?(:ends_with?)
negated_match_ends_with(actual)
value = actual.value
if value.is_a?(String) || value.responds_to?(:ends_with?)
negated_match_ends_with(value, actual.label)
else
negated_match_last(actual)
negated_match_last(value, actual.label)
end
end
@ -44,9 +46,9 @@ module Spectator::Matchers
# This method expects (and uses) the `#ends_with?` method on the value.
private def match_ends_with(actual_value, actual_label)
if actual_value.ends_with?(expected.value)
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
else
FailedMatchData.new("#{actual_label} does not end with #{expected.label} (using #ends_with?)",
FailedMatchData.new(description, "#{actual_label} does not end with #{expected.label} (using #ends_with?)",
expected: expected.value.inspect,
actual: actual_value.inspect
)
@ -60,9 +62,9 @@ module Spectator::Matchers
last = list.last
if expected.value === last
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
else
FailedMatchData.new("#{actual_label} does not end with #{expected.label} (using expected === last)",
FailedMatchData.new(description, "#{actual_label} does not end with #{expected.label} (using expected === last)",
expected: expected.value.inspect,
actual: last.inspect,
list: list.inspect
@ -72,31 +74,31 @@ module Spectator::Matchers
# Checks whether the actual value does not end with the expected value.
# This method expects (and uses) the `#ends_with?` method on the value.
private def negated_match_ends_with(actual)
if actual.value.ends_with?(expected.value)
FailedMatchData.new("#{actual.label} ends with #{expected.label} (using #ends_with?)",
expected: expected.value.inspect,
actual: actual.value.inspect
private def negated_match_ends_with(actual_value, actual_label)
if actual_value.ends_with?(expected.value)
FailedMatchData.new(description, "#{actual_label} ends with #{expected.label} (using #ends_with?)",
expected: "Not #{expected.value.inspect}",
actual: actual_value.inspect
)
else
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
end
end
# Checks whether the last element of the value is not the expected value.
# This method expects that the actual value is a set (enumerable).
private def negated_match_last(actual)
list = actual.value.to_a
private def negated_match_last(actual_value, actual_label)
list = actual_value.to_a
last = list.last
if expected.value === last
FailedMatchData.new("#{actual.label} ends with #{expected.label} (using expected === last)",
expected: expected.value.inspect,
FailedMatchData.new(description, "#{actual_label} ends with #{expected.label} (using expected === last)",
expected: "Not #{expected.value.inspect}",
actual: last.inspect,
list: list.inspect
)
else
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
end
end
end

View file

@ -33,16 +33,16 @@ module Spectator::Matchers
def match(actual : TestExpression(T)) : MatchData forall T
exception = capture_exception { actual.value }
if exception.nil?
FailedMatchData.new("#{actual.label} did not raise", expected: ExceptionType.inspect)
FailedMatchData.new(description, "#{actual.label} did not raise", expected: ExceptionType.inspect)
else
if exception.is_a?(ExceptionType)
if (value = expected.value).nil?
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
else
if value === exception.message
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
else
FailedMatchData.new("#{actual.label} raised #{exception.class}, but the message is not #{expected.label}",
FailedMatchData.new(description, "#{actual.label} raised #{exception.class}, but the message is not #{expected.label}",
"expected type": ExceptionType.inspect,
"actual type": exception.class.inspect,
"expected message": value.inspect,
@ -51,7 +51,7 @@ module Spectator::Matchers
end
end
else
FailedMatchData.new("#{actual.label} did not raise #{ExceptionType}",
FailedMatchData.new(description, "#{actual.label} did not raise #{ExceptionType}",
expected: ExceptionType.inspect,
actual: exception.class.inspect
)
@ -64,32 +64,37 @@ module Spectator::Matchers
def negated_match(actual : TestExpression(T)) : MatchData forall T
exception = capture_exception { actual.value }
if exception.nil?
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
else
if exception.is_a?(ExceptionType)
if (value = expected.value).nil?
FailedMatchData.new("#{actual.label} raised #{exception.class}",
FailedMatchData.new(description, "#{actual.label} raised #{exception.class}",
expected: "Not #{ExceptionType}",
actual: exception.class.inspect
)
else
if value === exception.message
FailedMatchData.new("#{actual.label} raised #{exception.class} with message matching #{expected.label}",
FailedMatchData.new(description, "#{actual.label} raised #{exception.class} with message matching #{expected.label}",
"expected type": ExceptionType.inspect,
"actual type": exception.class.inspect,
"expected message": value.inspect,
"actual message": exception.message.to_s
)
else
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
end
end
else
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
end
end
end
def with_message(message : T) forall T
value = TestValue.new(message)
ExceptionMatcher(ExceptionType, T).new(value)
end
# Runs a block of code and returns the exception it threw.
# If no exception was thrown, *nil* is returned.
private def capture_exception

View file

@ -15,11 +15,13 @@ module Spectator::Matchers
getter values : Array(Tuple(Symbol, String))
# Creates the match data.
def initialize(@failure_message, @values)
def initialize(description, @failure_message, @values)
super(description)
end
# Creates the match data.
def initialize(@failure_message, **values)
def initialize(description, @failure_message, **values)
super(description)
@values = values.to_a
end
end

View file

@ -7,13 +7,6 @@ module Spectator::Matchers
# Each key in the tuple is a predicate (without the '?' and 'has_' prefix) to test.
# Each value is a a `Tuple` of arguments to pass to the predicate method.
struct HavePredicateMatcher(ExpectedType) < ValueMatcher(ExpectedType)
# Expected value and label.
private getter expected
# Creates the matcher with a expected values.
def initialize(@expected : TestValue(ExpectedType))
end
# Short text about the matcher's purpose.
# This explains what condition satisfies the matcher.
# The description is used when the one-liner syntax is used.
@ -25,9 +18,9 @@ module Spectator::Matchers
def match(actual : TestExpression(T)) : MatchData forall T
snapshot = snapshot_values(actual.value)
if match?(snapshot)
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
else
FailedMatchData.new("#{actual.label} does not have #{expected.label}", **values(snapshot))
FailedMatchData.new(description, "#{actual.label} does not have #{expected.label}", **values(snapshot))
end
end
@ -36,9 +29,9 @@ module Spectator::Matchers
def negated_match(actual : TestExpression(T)) : MatchData forall T
snapshot = snapshot_values(actual.value)
if match?(snapshot)
FailedMatchData.new("#{actual.label} has #{expected.label}", **values(snapshot))
FailedMatchData.new(description, "#{actual.label} has #{expected.label}", **values(snapshot))
else
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
end
end

View file

@ -0,0 +1,57 @@
require "./matcher"
module Spectator::Matchers
# Matcher that tests a value is of a specified type.
struct InstanceMatcher(Expected) < StandardMatcher
# Short text about the matcher's purpose.
# This explains what condition satisfies the matcher.
# The description is used when the one-liner syntax is used.
def description : String
"is an instance of #{Expected}"
end
# Checks whether the matcher is satisifed with the expression given to it.
private def match?(actual : TestExpression(T)) : Bool forall T
actual.value.class == Expected
end
# Message displayed when the matcher isn't satisifed.
#
# This is only called when `#match?` returns false.
#
# The message should typically only contain the test expression labels.
# Actual values should be returned by `#values`.
private def failure_message(actual) : String
"#{actual.label} is not an instance of #{Expected}"
end
# Message displayed when the matcher isn't satisifed and is negated.
# This is essentially what would satisfy the matcher if it wasn't negated.
#
# This is only called when `#does_not_match?` returns false.
#
# The message should typically only contain the test expression labels.
# Actual values should be returned by `#values`.
private def failure_message_when_negated(actual) : String
"#{actual.label} is an instance of #{Expected}"
end
# Additional information about the match failure.
# The return value is a NamedTuple with Strings for each value.
private def values(actual)
{
expected: Expected.to_s,
actual: actual.value.class.inspect,
}
end
# Additional information about the match failure when negated.
# The return value is a NamedTuple with Strings for each value.
private def negated_values(actual)
{
expected: "Not #{Expected}",
actual: actual.value.class.inspect,
}
end
end
end

View file

@ -3,5 +3,10 @@ module Spectator::Matchers
abstract struct MatchData
# Indicates whether the match as successful or not.
abstract def matched? : Bool
getter description : String
def initialize(@description : String)
end
end
end

View file

@ -24,9 +24,9 @@ module Spectator::Matchers
def match(actual : TestExpression(T)) : MatchData forall T
snapshot = snapshot_values(actual.value)
if match?(snapshot)
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
else
FailedMatchData.new("#{actual.label} is not #{expected.label}", **values(snapshot))
FailedMatchData.new(description, "#{actual.label} is not #{expected.label}", **values(snapshot))
end
end
@ -35,9 +35,9 @@ module Spectator::Matchers
def negated_match(actual : TestExpression(T)) : MatchData forall T
snapshot = snapshot_values(actual.value)
if match?(snapshot)
FailedMatchData.new("#{actual.label} is #{expected.label}", **values(snapshot))
FailedMatchData.new(description, "#{actual.label} is #{expected.label}", **values(snapshot))
else
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
end
end

View file

@ -13,6 +13,7 @@ module Spectator::Matchers
# Returns a new matcher, with the same bounds, but uses an inclusive range.
def inclusive
label = expected.label
new_range = Range.new(range.begin, range.end, exclusive: false)
expected = TestValue.new(new_range, label)
RangeMatcher.new(expected)
@ -20,6 +21,7 @@ module Spectator::Matchers
# Returns a new matcher, with the same bounds, but uses an exclusive range.
def exclusive
label = expected.label
new_range = Range.new(range.begin, range.end, exclusive: true)
expected = TestValue.new(new_range, label)
RangeMatcher.new(expected)

View file

@ -13,7 +13,13 @@ module Spectator::Matchers
# Checks whether the matcher is satisifed with the expression given to it.
private def match?(actual : TestExpression(T)) : Bool forall T
expected.value.same?(actual.value)
value = expected.value
if value.responds_to?(:same?)
value.same?(actual.value)
else
# Value type (struct) comparison.
actual.value.class == value.class && actual.value == value
end
end
# Message displayed when the matcher isn't satisifed.

View file

@ -16,10 +16,10 @@ module Spectator::Matchers
# Actually performs the test against the expression.
def match(actual : TestExpression(T)) : MatchData forall T
snapshot = snapshot_values(actual.value)
if match?(snapshot)
SuccessfulMatchData.new
if snapshot.values.all?
SuccessfulMatchData.new(description)
else
FailedMatchData.new("#{actual.label} does not respond to #{label}", **values(snapshot))
FailedMatchData.new(description, "#{actual.label} does not respond to #{label}", **values(snapshot))
end
end
@ -27,10 +27,10 @@ module Spectator::Matchers
# A successful match with `#match` should normally fail for this method, and vice-versa.
def negated_match(actual : TestExpression(T)) : MatchData forall T
snapshot = snapshot_values(actual.value)
if match?(snapshot)
FailedMatchData.new("#{actual.label} responds to #{label}", **values(snapshot))
if snapshot.values.any?
FailedMatchData.new(description, "#{actual.label} responds to #{label}", **values(snapshot))
else
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
end
end
@ -46,13 +46,6 @@ module Spectator::Matchers
{% end %}
end
# Checks if all results from the snapshot are satisified.
private def match?(snapshot)
# The snapshot did the hard work.
# Here just check if all values are true.
snapshot.values.all?
end
# Produces the tuple for the failed match data from a snapshot of the results.
private def values(snapshot)
{% begin %}

View file

@ -25,9 +25,9 @@ module Spectator::Matchers
# Additionally, `#failure_message` and `#values` are called for a failed match.
def match(actual : TestExpression(T)) : MatchData forall T
if match?(actual)
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
else
FailedMatchData.new(failure_message(actual), values(actual).to_a)
FailedMatchData.new(description, failure_message(actual), values(actual).to_a)
end
end
@ -39,10 +39,11 @@ module Spectator::Matchers
# Otherwise, a `FailedMatchData` instance is returned.
# Additionally, `#failure_message_when_negated` and `#negated_values` are called for a failed match.
def negated_match(actual : TestExpression(T)) : MatchData forall T
# TODO: Invert description.
if does_not_match?(actual)
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
else
FailedMatchData.new(failure_message_when_negated(actual), negated_values(actual).to_a)
FailedMatchData.new(description, failure_message_when_negated(actual), negated_values(actual).to_a)
end
end

View file

@ -22,7 +22,8 @@ module Spectator::Matchers
# Actually performs the test against the expression.
def match(actual : TestExpression(T)) : MatchData forall T
if (value = actual.value).responds_to?(:starts_with?)
value = actual.value
if value.is_a?(String) || value.responds_to?(:starts_with?)
match_starts_with(value, actual.label)
else
match_first(value, actual.label)
@ -32,7 +33,8 @@ module Spectator::Matchers
# Performs the test against the expression, but inverted.
# A successful match with `#match` should normally fail for this method, and vice-versa.
def negated_match(actual : TestExpression(T)) : MatchData forall T
if (value = actual.value).responds_to?(:starts_with?)
value = actual.value
if value.is_a?(String) || value.responds_to?(:starts_with?)
negated_match_starts_with(value, actual.label)
else
negated_match_first(value, actual.label)
@ -43,9 +45,9 @@ module Spectator::Matchers
# This method expects (and uses) the `#starts_with?` method on the value.
private def match_starts_with(actual_value, actual_label)
if actual_value.starts_with?(expected.value)
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
else
FailedMatchData.new("#{actual_label} does not start with #{expected.label} (using #starts_with?)",
FailedMatchData.new(description, "#{actual_label} does not start with #{expected.label} (using #starts_with?)",
expected: expected.value.inspect,
actual: actual_value.inspect
)
@ -59,9 +61,9 @@ module Spectator::Matchers
first = list.first
if expected.value === first
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
else
FailedMatchData.new("#{actual_label} does not start with #{expected.label} (using expected === first)",
FailedMatchData.new(description, "#{actual_label} does not start with #{expected.label} (using expected === first)",
expected: expected.value.inspect,
actual: first.inspect,
list: list.inspect
@ -73,12 +75,12 @@ module Spectator::Matchers
# This method expects (and uses) the `#starts_with?` method on the value.
private def negated_match_starts_with(actual_value, actual_label)
if actual_value.starts_with?(expected.value)
FailedMatchData.new("#{actual_label} starts with #{expected.label} (using #starts_with?)",
expected: expected.value.inspect,
FailedMatchData.new(description, "#{actual_label} starts with #{expected.label} (using #starts_with?)",
expected: "Not #{expected.value.inspect}",
actual: actual_value.inspect
)
else
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
end
end
@ -89,13 +91,13 @@ module Spectator::Matchers
first = list.first
if expected.value === first
FailedMatchData.new("#{actual_label} starts with #{expected.label} (using expected === first)",
expected: expected.value.inspect,
FailedMatchData.new(description, "#{actual_label} starts with #{expected.label} (using expected === first)",
expected: "Not #{expected.value.inspect}",
actual: first.inspect,
list: list.inspect
)
else
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
end
end
end

View file

@ -8,7 +8,7 @@ module Spectator::Matchers
# This explains what condition satisfies the matcher.
# The description is used when the one-liner syntax is used.
def description : String
"is as #{Expected}"
"is a #{Expected}"
end
# Checks whether the matcher is satisifed with the expression given to it.

View file

@ -25,9 +25,9 @@ module Spectator::Matchers
missing, extra = array_diff(expected_elements, actual_elements)
if missing.empty? && extra.empty?
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
else
FailedMatchData.new("#{actual_label} does not contain #{expected.label} (unordered)",
FailedMatchData.new(description, "#{actual_label} does not contain #{expected.label} (unordered)",
expected: expected_elements.inspect,
actual: actual_elements.inspect,
missing: missing.inspect,
@ -44,12 +44,12 @@ module Spectator::Matchers
missing, extra = array_diff(expected_elements, actual_elements)
if missing.empty? && extra.empty?
FailedMatchData.new("#{actual_label} contains #{expected.label} (unordered)",
FailedMatchData.new(description, "#{actual_label} contains #{expected.label} (unordered)",
expected: "Not #{expected_elements.inspect}",
actual: actual_elements.inspect,
)
else
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
end
end

View file

@ -17,13 +17,28 @@ module Spectator::Mocks
named = false
name = definition.name.id
params = definition.args
# Possibly a weird compiler bug, but syntax like this:
# stub instance.==(other) { true }
# Results in `other` being the call `other { true }`.
# This works around the issue by pulling out the block
# and setting the parameter to just the name.
if params.last.is_a?(Call)
body = params.last.block
params[-1] = params.last.name
end
args = params.map do |p|
n = p.is_a?(TypeDeclaration) ? p.var : p.id
r = named ? "#{n}: #{n}".id : n
named = true if n.starts_with?('*')
r
end
body = definition.block.is_a?(Nop) ? block : definition.block
# The unless is here because `||=` can't be used in macros @_@
unless body
body = definition.block.is_a?(Nop) ? block : definition.block
end
elsif definition.is_a?(TypeDeclaration) # stub foo : Symbol
name = definition.var
params = [] of MacroId

View file

@ -12,13 +12,19 @@ module Spectator::Mocks
named = false
name = definition.name.id
params = definition.args
if params.last.is_a?(Call)
body = params.last.block
params[-1] = params.last.name
end
args = params.map do |p|
n = p.is_a?(TypeDeclaration) ? p.var : p.id
r = named ? "#{n}: #{n}".id : n
named = true if n.starts_with?('*')
r
end
body = definition.block.is_a?(Nop) ? block : definition.block
unless body
body = definition.block.is_a?(Nop) ? block : definition.block
end
elsif definition.is_a?(TypeDeclaration) # stub foo : Symbol
name = definition.var
params = [] of MacroId
@ -28,17 +34,19 @@ module Spectator::Mocks
raise "Unrecognized stub format"
end
original = if @type.methods.find { |m| m.name.id == name }
:previous_def
else
:super
end.id
t = @type
receiver = if receiver == :self.id
original = :previous_def.id
t = t.class
"self."
else
""
end.id
original = if (name == :new.id && receiver == "self.".id) ||
(t.superclass.has_method?(name) && !t.overrides?(t.superclass, name))
:super
else
:previous_def
end.id
%}
{% if body && !body.is_a?(Nop) %}

View file

@ -19,6 +19,7 @@ module Spectator
ResultCapture.new.tap do |result|
context.run_before_hooks(self)
run_example(result)
@finished = true
context.run_after_hooks(self)
run_deferred(result) unless result.error
end

View file

@ -42,7 +42,7 @@ module Spectator
# Adds an example type to the current group.
# The class name of the example should be passed as an argument.
# The example will be instantiated later.
def add_example(description : String, source : Source,
def add_example(description : String?, source : Source,
example_type : ::SpectatorTest.class, &runner : ::SpectatorTest ->) : Nil
builder = ->(values : TestValues) { example_type.new(values).as(::SpectatorTest) }
factory = RunnableExampleBuilder.new(description, source, builder, runner)
@ -52,7 +52,7 @@ module Spectator
# Adds an example type to the current group.
# The class name of the example should be passed as an argument.
# The example will be instantiated later.
def add_pending_example(description : String, source : Source,
def add_pending_example(description : String?, source : Source,
example_type : ::SpectatorTest.class, &runner : ::SpectatorTest ->) : Nil
builder = ->(values : TestValues) { example_type.new(values).as(::SpectatorTest) }
factory = PendingExampleBuilder.new(description, source, builder, runner)

View file

@ -6,7 +6,7 @@ module Spectator::SpecBuilder
abstract class ExampleBuilder
alias FactoryMethod = TestValues -> ::SpectatorTest
def initialize(@description : String, @source : Source, @builder : FactoryMethod, @runner : TestMethod)
def initialize(@description : String?, @source : Source, @builder : FactoryMethod, @runner : TestMethod)
end
abstract def build(group) : ExampleComponent

View file

@ -8,13 +8,19 @@ module Spectator
# Used to instantiate tests and run them.
struct TestWrapper
# Description the user provided for the test.
getter description
def description
@description || @source.to_s
end
# Location of the test in source code.
getter source
# Creates a wrapper for the test.
def initialize(@description : String, @source : Source, @test : ::SpectatorTest, @runner : TestMethod)
def initialize(@description : String?, @source : Source, @test : ::SpectatorTest, @runner : TestMethod)
end
def description?
!@description.nil?
end
def run

View file

@ -6,6 +6,14 @@ require "./spectator/dsl"
class SpectatorTest
include ::Spectator::DSL
def _spectator_implicit_subject
nil
end
def subject
_spectator_implicit_subject
end
def initialize(@spectator_test_values : ::Spectator::TestValues)
end
end