mirror of
https://gitea.invidious.io/iv-org/shard-spectator.git
synced 2024-08-15 00:53:35 +00:00
Merge branch 'master' into mocks-and-doubles
This commit is contained in:
commit
86729c6745
70 changed files with 1799 additions and 209 deletions
|
@ -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
5
.guardian.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
files: ./**/*.cr
|
||||
run: time crystal spec --error-trace
|
||||
---
|
||||
files: ./shard.yml
|
||||
run: shards
|
|
@ -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
|
||||
|
|
32
spec/matchers/equality_matcher_spec.cr
Normal file
32
spec/matchers/equality_matcher_spec.cr
Normal 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
|
112
spec/rspec/core/before_and_after_hooks_spec.cr
Normal file
112
spec/rspec/core/before_and_after_hooks_spec.cr
Normal 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
|
140
spec/rspec/core/explicit_subject_spec.cr
Normal file
140
spec/rspec/core/explicit_subject_spec.cr
Normal 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
|
29
spec/rspec/core/helper_methods_spec.cr
Normal file
29
spec/rspec/core/helper_methods_spec.cr
Normal 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
|
43
spec/rspec/core/implicit_subject_spec.cr
Normal file
43
spec/rspec/core/implicit_subject_spec.cr
Normal 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
|
45
spec/rspec/core/let_spec.cr
Normal file
45
spec/rspec/core/let_spec.cr
Normal 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
|
31
spec/rspec/core/one_liner_subject_spec.cr
Normal file
31
spec/rspec/core/one_liner_subject_spec.cr
Normal 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
|
37
spec/rspec/expectations/all_matcher_spec.cr
Normal file
37
spec/rspec/expectations/all_matcher_spec.cr
Normal 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
|
17
spec/rspec/expectations/be_between_matcher_spec.cr
Normal file
17
spec/rspec/expectations/be_between_matcher_spec.cr
Normal 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
|
66
spec/rspec/expectations/be_matchers_spec.cr
Normal file
66
spec/rspec/expectations/be_matchers_spec.cr
Normal 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
|
24
spec/rspec/expectations/be_within_matcher_spec.cr
Normal file
24
spec/rspec/expectations/be_within_matcher_spec.cr
Normal 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
|
47
spec/rspec/expectations/change_matcher_spec.cr
Normal file
47
spec/rspec/expectations/change_matcher_spec.cr
Normal 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
|
44
spec/rspec/expectations/comparison_matchers_spec.cr
Normal file
44
spec/rspec/expectations/comparison_matchers_spec.cr
Normal 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
|
30
spec/rspec/expectations/contain_exactly_matcher_spec.cr
Normal file
30
spec/rspec/expectations/contain_exactly_matcher_spec.cr
Normal 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
|
96
spec/rspec/expectations/contain_matcher_spec.cr
Normal file
96
spec/rspec/expectations/contain_matcher_spec.cr
Normal 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
|
29
spec/rspec/expectations/cover_matcher_spec.cr
Normal file
29
spec/rspec/expectations/cover_matcher_spec.cr
Normal 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
|
31
spec/rspec/expectations/end_with_matcher_spec.cr
Normal file
31
spec/rspec/expectations/end_with_matcher_spec.cr
Normal 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
|
64
spec/rspec/expectations/equality_matchers_spec.cr
Normal file
64
spec/rspec/expectations/equality_matchers_spec.cr
Normal 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
|
36
spec/rspec/expectations/have_attributes_matcher_spec.cr
Normal file
36
spec/rspec/expectations/have_attributes_matcher_spec.cr
Normal 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
|
28
spec/rspec/expectations/match_matcher_spec.cr
Normal file
28
spec/rspec/expectations/match_matcher_spec.cr
Normal 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
|
81
spec/rspec/expectations/predicate_matchers_spec.cr
Normal file
81
spec/rspec/expectations/predicate_matchers_spec.cr
Normal 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
|
94
spec/rspec/expectations/raise_error_matcher_spec.cr
Normal file
94
spec/rspec/expectations/raise_error_matcher_spec.cr
Normal 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
|
28
spec/rspec/expectations/respond_to_matcher_spec.cr
Normal file
28
spec/rspec/expectations/respond_to_matcher_spec.cr
Normal 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
|
31
spec/rspec/expectations/start_with_matcher_spec.cr
Normal file
31
spec/rspec/expectations/start_with_matcher_spec.cr
Normal 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
|
100
spec/rspec/expectations/type_matchers_spec.cr
Normal file
100
spec/rspec/expectations/type_matchers_spec.cr
Normal 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
|
|
@ -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
23
spec/subject_spec.cr
Normal 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
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 %}
|
||||
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
true
|
||||
|
||||
# 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
|
||||
false
|
||||
{element, count}
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
57
src/spectator/matchers/instance_matcher.cr
Normal file
57
src/spectator/matchers/instance_matcher.cr
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
# 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
|
||||
|
|
|
@ -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
|
||||
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) %}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue