Merge branch 'specs' into 'master'

Initial RSpec specs and various fixes

See merge request arctic-fox/spectator!24
This commit is contained in:
Mike Miller 2020-01-15 03:14:50 +00:00
commit d2f0f52729
54 changed files with 1200 additions and 188 deletions

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,12 +1,12 @@
name: spectator
version: 0.9.1
version: 0.9.2
description: |
A feature-rich spec testing framework for Crystal with similarities to RSpec.
authors:
- Michael Miller <icy.arctic.fox@gmail.com>
crystal: 0.31.1
crystal: 0.32.1
license: MIT

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,39 @@
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
# TODO: Add support for expected failures.
xit { is_expected.to all(be_even) }
xit { is_expected.to all(be_a(String)) }
xit { 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 expected 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,70 @@
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
# TODO: Add support for expected failures.
pending { expect(true).not_to be_truthy }
pending { expect(7).not_to be_truthy }
pending { expect("foo").not_to be_truthy }
pending { expect(nil).to be_truthy }
pending { 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
# TODO: Add support for expected failures.
pending { expect(nil).not_to be_falsey }
pending { expect(false).not_to be_falsey }
pending { expect(true).to be_falsey }
pending { expect(7).to be_falsey }
pending { 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
# TODO: Add support for expected failures.
pending { expect(nil).not_to be_nil }
pending { expect(false).to be_nil }
pending { expect(true).to be_nil }
pending { expect(7).to be_nil }
pending { 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
# TODO: Add support for expected failures.
pending { expect(true).not_to be }
pending { expect(7).not_to be }
pending { expect("foo").not_to be }
pending { expect(nil).to be }
pending { expect(false).to be }
end
end

View file

@ -0,0 +1,25 @@
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
# TODO: Add support for expected failures.
xit { is_expected.not_to be_within(0.5).of(28) }
xit { is_expected.not_to be_within(0.5).of(27) }
xit { is_expected.to be_within(0.5).of(28.1) }
xit { is_expected.to be_within(0.5).of(26.9) }
end
end
end

View file

@ -0,0 +1,49 @@
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
# TODO: Add support for expected failures.
xit "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
# TODO: Add support for expected failures.
xit "should not increment the count by 1 (using not_to)" do
expect { Counter.increment }.not_to change { Counter.count }
end
xit "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,47 @@
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
# TODO: Add support for expected failures.
xit { is_expected.to be < 15 }
xit { is_expected.to be > 20 }
xit { is_expected.to be <= 17 }
xit { 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
# TODO: Add support for expected 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
# TODO: Add support for expected failures.
xit { is_expected.to be < "Cranberry" }
xit { is_expected.to be > "Zuchini" }
xit { is_expected.to be <= "Potato" }
xit { is_expected.to be >= "Tomato" }
end
end
end

View file

@ -0,0 +1,32 @@
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
# TODO: Add support for expected failures.
xit { 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
# TODO: Add support for expected failures.
xit { is_expected.to_not contain_exactly(1, 3, 2) }
end
end
end

View file

@ -0,0 +1,99 @@
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
# TODO: Add support for expected failures.
xit { is_expected.to contain(4) }
xit { is_expected.to contain(be_even) }
xit { is_expected.not_to contain(1) }
xit { is_expected.not_to contain(3) }
xit { is_expected.not_to contain(7) }
xit { is_expected.not_to contain(1, 3, 7) }
# both of these should fail since it contains 1 but not 9
xit { is_expected.to contain(1, 9) }
xit { 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
# TODO: Add support for expected failures.
xit { is_expected.to contain("foo") }
xit { is_expected.not_to contain("str") }
xit { is_expected.to contain("str", "foo") }
xit { 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`.
xit { is_expected.to contain(:a) }
xit { 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) }
xit { is_expected.not_to contain(:c) }
xit { 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
# TODO: Add support for expected failures.
xit { is_expected.not_to contain(:a) }
xit { 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) }
xit { is_expected.to contain(:c) }
xit { 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.
xit { is_expected.to contain(:a, :d) }
xit { 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,30 @@
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
# TODO: Add support for expected failures.
xit { is_expected.to cover(11) }
xit { is_expected.not_to cover(4) }
xit { is_expected.not_to cover(6) }
xit { is_expected.not_to cover(8) }
xit { is_expected.not_to cover(4, 6, 8) }
# both of these should fail since it covers 5 but not 11
xit { is_expected.to cover(5, 11) }
xit { is_expected.not_to cover(5, 11) }
end
end
end

View file

@ -0,0 +1,33 @@
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
# TODO: Add support for expected failures.
xit { is_expected.not_to end_with "string" }
xit { 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
# TODO: Add support for expected failures.
xit { is_expected.not_to end_with 4 }
xit { 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,37 @@
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
# TODO: Add support for expected failures.
xit { is_expected.to have_attributes(name: "Bob") }
xit { is_expected.to have_attributes(name: 10) }
# fails if any of the attributes don't match
xit { is_expected.to have_attributes(name: "Bob", age: 32) }
xit { is_expected.to have_attributes(name: "Jim", age: 10) }
xit { is_expected.to have_attributes(name: "Bob", age: 10) }
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/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
# TODO: Add support for expected failures.
xit { is_expected.not_to match(/str/) }
xit { 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
# TODO: Add support for expected failures.
xit { is_expected.not_to match("food") }
xit { is_expected.to match("drinks") }
end
end
end

View file

@ -0,0 +1,86 @@
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
# TODO: Add support for expected failures.
xit { 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
# TODO: Add support for expected failures.
xit { 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
# TODO: Add support for expected failures.
xit { 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
# TODO: Add support for expected failures.
xit { 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
# TODO: Add support for expected failures.
xit { is_expected.not_to be_multiple_of(4) }
xit { 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,33 @@
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
# TODO: Add support for expected failures.
xit { is_expected.not_to start_with "this" }
xit { 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
# TODO: Add support for expected failures.
xit { is_expected.not_to start_with 0 }
xit { is_expected.to start_with 3 }
end
end
end

View file

@ -0,0 +1,101 @@
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
# TODO: Add support for expected failures.
xit { is_expected.not_to be_kind_of(Derived) }
xit { is_expected.not_to be_a_kind_of(Derived) }
xit { is_expected.not_to be_a(Derived) }
xit { is_expected.not_to be_kind_of(Base) }
xit { is_expected.not_to be_a_kind_of(Base) }
xit { is_expected.not_to be_an(Base) }
xit { is_expected.not_to be_kind_of(MyModule) }
xit { is_expected.not_to be_a_kind_of(MyModule) }
xit { is_expected.not_to be_a(MyModule) }
xit { is_expected.to be_kind_of(String) }
xit { is_expected.to be_a_kind_of(String) }
xit { 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
xit { is_expected.not_to be_instance_of(Derived) }
xit { is_expected.not_to be_an_instance_of(Derived) }
xit { is_expected.to be_instance_of(Base) }
xit { is_expected.to be_an_instance_of(Base) }
xit { is_expected.to be_instance_of(MyModule) }
xit { is_expected.to be_an_instance_of(MyModule) }
xit { is_expected.to be_instance_of(String) }
xit { is_expected.to be_an_instance_of(String) }
end
end
end
end

View file

@ -1,4 +1 @@
require "../src/spectator"
# Prevent Spectator from trying to run tests on its own.
Spectator.autorun = false

View file

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

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,7 +19,7 @@ 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)) && what.resolve? %}
macro described_class
{{what}}
end
@ -27,6 +27,10 @@ module Spectator
def subject(*args)
described_class.new(*args)
end
{% else %}
def subject
{{what}}
end
{% end %}
{{block.body}}

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.
#
@ -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

@ -17,9 +17,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 respond to #{label}", **values(snapshot))
FailedMatchData.new(description, "#{actual.label} does not respond to #{label}", **values(snapshot))
end
end
@ -28,9 +28,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} responds to #{label}", **values(snapshot))
FailedMatchData.new(description, "#{actual.label} responds to #{label}", **values(snapshot))
else
SuccessfulMatchData.new
SuccessfulMatchData.new(description)
end
end

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

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