From b95cf5693bb5010c1ee5ee12d4dd7bd4fc804399 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 24 Oct 2019 20:56:22 -0600 Subject: [PATCH 01/78] Rework let and subject to allow super This addresses https://gitlab.com/arctic-fox/spectator/issues/32 --- src/spectator/dsl/structure_dsl.cr | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/spectator/dsl/structure_dsl.cr b/src/spectator/dsl/structure_dsl.cr index bed9c59..1c9892c 100644 --- a/src/spectator/dsl/structure_dsl.cr +++ b/src/spectator/dsl/structure_dsl.cr @@ -897,7 +897,7 @@ module Spectator::DSL # Block variant. # Create a block that returns the value. - let!(%value) {{block}} + let!({{name.id}}) {{block}} # Wrapper to hold the value. # This will be nil if the value hasn't been referenced yet. @@ -913,12 +913,12 @@ module Spectator::DSL # Unwrap it from the wrapper variable. # Here we use typeof to get around the issue # that the macro has no idea what type the value is. - wrapper.unsafe_as(::Spectator::Internals::TypedValueWrapper(typeof(%value))).value + wrapper.unsafe_as(::Spectator::Internals::TypedValueWrapper(typeof(previous_def))).value else # The value isn't cached, # Construct it and store it in the wrapper. - %value.tap do |value| - @%wrapper = ::Spectator::Internals::TypedValueWrapper(typeof(%value)).new(value) + previous_def.tap do |value| + @%wrapper = ::Spectator::Internals::TypedValueWrapper(typeof(previous_def)).new(value) end end end From af24e5c4cd5e63660b9579dd8c02e676cb6221b7 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 24 Oct 2019 20:58:53 -0600 Subject: [PATCH 02/78] Bump version to 0.8.4 --- shard.yml | 4 ++-- src/spectator.cr | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/shard.yml b/shard.yml index fb2bced..f4c31b6 100644 --- a/shard.yml +++ b/shard.yml @@ -1,12 +1,12 @@ name: spectator -version: 0.8.3 +version: 0.8.4 description: | A feature-rich spec testing framework for Crystal with similarities to RSpec. authors: - Michael Miller -crystal: 0.31.0 +crystal: 0.31.1 license: MIT diff --git a/src/spectator.cr b/src/spectator.cr index 5f17921..bf476b6 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -5,7 +5,7 @@ module Spectator extend self # Current version of the Spectator library. - VERSION = "0.8.2" + VERSION = "0.8.4" # Top-level describe method. # All specs in a file must be wrapped in this call. From 4684840d2feed1479458e4ce333b5d51fa954069 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 8 Dec 2019 16:23:43 -0700 Subject: [PATCH 03/78] Update README --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1585016..7e0d233 100644 --- a/README.md +++ b/README.md @@ -310,14 +310,15 @@ Items not marked as completed may have partial implementations. - [X] `have_attributes` - [ ] Compound - `and`, `or` - [ ] Mocks and Doubles - - [ ] Mocks (Stub real types) - `mock TYPE { }` + - [X] Mocks (Stub real types) - `mock TYPE { }` - [X] Doubles (Stand-ins for real types) - `double NAME { }` - [X] Method stubs - `allow().to receive()`, `allow().to receive().and_return()` - - [ ] Spies - `expect().to receive()` + - [X] Spies - `expect().to receive()` - [X] Message expectations - `expect().to receive().at_least()` - [X] Argument expectations - `expect().to receive().with()` - [ ] Message ordering - `expect().to receive().ordered` - - [ ] Null doubles + - [X] Null doubles + - [X] Verifying doubles - [ ] Runner - [X] Fail fast - [ ] Test filtering - by name, context, and tags From 061028782bcc4d5d496c4b5568a7d81a97148b6f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 1 Jan 2020 21:48:58 -0700 Subject: [PATCH 04/78] Workaround for what seems to be a Crystal macro bug The syntax: stub instance.==(other) { true } effectively becomes: stub instance.==(other { true }) --- src/spectator/mocks/double.cr | 17 ++++++++++++++++- src/spectator/mocks/stubs.cr | 8 +++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/spectator/mocks/double.cr b/src/spectator/mocks/double.cr index 45071ac..ef17e84 100644 --- a/src/spectator/mocks/double.cr +++ b/src/spectator/mocks/double.cr @@ -17,13 +17,28 @@ module Spectator::Mocks named = false name = definition.name.id params = definition.args + + # Possibly a weird compiler bug, but syntax like this: + # stub instance.==(other) { true } + # Results in `other` being the call `other { true }`. + # This works around the issue by pulling out the block + # and setting the parameter to just the name. + if params.last.is_a?(Call) + body = params.last.block + params[-1] = params.last.name + end + args = params.map do |p| n = p.is_a?(TypeDeclaration) ? p.var : p.id r = named ? "#{n}: #{n}".id : n named = true if n.starts_with?('*') r end - body = definition.block.is_a?(Nop) ? block : definition.block + + # The unless is here because `||=` can't be used in macros @_@ + unless body + body = definition.block.is_a?(Nop) ? block : definition.block + end elsif definition.is_a?(TypeDeclaration) # stub foo : Symbol name = definition.var params = [] of MacroId diff --git a/src/spectator/mocks/stubs.cr b/src/spectator/mocks/stubs.cr index 374cc78..00677f8 100644 --- a/src/spectator/mocks/stubs.cr +++ b/src/spectator/mocks/stubs.cr @@ -12,13 +12,19 @@ module Spectator::Mocks named = false name = definition.name.id params = definition.args + if params.last.is_a?(Call) + body = params.last.block + params[-1] = params.last.name + end args = params.map do |p| n = p.is_a?(TypeDeclaration) ? p.var : p.id r = named ? "#{n}: #{n}".id : n named = true if n.starts_with?('*') r end - body = definition.block.is_a?(Nop) ? block : definition.block + unless body + body = definition.block.is_a?(Nop) ? block : definition.block + end elsif definition.is_a?(TypeDeclaration) # stub foo : Symbol name = definition.var params = [] of MacroId From 0a6493964d0e261bcf37c49b926a78a8a7f59337 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 1 Jan 2020 21:50:08 -0700 Subject: [PATCH 05/78] Allow auto-run of Spectator Needed to test Spectator with itself. --- spec/spec_helper.cr | 3 --- 1 file changed, 3 deletions(-) diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 30b0ee7..bca1453 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -1,4 +1 @@ require "../src/spectator" - -# Prevent Spectator from trying to run tests on its own. -Spectator.autorun = false From 681f978df0057983c2e5afe0b28c8fbf29322c39 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 1 Jan 2020 21:50:31 -0700 Subject: [PATCH 06/78] Add Guardian config to automatically run specs --- .guardian.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .guardian.yml diff --git a/.guardian.yml b/.guardian.yml new file mode 100644 index 0000000..ad67b38 --- /dev/null +++ b/.guardian.yml @@ -0,0 +1,5 @@ +files: ./**/*.cr +run: time crystal spec --error-trace +--- +files: ./shard.yml +run: shards From 9f91e3a2660b4ac3b5ff6782fb895d073e29247e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Wed, 1 Jan 2020 21:58:37 -0700 Subject: [PATCH 07/78] Add tests for equality matcher --- spec/matchers/equality_matcher_spec.cr | 32 ++++++++++ .../expectations/equality_matchers_spec.cr | 64 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 spec/matchers/equality_matcher_spec.cr create mode 100644 spec/rspec/expectations/equality_matchers_spec.cr diff --git a/spec/matchers/equality_matcher_spec.cr b/spec/matchers/equality_matcher_spec.cr new file mode 100644 index 0000000..3b6b83b --- /dev/null +++ b/spec/matchers/equality_matcher_spec.cr @@ -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 diff --git a/spec/rspec/expectations/equality_matchers_spec.cr b/spec/rspec/expectations/equality_matchers_spec.cr new file mode 100644 index 0000000..9aa923c --- /dev/null +++ b/spec/rspec/expectations/equality_matchers_spec.cr @@ -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 From ff2cbcd4c72232e2cbd53dbca4f275e9a81d29b6 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 2 Jan 2020 18:40:25 -0700 Subject: [PATCH 08/78] Allow test description to be blank Currently defaults to the example's source. --- src/spectator/dsl/examples.cr | 14 +++++++------- src/spectator/example.cr | 2 +- src/spectator/spec_builder.cr | 4 ++-- src/spectator/spec_builder/example_builder.cr | 2 +- src/spectator/test_wrapper.cr | 5 +++++ 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/spectator/dsl/examples.cr b/src/spectator/dsl/examples.cr index 041b1a8..b356ea5 100644 --- a/src/spectator/dsl/examples.cr +++ b/src/spectator/dsl/examples.cr @@ -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 diff --git a/src/spectator/example.cr b/src/spectator/example.cr index e437bc4..3cf7051 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -23,7 +23,7 @@ module Spectator @test_wrapper.source end - def description : String | Symbol + def description : String | Symbol? @test_wrapper.description end diff --git a/src/spectator/spec_builder.cr b/src/spectator/spec_builder.cr index 9e199c2..39834be 100644 --- a/src/spectator/spec_builder.cr +++ b/src/spectator/spec_builder.cr @@ -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) diff --git a/src/spectator/spec_builder/example_builder.cr b/src/spectator/spec_builder/example_builder.cr index 44f23fe..378c024 100644 --- a/src/spectator/spec_builder/example_builder.cr +++ b/src/spectator/spec_builder/example_builder.cr @@ -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 diff --git a/src/spectator/test_wrapper.cr b/src/spectator/test_wrapper.cr index 756f287..7bd756f 100644 --- a/src/spectator/test_wrapper.cr +++ b/src/spectator/test_wrapper.cr @@ -17,6 +17,11 @@ module Spectator def initialize(@description : String, @source : Source, @test : ::SpectatorTest, @runner : TestMethod) end + # Creates a wrapper for the test. + def initialize(description : Nil, @source : Source, @test : ::SpectatorTest, @runner : TestMethod) + @description = @source.to_s + end + def run call(@runner) end From 00fe913d77c7d9ec5131981ffb6189b5216cfd13 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 2 Jan 2020 18:43:30 -0700 Subject: [PATCH 09/78] Add predicate method to check for description --- src/spectator/test_wrapper.cr | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/spectator/test_wrapper.cr b/src/spectator/test_wrapper.cr index 7bd756f..79d5265 100644 --- a/src/spectator/test_wrapper.cr +++ b/src/spectator/test_wrapper.cr @@ -8,18 +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 - # Creates a wrapper for the test. - def initialize(description : Nil, @source : Source, @test : ::SpectatorTest, @runner : TestMethod) - @description = @source.to_s + def description? + !@description.nil? end def run From d64ecc41924e6a9011de03b9f87541842b3f99cd Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 2 Jan 2020 19:16:33 -0700 Subject: [PATCH 10/78] Store matcher description in MatchData --- src/spectator/matchers/all_matcher.cr | 2 +- src/spectator/matchers/array_matcher.cr | 12 +++++------ src/spectator/matchers/attributes_matcher.cr | 8 ++++---- .../matchers/change_exact_matcher.cr | 14 ++++++------- src/spectator/matchers/change_from_matcher.cr | 12 +++++------ src/spectator/matchers/change_matcher.cr | 8 ++++---- .../matchers/change_relative_matcher.cr | 6 +++--- src/spectator/matchers/change_to_matcher.cr | 6 +++--- src/spectator/matchers/end_with_matcher.cr | 16 +++++++-------- src/spectator/matchers/exception_matcher.cr | 20 +++++++++---------- src/spectator/matchers/failed_match_data.cr | 6 ++++-- src/spectator/matchers/match_data.cr | 5 +++++ src/spectator/matchers/predicate_matcher.cr | 8 ++++---- src/spectator/matchers/respond_matcher.cr | 8 ++++---- src/spectator/matchers/standard_matcher.cr | 9 +++++---- src/spectator/matchers/start_with_matcher.cr | 16 +++++++-------- .../matchers/unordered_array_matcher.cr | 8 ++++---- 17 files changed, 86 insertions(+), 78 deletions(-) diff --git a/src/spectator/matchers/all_matcher.cr b/src/spectator/matchers/all_matcher.cr index bf6231d..d99ade4 100644 --- a/src/spectator/matchers/all_matcher.cr +++ b/src/spectator/matchers/all_matcher.cr @@ -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. diff --git a/src/spectator/matchers/array_matcher.cr b/src/spectator/matchers/array_matcher.cr index 314a31e..8144db5 100644 --- a/src/spectator/matchers/array_matcher.cr +++ b/src/spectator/matchers/array_matcher.cr @@ -31,7 +31,7 @@ module Spectator::Matchers when Int # Content differs. failed_content_mismatch(expected_elements, actual_elements, index, actual.label) when true # Contents are identical. - SuccessfulMatchData.new + SuccessfulMatchData.new(description) else # Size differs. failed_size_mismatch(expected_elements, actual_elements, actual.label) end @@ -45,11 +45,11 @@ module Spectator::Matchers case compare_arrays(expected_elements, actual_elements) when Int # Contents differ. - SuccessfulMatchData.new + SuccessfulMatchData.new(description) when true # Contents are identical. failed_content_identical(expected_elements, actual_elements, actual.label) else # Size differs. - SuccessfulMatchData.new + SuccessfulMatchData.new(description) end end @@ -85,7 +85,7 @@ module Spectator::Matchers # 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)", + FailedMatchData.new(description, "#{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, @@ -95,7 +95,7 @@ module Spectator::Matchers # 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)", + FailedMatchData.new(description, "#{actual_label} does not contain exactly #{expected.label} (element mismatch)", expected: expected_elements[index].inspect, actual: actual_elements[index].inspect, index: index.to_s @@ -104,7 +104,7 @@ module Spectator::Matchers # 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}", + FailedMatchData.new(description, "#{actual_label} contains exactly #{expected.label}", expected: "Not #{expected_elements.inspect}", actual: actual_elements.inspect ) diff --git a/src/spectator/matchers/attributes_matcher.cr b/src/spectator/matchers/attributes_matcher.cr index 93cd62a..2a72d5b 100644 --- a/src/spectator/matchers/attributes_matcher.cr +++ b/src/spectator/matchers/attributes_matcher.cr @@ -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 diff --git a/src/spectator/matchers/change_exact_matcher.cr b/src/spectator/matchers/change_exact_matcher.cr index 7245386..6aa562d 100644 --- a/src/spectator/matchers/change_exact_matcher.cr +++ b/src/spectator/matchers/change_exact_matcher.cr @@ -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, ) diff --git a/src/spectator/matchers/change_from_matcher.cr b/src/spectator/matchers/change_from_matcher.cr index fa3504b..c0e08c1 100644 --- a/src/spectator/matchers/change_from_matcher.cr +++ b/src/spectator/matchers/change_from_matcher.cr @@ -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 diff --git a/src/spectator/matchers/change_matcher.cr b/src/spectator/matchers/change_matcher.cr index 5de2415..a60891b 100644 --- a/src/spectator/matchers/change_matcher.cr +++ b/src/spectator/matchers/change_matcher.cr @@ -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 ) diff --git a/src/spectator/matchers/change_relative_matcher.cr b/src/spectator/matchers/change_relative_matcher.cr index 2f170eb..4ac6e38 100644 --- a/src/spectator/matchers/change_relative_matcher.cr +++ b/src/spectator/matchers/change_relative_matcher.cr @@ -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 ) diff --git a/src/spectator/matchers/change_to_matcher.cr b/src/spectator/matchers/change_to_matcher.cr index e29d23b..fb43204 100644 --- a/src/spectator/matchers/change_to_matcher.cr +++ b/src/spectator/matchers/change_to_matcher.cr @@ -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 diff --git a/src/spectator/matchers/end_with_matcher.cr b/src/spectator/matchers/end_with_matcher.cr index cc28e9a..4d77a36 100644 --- a/src/spectator/matchers/end_with_matcher.cr +++ b/src/spectator/matchers/end_with_matcher.cr @@ -44,9 +44,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 +60,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 @@ -74,12 +74,12 @@ module Spectator::Matchers # 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?)", + FailedMatchData.new(description, "#{actual.label} ends with #{expected.label} (using #ends_with?)", expected: expected.value.inspect, actual: actual.value.inspect ) else - SuccessfulMatchData.new + SuccessfulMatchData.new(description) end end @@ -90,13 +90,13 @@ module Spectator::Matchers last = list.last if expected.value === last - FailedMatchData.new("#{actual.label} ends with #{expected.label} (using expected === last)", + FailedMatchData.new(description, "#{actual.label} ends with #{expected.label} (using expected === last)", expected: expected.value.inspect, actual: last.inspect, list: list.inspect ) else - SuccessfulMatchData.new + SuccessfulMatchData.new(description) end end end diff --git a/src/spectator/matchers/exception_matcher.cr b/src/spectator/matchers/exception_matcher.cr index 6455df5..f94c3f2 100644 --- a/src/spectator/matchers/exception_matcher.cr +++ b/src/spectator/matchers/exception_matcher.cr @@ -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,28 +64,28 @@ 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 diff --git a/src/spectator/matchers/failed_match_data.cr b/src/spectator/matchers/failed_match_data.cr index 205b31c..537cf95 100644 --- a/src/spectator/matchers/failed_match_data.cr +++ b/src/spectator/matchers/failed_match_data.cr @@ -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 diff --git a/src/spectator/matchers/match_data.cr b/src/spectator/matchers/match_data.cr index 47b444c..31454f5 100644 --- a/src/spectator/matchers/match_data.cr +++ b/src/spectator/matchers/match_data.cr @@ -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 diff --git a/src/spectator/matchers/predicate_matcher.cr b/src/spectator/matchers/predicate_matcher.cr index 5c43c60..bacfac0 100644 --- a/src/spectator/matchers/predicate_matcher.cr +++ b/src/spectator/matchers/predicate_matcher.cr @@ -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 diff --git a/src/spectator/matchers/respond_matcher.cr b/src/spectator/matchers/respond_matcher.cr index ce69a7e..3f26041 100644 --- a/src/spectator/matchers/respond_matcher.cr +++ b/src/spectator/matchers/respond_matcher.cr @@ -17,9 +17,9 @@ module Spectator::Matchers def match(actual : TestExpression(T)) : MatchData forall T snapshot = snapshot_values(actual.value) if match?(snapshot) - SuccessfulMatchData.new + SuccessfulMatchData.new(description) else - FailedMatchData.new("#{actual.label} does not respond to #{label}", **values(snapshot)) + FailedMatchData.new(description, "#{actual.label} does not respond to #{label}", **values(snapshot)) end end @@ -28,9 +28,9 @@ module Spectator::Matchers def negated_match(actual : TestExpression(T)) : MatchData forall T snapshot = snapshot_values(actual.value) if match?(snapshot) - FailedMatchData.new("#{actual.label} responds to #{label}", **values(snapshot)) + FailedMatchData.new(description, "#{actual.label} responds to #{label}", **values(snapshot)) else - SuccessfulMatchData.new + SuccessfulMatchData.new(description) end end diff --git a/src/spectator/matchers/standard_matcher.cr b/src/spectator/matchers/standard_matcher.cr index 2a01ab3..52cdc67 100644 --- a/src/spectator/matchers/standard_matcher.cr +++ b/src/spectator/matchers/standard_matcher.cr @@ -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 diff --git a/src/spectator/matchers/start_with_matcher.cr b/src/spectator/matchers/start_with_matcher.cr index 29da717..aaa8846 100644 --- a/src/spectator/matchers/start_with_matcher.cr +++ b/src/spectator/matchers/start_with_matcher.cr @@ -43,9 +43,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 +59,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 +73,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?)", + FailedMatchData.new(description, "#{actual_label} starts with #{expected.label} (using #starts_with?)", expected: expected.value.inspect, actual: actual_value.inspect ) else - SuccessfulMatchData.new + SuccessfulMatchData.new(description) end end @@ -89,13 +89,13 @@ module Spectator::Matchers first = list.first if expected.value === first - FailedMatchData.new("#{actual_label} starts with #{expected.label} (using expected === first)", + FailedMatchData.new(description, "#{actual_label} starts with #{expected.label} (using expected === first)", expected: expected.value.inspect, actual: first.inspect, list: list.inspect ) else - SuccessfulMatchData.new + SuccessfulMatchData.new(description) end end end diff --git a/src/spectator/matchers/unordered_array_matcher.cr b/src/spectator/matchers/unordered_array_matcher.cr index 5bbbdf1..81125a7 100644 --- a/src/spectator/matchers/unordered_array_matcher.cr +++ b/src/spectator/matchers/unordered_array_matcher.cr @@ -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 From 88ed415191685299d35294b0602ba5b00a00c63d Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 2 Jan 2020 19:19:55 -0700 Subject: [PATCH 11/78] Bubble up matcher description The last run matcher's description is used if one wasn't provided by the user. Only applies to examples that actually run and have expectations. --- src/spectator/example.cr | 9 +++++++-- src/spectator/expectations/expectation.cr | 4 ++++ src/spectator/harness.cr | 1 + 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/spectator/example.cr b/src/spectator/example.cr index 3cf7051..cc955d1 100644 --- a/src/spectator/example.cr +++ b/src/spectator/example.cr @@ -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 @@ -23,11 +26,13 @@ module Spectator @test_wrapper.source end - def description : String | Symbol? - @test_wrapper.description + def description : String | Symbol + @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 diff --git a/src/spectator/expectations/expectation.cr b/src/spectator/expectations/expectation.cr index 300f134..72d3dbd 100644 --- a/src/spectator/expectations/expectation.cr +++ b/src/spectator/expectations/expectation.cr @@ -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 diff --git a/src/spectator/harness.cr b/src/spectator/harness.cr index 33ed2f0..5268edc 100644 --- a/src/spectator/harness.cr +++ b/src/spectator/harness.cr @@ -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 From 71740113d57a8c02b5e2761032ea72a7fe456571 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 2 Jan 2020 19:30:46 -0700 Subject: [PATCH 12/78] Allow subject for non-class types --- src/spectator/dsl/groups.cr | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 8153b0e..bce9215 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -19,7 +19,7 @@ module Spectator %source = ::Spectator::Source.new({{_source_file}}, {{_source_line}}) ::Spectator::SpecBuilder.start_group({{description}}, %source) - {% if what.is_a?(Path) || what.is_a?(Generic) %} + {% if (what.is_a?(Path) || what.is_a?(Generic)) && what.resolve? %} macro described_class {{what}} end @@ -27,6 +27,10 @@ module Spectator def subject(*args) described_class.new(*args) end + {% else %} + def subject + {{what}} + end {% end %} {{block.body}} From 7f8d9d5ef3fdf309d2af0513fd5b11c76633354c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 2 Jan 2020 20:00:51 -0700 Subject: [PATCH 13/78] Add comparison tests from RSpec docs --- .../expectations/comparison_matchers_spec.cr | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 spec/rspec/expectations/comparison_matchers_spec.cr diff --git a/spec/rspec/expectations/comparison_matchers_spec.cr b/spec/rspec/expectations/comparison_matchers_spec.cr new file mode 100644 index 0000000..aef4194 --- /dev/null +++ b/spec/rspec/expectations/comparison_matchers_spec.cr @@ -0,0 +1,47 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/comparison-matchers +# and modified to fit Spectator and Crystal. +Spectator.describe "Comparison matchers" do + context "numeric operator matchers" do + describe 18 do + it { is_expected.to be < 20 } + it { is_expected.to be > 15 } + it { is_expected.to be <= 19 } + it { is_expected.to be >= 17 } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.to be < 15 } + xit { is_expected.to be > 20 } + xit { is_expected.to be <= 17 } + xit { is_expected.to be >= 19 } + # it { is_expected.to be < 'a' } # Removed because Crystal doesn't support Int32#<(Char) + end + + describe 'a' do + it { is_expected.to be < 'b' } + + # deliberate failures + # TODO: Add support for expected failures. + # it { is_expected.to be < 18 } # Removed because Crystal doesn't support Char#<(Int32) + end + end + + context "string operator matchers" do + describe "Strawberry" do + it { is_expected.to be < "Tomato" } + it { is_expected.to be > "Apple" } + it { is_expected.to be <= "Turnip" } + it { is_expected.to be >= "Banana" } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.to be < "Cranberry" } + xit { is_expected.to be > "Zuchini" } + xit { is_expected.to be <= "Potato" } + xit { is_expected.to be >= "Tomato" } + end + end +end From 4c9d76713762780aa89f4b1f3503c5308a7eb07e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 4 Jan 2020 09:32:56 -0700 Subject: [PATCH 14/78] Add missing description arguments --- src/spectator/matchers/have_predicate_matcher.cr | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/spectator/matchers/have_predicate_matcher.cr b/src/spectator/matchers/have_predicate_matcher.cr index 93bcca6..c8d2ad1 100644 --- a/src/spectator/matchers/have_predicate_matcher.cr +++ b/src/spectator/matchers/have_predicate_matcher.cr @@ -25,9 +25,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 +36,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 From c4a97230f17db2485c34979cc72b5b352476a9e0 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 4 Jan 2020 09:33:14 -0700 Subject: [PATCH 15/78] Remove unecessary members provided by ValueMatcher --- src/spectator/matchers/have_predicate_matcher.cr | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/spectator/matchers/have_predicate_matcher.cr b/src/spectator/matchers/have_predicate_matcher.cr index c8d2ad1..7b23cd0 100644 --- a/src/spectator/matchers/have_predicate_matcher.cr +++ b/src/spectator/matchers/have_predicate_matcher.cr @@ -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. From 34060ce1811285828110c05a4ab3a92d64ba4d52 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 4 Jan 2020 09:49:28 -0700 Subject: [PATCH 16/78] Ensure top-level Crystal types are used --- src/spectator/dsl/matchers.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spectator/dsl/matchers.cr b/src/spectator/dsl/matchers.cr index 0e779c3..5d3e2ae 100644 --- a/src/spectator/dsl/matchers.cr +++ b/src/spectator/dsl/matchers.cr @@ -716,8 +716,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 %} From 2538f3a9a4eb975350c1e9f5c930057835c77b5f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 4 Jan 2020 09:58:53 -0700 Subject: [PATCH 17/78] Add RSpec predicate matchers spec --- .../expectations/predicate_matchers_spec.cr | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 spec/rspec/expectations/predicate_matchers_spec.cr diff --git a/spec/rspec/expectations/predicate_matchers_spec.cr b/spec/rspec/expectations/predicate_matchers_spec.cr new file mode 100644 index 0000000..f2dac5c --- /dev/null +++ b/spec/rspec/expectations/predicate_matchers_spec.cr @@ -0,0 +1,86 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/predicate-matchers +# and modified to fit Spectator and Crystal. +Spectator.describe "Predicate matchers" do + context "should be_zero (based on Int#zero?)" do + describe 0 do + it { is_expected.to be_zero } + end + + describe 7 do + # deliberate failure + # TODO: Add support for expected failures. + xit { is_expected.to be_zero } + end + end + + context "should_not be_empty (based on Array#empty?)" do + describe [1, 2, 3] do + it { is_expected.not_to be_empty } + end + + describe [] of Int32 do + # deliberate failure + # TODO: Add support for expected failures. + xit { is_expected.not_to be_empty } + end + end + + context "should have_key (based on Hash#has_key?)" do + describe Hash do + subject { {:foo => 7} } + + it { is_expected.to have_key(:foo) } + + # deliberate failure + # TODO: Add support for expected failures. + xit { is_expected.to have_key(:bar) } + end + end + + context "should_not have_all_string_keys (based on custom #has_all_string_keys? method)" do + class ::Hash(K, V) + def has_all_string_keys? + keys.all? { |k| String === k } + end + end + + describe Hash do + context "with symbol keys" do + subject { {:foo => 7, :bar => 5} } + + it { is_expected.not_to have_all_string_keys } + end + + context "with string keys" do + subject { {"foo" => 7, "bar" => 5} } + + # deliberate failure + # TODO: Add support for expected failures. + xit { is_expected.not_to have_all_string_keys } + end + end + end + + context "matcher arguments are passed on to the predicate method" do + struct ::Int + def multiple_of?(x) + (self % x).zero? + end + end + + describe 12 do + it { is_expected.to be_multiple_of(3) } + it { is_expected.not_to be_multiple_of(7) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to be_multiple_of(4) } + xit { is_expected.to be_multiple_of(5) } + end + end + + # The examples using private methods cause a compilation error in Crystal, and can't be used here. +end From c3688807d425f76c8969e86ec34e413a18349017 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 4 Jan 2020 10:08:01 -0700 Subject: [PATCH 18/78] Add kind_of aliases for be_a matcher --- src/spectator/dsl/matchers.cr | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/spectator/dsl/matchers.cr b/src/spectator/dsl/matchers.cr index 5d3e2ae..f37ada2 100644 --- a/src/spectator/dsl/matchers.cr +++ b/src/spectator/dsl/matchers.cr @@ -94,6 +94,34 @@ 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 respond to a method call. # One or more method names can be provided. # From 8cfed440ed8e10dc2666a3f2d08c7d9e6f31b2d0 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 4 Jan 2020 12:45:49 -0700 Subject: [PATCH 19/78] Fix typo with be_a matcher --- src/spectator/matchers/type_matcher.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/matchers/type_matcher.cr b/src/spectator/matchers/type_matcher.cr index 4b7aff3..f8401d4 100644 --- a/src/spectator/matchers/type_matcher.cr +++ b/src/spectator/matchers/type_matcher.cr @@ -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. From 93c442d1e235235ff5b03a5baf12972044beb466 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 4 Jan 2020 12:46:08 -0700 Subject: [PATCH 20/78] Add instance_of matcher to check exact type --- src/spectator/dsl/matchers.cr | 26 ++++++++++ src/spectator/matchers/instance_matcher.cr | 57 ++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 src/spectator/matchers/instance_matcher.cr diff --git a/src/spectator/dsl/matchers.cr b/src/spectator/dsl/matchers.cr index f37ada2..4eb83e8 100644 --- a/src/spectator/dsl/matchers.cr +++ b/src/spectator/dsl/matchers.cr @@ -122,6 +122,32 @@ module Spectator 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. # diff --git a/src/spectator/matchers/instance_matcher.cr b/src/spectator/matchers/instance_matcher.cr new file mode 100644 index 0000000..c77531e --- /dev/null +++ b/src/spectator/matchers/instance_matcher.cr @@ -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 From 42ef2cc904da1350349776d279486f90e1fe9a9b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 4 Jan 2020 12:47:23 -0700 Subject: [PATCH 21/78] Add RSpec type matchers spec --- spec/rspec/expectations/type_matchers_spec.cr | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 spec/rspec/expectations/type_matchers_spec.cr diff --git a/spec/rspec/expectations/type_matchers_spec.cr b/spec/rspec/expectations/type_matchers_spec.cr new file mode 100644 index 0000000..c2b1a2d --- /dev/null +++ b/spec/rspec/expectations/type_matchers_spec.cr @@ -0,0 +1,101 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/type-matchers +# and modified to fit Spectator and Crystal. +Spectator.describe "Type matchers" do + context "be_(a_)kind_of matcher" do + # The docs use Float as an example. + # This doesn't work with the Crystal compiler, + # so a custom hierarchy is used instead. + # "Error: can't use Number as generic type argument yet, use a more specific type" + + module MyModule; end + + class Base; end + + class Derived < Base + include MyModule + end + + describe Derived do + # the actual class + it { is_expected.to be_kind_of(Derived) } + it { is_expected.to be_a_kind_of(Derived) } + it { is_expected.to be_a(Derived) } + + # the superclass + it { is_expected.to be_kind_of(Base) } + it { is_expected.to be_a_kind_of(Base) } + it { is_expected.to be_an(Base) } + + # an included module + it { is_expected.to be_kind_of(MyModule) } + it { is_expected.to be_a_kind_of(MyModule) } + it { is_expected.to be_a(MyModule) } + + # negative passing case + it { is_expected.not_to be_kind_of(String) } + it { is_expected.not_to be_a_kind_of(String) } + it { is_expected.not_to be_a(String) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to be_kind_of(Derived) } + xit { is_expected.not_to be_a_kind_of(Derived) } + xit { is_expected.not_to be_a(Derived) } + xit { is_expected.not_to be_kind_of(Base) } + xit { is_expected.not_to be_a_kind_of(Base) } + xit { is_expected.not_to be_an(Base) } + xit { is_expected.not_to be_kind_of(MyModule) } + xit { is_expected.not_to be_a_kind_of(MyModule) } + xit { is_expected.not_to be_a(MyModule) } + xit { is_expected.to be_kind_of(String) } + xit { is_expected.to be_a_kind_of(String) } + xit { is_expected.to be_a(String) } + end + + context "be_(an_)instance_of matcher" do + # The docs use Float as an example. + # This doesn't work with the Crystal compiler, + # so a custom hierarchy is used instead. + # "Error: can't use Number as generic type argument yet, use a more specific type" + + module MyModule; end + + class Base; end + + class Derived < Base + include MyModule + end + + describe Derived do + # the actual class + it { is_expected.to be_instance_of(Derived) } + it { is_expected.to be_an_instance_of(Derived) } + + # the superclass + it { is_expected.not_to be_instance_of(Base) } + it { is_expected.not_to be_an_instance_of(Base) } + + # an included module + it { is_expected.not_to be_instance_of(MyModule) } + it { is_expected.not_to be_an_instance_of(MyModule) } + + # another class with no relation to the subject's hierarchy + it { is_expected.not_to be_instance_of(String) } + it { is_expected.not_to be_an_instance_of(String) } + + # deliberate failures + xit { is_expected.not_to be_instance_of(Derived) } + xit { is_expected.not_to be_an_instance_of(Derived) } + xit { is_expected.to be_instance_of(Base) } + xit { is_expected.to be_an_instance_of(Base) } + xit { is_expected.to be_instance_of(MyModule) } + xit { is_expected.to be_an_instance_of(MyModule) } + xit { is_expected.to be_instance_of(String) } + xit { is_expected.to be_an_instance_of(String) } + end + end + end +end From 80c4e97c29670f3fe7d46e558e1452421d426867 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 5 Jan 2020 00:12:57 -0700 Subject: [PATCH 22/78] Add RSpec `all` matchers spec --- spec/rspec/expectations/all_matcher_spec.cr | 39 +++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 spec/rspec/expectations/all_matcher_spec.cr diff --git a/spec/rspec/expectations/all_matcher_spec.cr b/spec/rspec/expectations/all_matcher_spec.cr new file mode 100644 index 0000000..35ad484 --- /dev/null +++ b/spec/rspec/expectations/all_matcher_spec.cr @@ -0,0 +1,39 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/all-matcher +# and modified to fit Spectator and Crystal. +Spectator.describe "`all` matcher" do + context "array usage" do + describe [1, 3, 5] do + it { is_expected.to all(be_odd) } + it { is_expected.to all(be_an(Int32)) } # Changed to Int32 to satisfy compiler. + it { is_expected.to all(be < 10) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.to all(be_even) } + xit { is_expected.to all(be_a(String)) } + xit { is_expected.to all(be > 2) } + end + end + + context "compound matcher usage" do + # Changed `include` to `contain` to match our own. + # `include` is a keyword and can't be used as a method name in Crystal. + + # TODO: Add support for compound matchers. + describe ["anything", "everything", "something"] do + xit { is_expected.to all(be_a(String)) } # .and contain("thing") ) } + xit { is_expected.to all(be_a(String)) } # .and end_with("g") ) } + xit { is_expected.to all(start_with("s")) } # .or contain("y") ) } + + # deliberate failures + # TODO: Add support for expected failures. + # TODO: Add support for compound matchers. + xit { is_expected.to all(contain("foo")) } # .and contain("bar") ) } + xit { is_expected.to all(be_a(String)) } # .and start_with("a") ) } + xit { is_expected.to all(start_with("a")) } # .or contain("z") ) } + end + end +end From 1f7ac79c785f227fb8919ecd180732bb53b6a987 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 5 Jan 2020 00:29:10 -0700 Subject: [PATCH 23/78] Add RSpec `be` matchers spec --- spec/rspec/expectations/be_matchers_spec.cr | 70 +++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 spec/rspec/expectations/be_matchers_spec.cr diff --git a/spec/rspec/expectations/be_matchers_spec.cr b/spec/rspec/expectations/be_matchers_spec.cr new file mode 100644 index 0000000..79636f2 --- /dev/null +++ b/spec/rspec/expectations/be_matchers_spec.cr @@ -0,0 +1,70 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/be-matchers +# and modified to fit Spectator and Crystal. +Spectator.describe "`be` matchers" do + context "be_truthy matcher" do + specify { expect(true).to be_truthy } + specify { expect(7).to be_truthy } + specify { expect("foo").to be_truthy } + specify { expect(nil).not_to be_truthy } + specify { expect(false).not_to be_truthy } + + # deliberate failures + # TODO: Add support for expected failures. + pending { expect(true).not_to be_truthy } + pending { expect(7).not_to be_truthy } + pending { expect("foo").not_to be_truthy } + pending { expect(nil).to be_truthy } + pending { expect(false).to be_truthy } + end + + context "be_falsey matcher" do + specify { expect(nil).to be_falsey } + specify { expect(false).to be_falsey } + specify { expect(true).not_to be_falsey } + specify { expect(7).not_to be_falsey } + specify { expect("foo").not_to be_falsey } + + # deliberate failures + # TODO: Add support for expected failures. + pending { expect(nil).not_to be_falsey } + pending { expect(false).not_to be_falsey } + pending { expect(true).to be_falsey } + pending { expect(7).to be_falsey } + pending { expect("foo").to be_falsey } + end + + context "be_nil matcher" do + specify { expect(nil).to be_nil } + specify { expect(false).not_to be_nil } + specify { expect(true).not_to be_nil } + specify { expect(7).not_to be_nil } + specify { expect("foo").not_to be_nil } + + # deliberate failures + # TODO: Add support for expected failures. + pending { expect(nil).not_to be_nil } + pending { expect(false).to be_nil } + pending { expect(true).to be_nil } + pending { expect(7).to be_nil } + pending { expect("foo").to be_nil } + end + + context "be matcher" do + specify { expect(true).to be } + specify { expect(7).to be } + specify { expect("foo").to be } + specify { expect(nil).not_to be } + specify { expect(false).not_to be } + + # deliberate failures + # TODO: Add support for expected failures. + pending { expect(true).not_to be } + pending { expect(7).not_to be } + pending { expect("foo").not_to be } + pending { expect(nil).to be } + pending { expect(false).to be } + end +end From 5987574a0ea79c9c7b0494273df6be3ccf997c66 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 5 Jan 2020 00:42:15 -0700 Subject: [PATCH 24/78] Add RSpec `be_within` matcher spec --- .../expectations/be_within_matcher_spec.cr | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 spec/rspec/expectations/be_within_matcher_spec.cr diff --git a/spec/rspec/expectations/be_within_matcher_spec.cr b/spec/rspec/expectations/be_within_matcher_spec.cr new file mode 100644 index 0000000..0307d1b --- /dev/null +++ b/spec/rspec/expectations/be_within_matcher_spec.cr @@ -0,0 +1,25 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/be-within-matcher +# and modified to fit Spectator and Crystal. +Spectator.describe "`be_within` matcher" do + context "basic usage" do + describe 27.5 do + it { is_expected.to be_within(0.5).of(27.9) } + it { is_expected.to be_within(0.5).of(28.0) } + it { is_expected.to be_within(0.5).of(27.1) } + it { is_expected.to be_within(0.5).of(27.0) } + + it { is_expected.not_to be_within(0.5).of(28.1) } + it { is_expected.not_to be_within(0.5).of(26.9) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to be_within(0.5).of(28) } + xit { is_expected.not_to be_within(0.5).of(27) } + xit { is_expected.to be_within(0.5).of(28.1) } + xit { is_expected.to be_within(0.5).of(26.9) } + end + end +end From e17435f6e87b6e3aae3b4fbc9821c15877144e20 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 5 Jan 2020 10:28:21 -0700 Subject: [PATCH 25/78] Add RSpec `be_within` matcher spec --- .../expectations/be_within_matcher_spec.cr | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 spec/rspec/expectations/be_within_matcher_spec.cr diff --git a/spec/rspec/expectations/be_within_matcher_spec.cr b/spec/rspec/expectations/be_within_matcher_spec.cr new file mode 100644 index 0000000..0307d1b --- /dev/null +++ b/spec/rspec/expectations/be_within_matcher_spec.cr @@ -0,0 +1,25 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/be-within-matcher +# and modified to fit Spectator and Crystal. +Spectator.describe "`be_within` matcher" do + context "basic usage" do + describe 27.5 do + it { is_expected.to be_within(0.5).of(27.9) } + it { is_expected.to be_within(0.5).of(28.0) } + it { is_expected.to be_within(0.5).of(27.1) } + it { is_expected.to be_within(0.5).of(27.0) } + + it { is_expected.not_to be_within(0.5).of(28.1) } + it { is_expected.not_to be_within(0.5).of(26.9) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to be_within(0.5).of(28) } + xit { is_expected.not_to be_within(0.5).of(27) } + xit { is_expected.to be_within(0.5).of(28.1) } + xit { is_expected.to be_within(0.5).of(26.9) } + end + end +end From 26656b7c127a4f805a358d48d0574203900168a1 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 5 Jan 2020 10:35:35 -0700 Subject: [PATCH 26/78] Add RSpec `change` matcher spec --- .../rspec/expectations/change_matcher_spec.cr | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 spec/rspec/expectations/change_matcher_spec.cr diff --git a/spec/rspec/expectations/change_matcher_spec.cr b/spec/rspec/expectations/change_matcher_spec.cr new file mode 100644 index 0000000..c59423e --- /dev/null +++ b/spec/rspec/expectations/change_matcher_spec.cr @@ -0,0 +1,49 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/change-matcher +# and modified to fit Spectator and Crystal. +Spectator.describe "`change` matcher" do + # Modified this example type to work in Crystal. + module Counter + extend self + + @@count = 0 + + def increment + @@count += 1 + end + + def count + @@count + end + end + + context "expect change" do + describe "Counter#increment" do # TODO: Allow multiple arguments to context/describe. + it "should increment the count" do + expect { Counter.increment }.to change { Counter.count }.from(0).to(1) + end + + # deliberate failure + # TODO: Add support for expected failures. + xit "should increment the count by 2" do + expect { Counter.increment }.to change { Counter.count }.by(2) + end + end + end + + context "expect no change" do + describe "Counter#increment" do # TODO: Allow multiple arguments to context/describe. + # deliberate failures + # TODO: Add support for expected failures. + xit "should not increment the count by 1 (using not_to)" do + expect { Counter.increment }.not_to change { Counter.count } + end + + xit "should not increment the count by 1 (using to_not)" do + expect { Counter.increment }.to_not change { Counter.count } + end + end + end +end From 034c1cd6cb4f5105ea6f6661db3b5de8effa0e22 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 5 Jan 2020 11:42:39 -0700 Subject: [PATCH 27/78] Change array matcher to allow any order --- src/spectator/dsl/matchers.cr | 9 +- src/spectator/matchers/array_matcher.cr | 109 ++++++++++++------------ 2 files changed, 59 insertions(+), 59 deletions(-) diff --git a/src/spectator/dsl/matchers.cr b/src/spectator/dsl/matchers.cr index 4eb83e8..784426a 100644 --- a/src/spectator/dsl/matchers.cr +++ b/src/spectator/dsl/matchers.cr @@ -520,22 +520,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}}) diff --git a/src/spectator/matchers/array_matcher.cr b/src/spectator/matchers/array_matcher.cr index 8144db5..92fb01a 100644 --- a/src/spectator/matchers/array_matcher.cr +++ b/src/spectator/matchers/array_matcher.cr @@ -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. + if missing.empty? && extra.empty? + # Contents are identical. SuccessfulMatchData.new(description) - else # Size differs. - failed_size_mismatch(expected_elements, actual_elements, actual.label) + 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,13 +46,16 @@ 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(description) - when true # Contents are identical. - failed_content_identical(expected_elements, actual_elements, actual.label) - else # Size differs. + if missing.empty? && extra.empty? + # Contents are identical. + FailedMatchData.new(description, "#{actual.label} contains exactly #{expected.label}", + expected: "Not #{expected_elements.inspect}", + actual: actual_elements.inspect + ) + else + # Content differs. SuccessfulMatchData.new(description) end end @@ -65,49 +72,41 @@ module Spectator::Matchers UnorderedArrayMatcher.new(expected) end - # Compares two arrays to determine whether they contain the same elements, and in the same order. - # If the arrays are the same, then `true` is returned. - # If they are different, `false` or an integer is returned. - # `false` is returned when the sizes of the arrays don't match. - # An integer is returned, that is the index of the mismatched elements in the arrays. + # Compares two arrays to determine whether they contain the same elements, but in any order. + # A tuple of two arrays is returned. + # The first array is the missing elements (present in expected, missing in actual). + # The second array array is the extra elements (not present in expected, present in actual). private def compare_arrays(expected_elements, actual_elements) - if expected_elements.size == actual_elements.size - index = 0 - expected_elements.zip(actual_elements) do |expected_element, actual_element| - return index unless expected_element == actual_element - index += 1 + # Produce hashes where the array elements are the keys, and the values are the number of occurances. + expected_hash = expected_elements.group_by(&.itself).map { |k, v| {k, v.size} }.to_h + actual_hash = actual_elements.group_by(&.itself).map { |k, v| {k, v.size} }.to_h + + { + hash_count_difference(expected_hash, actual_hash), + hash_count_difference(actual_hash, expected_hash), + } + end + + # Expects two hashes, with values as counts for keys. + # Produces an array of differences with elements repeated if needed. + private def hash_count_difference(first, second) + # Subtract the number of occurances from the other array. + # A duplicate hash is used here because the original can't be modified, + # since it there's a two-way comparison. + # + # Then reject elements that have zero (or less) occurances. + # Lastly, expand to the correct number of elements. + first.map do |element, count| + if second_count = second[element]? + {element, count - second_count} + else + {element, count} end - true - else - false - end - end - - # Produces match data for a failure when the array sizes differ. - private def failed_size_mismatch(expected_elements, actual_elements, actual_label) - FailedMatchData.new(description, "#{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(description, "#{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(description, "#{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 From 5bbaad36d23f294667e647ebb7f50aa385f6808e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 5 Jan 2020 11:42:43 -0700 Subject: [PATCH 28/78] Add RSpec `contain_exactly` matcher spec --- .../contain_exactly_matcher_spec.cr | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 spec/rspec/expectations/contain_exactly_matcher_spec.cr diff --git a/spec/rspec/expectations/contain_exactly_matcher_spec.cr b/spec/rspec/expectations/contain_exactly_matcher_spec.cr new file mode 100644 index 0000000..14d3c33 --- /dev/null +++ b/spec/rspec/expectations/contain_exactly_matcher_spec.cr @@ -0,0 +1,32 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/contain-exactly-matcher +# and modified to fit Spectator and Crystal. +Spectator.describe "`contain_exactly` matcher" do + context "Array is expected to contain every value" do + describe [1, 2, 3] do + it { is_expected.to contain_exactly(1, 2, 3) } + it { is_expected.to contain_exactly(1, 3, 2) } + it { is_expected.to contain_exactly(2, 1, 3) } + it { is_expected.to contain_exactly(2, 3, 1) } + it { is_expected.to contain_exactly(3, 1, 2) } + it { is_expected.to contain_exactly(3, 2, 1) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.to contain_exactly(1, 2, 1) } + end + end + + context "Array is not expected to contain every value" do + describe [1, 2, 3] do + it { is_expected.to_not contain_exactly(1, 2, 3, 4) } + it { is_expected.to_not contain_exactly(1, 2) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.to_not contain_exactly(1, 3, 2) } + end + end +end From 8143229fdb8242d58de938b876b77cbaf37eb169 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 5 Jan 2020 22:47:57 -0700 Subject: [PATCH 29/78] Fix failure message --- src/spectator/matchers/contain_matcher.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/matchers/contain_matcher.cr b/src/spectator/matchers/contain_matcher.cr index 26d3287..edecfbd 100644 --- a/src/spectator/matchers/contain_matcher.cr +++ b/src/spectator/matchers/contain_matcher.cr @@ -25,7 +25,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. From b7c686e836c31d0a54f2978642220959fed051f4 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 5 Jan 2020 22:49:27 -0700 Subject: [PATCH 30/78] Add `cover` matcher Works the same as `contain` but is for ranges. --- src/spectator/dsl/matchers.cr | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/spectator/dsl/matchers.cr b/src/spectator/dsl/matchers.cr index 784426a..2108a88 100644 --- a/src/spectator/dsl/matchers.cr +++ b/src/spectator/dsl/matchers.cr @@ -454,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. From 56731445273bccf2b6df8023cc3c04ce2b4bc3ed Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 5 Jan 2020 23:01:25 -0700 Subject: [PATCH 31/78] Fix negative match case for contain/cover matcher --- src/spectator/matchers/contain_matcher.cr | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/spectator/matchers/contain_matcher.cr b/src/spectator/matchers/contain_matcher.cr index edecfbd..c009851 100644 --- a/src/spectator/matchers/contain_matcher.cr +++ b/src/spectator/matchers/contain_matcher.cr @@ -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. From 7868755eee6008470f712f9e863fea80b1cc2d3a Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 5 Jan 2020 23:02:09 -0700 Subject: [PATCH 32/78] Add RSpec `cover` matcher spec --- spec/rspec/expectations/cover_matcher_spec.cr | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 spec/rspec/expectations/cover_matcher_spec.cr diff --git a/spec/rspec/expectations/cover_matcher_spec.cr b/spec/rspec/expectations/cover_matcher_spec.cr new file mode 100644 index 0000000..fb1a74a --- /dev/null +++ b/spec/rspec/expectations/cover_matcher_spec.cr @@ -0,0 +1,30 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/cover-matcher +# and modified to fit Spectator and Crystal. +Spectator.describe "`cover` matcher" do + context "range usage" do + describe (1..10) do + it { is_expected.to cover(4) } + it { is_expected.to cover(6) } + it { is_expected.to cover(8) } + it { is_expected.to cover(4, 6) } + it { is_expected.to cover(4, 6, 8) } + it { is_expected.not_to cover(11) } + it { is_expected.not_to cover(11, 12) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.to cover(11) } + xit { is_expected.not_to cover(4) } + xit { is_expected.not_to cover(6) } + xit { is_expected.not_to cover(8) } + xit { is_expected.not_to cover(4, 6, 8) } + + # both of these should fail since it covers 5 but not 11 + xit { is_expected.to cover(5, 11) } + xit { is_expected.not_to cover(5, 11) } + end + end +end From 590d81979eeef0eb0c852d0651c9a989681c8a5b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 6 Jan 2020 22:04:05 -0700 Subject: [PATCH 33/78] Workaround typing issues --- src/spectator/matchers/end_with_matcher.cr | 24 +++++++++++--------- src/spectator/matchers/start_with_matcher.cr | 6 +++-- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/spectator/matchers/end_with_matcher.cr b/src/spectator/matchers/end_with_matcher.cr index 4d77a36..f555153 100644 --- a/src/spectator/matchers/end_with_matcher.cr +++ b/src/spectator/matchers/end_with_matcher.cr @@ -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 @@ -72,11 +74,11 @@ 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(description, "#{actual.label} ends with #{expected.label} (using #ends_with?)", + 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: expected.value.inspect, - actual: actual.value.inspect + actual: actual_value.inspect ) else SuccessfulMatchData.new(description) @@ -85,12 +87,12 @@ module Spectator::Matchers # 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(description, "#{actual.label} ends with #{expected.label} (using expected === last)", + FailedMatchData.new(description, "#{actual_label} ends with #{expected.label} (using expected === last)", expected: expected.value.inspect, actual: last.inspect, list: list.inspect diff --git a/src/spectator/matchers/start_with_matcher.cr b/src/spectator/matchers/start_with_matcher.cr index aaa8846..02060e7 100644 --- a/src/spectator/matchers/start_with_matcher.cr +++ b/src/spectator/matchers/start_with_matcher.cr @@ -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) From 5fa6b5d5494070a3497102490da144ec962f050b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 6 Jan 2020 22:11:36 -0700 Subject: [PATCH 34/78] Fix negation expectation text --- src/spectator/matchers/end_with_matcher.cr | 4 ++-- src/spectator/matchers/start_with_matcher.cr | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/spectator/matchers/end_with_matcher.cr b/src/spectator/matchers/end_with_matcher.cr index f555153..319c559 100644 --- a/src/spectator/matchers/end_with_matcher.cr +++ b/src/spectator/matchers/end_with_matcher.cr @@ -77,7 +77,7 @@ module Spectator::Matchers 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: expected.value.inspect, + expected: "Not #{expected.value.inspect}", actual: actual_value.inspect ) else @@ -93,7 +93,7 @@ module Spectator::Matchers if expected.value === last FailedMatchData.new(description, "#{actual_label} ends with #{expected.label} (using expected === last)", - expected: expected.value.inspect, + expected: "Not #{expected.value.inspect}", actual: last.inspect, list: list.inspect ) diff --git a/src/spectator/matchers/start_with_matcher.cr b/src/spectator/matchers/start_with_matcher.cr index 02060e7..b459bb4 100644 --- a/src/spectator/matchers/start_with_matcher.cr +++ b/src/spectator/matchers/start_with_matcher.cr @@ -76,7 +76,7 @@ module Spectator::Matchers private def negated_match_starts_with(actual_value, actual_label) if actual_value.starts_with?(expected.value) FailedMatchData.new(description, "#{actual_label} starts with #{expected.label} (using #starts_with?)", - expected: expected.value.inspect, + expected: "Not #{expected.value.inspect}", actual: actual_value.inspect ) else @@ -92,7 +92,7 @@ module Spectator::Matchers if expected.value === first FailedMatchData.new(description, "#{actual_label} starts with #{expected.label} (using expected === first)", - expected: expected.value.inspect, + expected: "Not #{expected.value.inspect}", actual: first.inspect, list: list.inspect ) From f23141b3e164020dc8c7f4406c7e5a840773ae4c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 6 Jan 2020 22:19:09 -0700 Subject: [PATCH 35/78] Add RSpec `start_with` and `end_with` matchers specs --- .../expectations/end_with_matcher_spec.cr | 33 +++++++++++++++++++ .../expectations/start_with_matcher_spec.cr | 33 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 spec/rspec/expectations/end_with_matcher_spec.cr create mode 100644 spec/rspec/expectations/start_with_matcher_spec.cr diff --git a/spec/rspec/expectations/end_with_matcher_spec.cr b/spec/rspec/expectations/end_with_matcher_spec.cr new file mode 100644 index 0000000..c19720f --- /dev/null +++ b/spec/rspec/expectations/end_with_matcher_spec.cr @@ -0,0 +1,33 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/end-with-matcher +# and modified to fit Spectator and Crystal. +Spectator.describe "`end_with` matcher" do + context "string usage" do + describe "this string" do + it { is_expected.to end_with "string" } + it { is_expected.not_to end_with "stringy" } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to end_with "string" } + xit { is_expected.to end_with "stringy" } + end + end + + context "array usage" do + describe [0, 1, 2, 3, 4] do + it { is_expected.to end_with 4 } + # TODO: Add support for multiple items at the end of an array. + # it { is_expected.to end_with 3, 4 } + it { is_expected.not_to end_with 3 } + # it { is_expected.not_to end_with 0, 1, 2, 3, 4, 5 } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to end_with 4 } + xit { is_expected.to end_with 3 } + end + end +end diff --git a/spec/rspec/expectations/start_with_matcher_spec.cr b/spec/rspec/expectations/start_with_matcher_spec.cr new file mode 100644 index 0000000..c2942cc --- /dev/null +++ b/spec/rspec/expectations/start_with_matcher_spec.cr @@ -0,0 +1,33 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/start-with-matcher +# and modified to fit Spectator and Crystal. +Spectator.describe "`start_with` matcher" do + context "with a string" do + describe "this string" do + it { is_expected.to start_with "this" } + it { is_expected.not_to start_with "that" } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to start_with "this" } + xit { is_expected.to start_with "that" } + end + end + + context "with an array" do + describe [0, 1, 2, 3, 4] do + it { is_expected.to start_with 0 } + # TODO: Add support for multiple items at the beginning of an array. + # it { is_expected.to start_with(0, 1) } + it { is_expected.not_to start_with(2) } + # it { is_expected.not_to start_with(0, 1, 2, 3, 4, 5) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to start_with 0 } + xit { is_expected.to start_with 3 } + end + end +end From 6ad861365c73ac4616b1f98316d80b960b0e3fbc Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 6 Jan 2020 22:33:52 -0700 Subject: [PATCH 36/78] Add RSpec `have_attributes` matcher spec --- .../have_attributes_matcher_spec.cr | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 spec/rspec/expectations/have_attributes_matcher_spec.cr diff --git a/spec/rspec/expectations/have_attributes_matcher_spec.cr b/spec/rspec/expectations/have_attributes_matcher_spec.cr new file mode 100644 index 0000000..dfa273b --- /dev/null +++ b/spec/rspec/expectations/have_attributes_matcher_spec.cr @@ -0,0 +1,37 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/have-attributes-matcher +# and modified to fit Spectator and Crystal. +Spectator.describe "`have_attributes` matcher" do + context "basic usage" do + # Use `record` instead of `Struct.new`. + record Person, name : String, age : Int32 + + describe Person.new("Jim", 32) do + # Changed some syntax for Ruby hashes to Crystal named tuples. + + # Spectator doesn't support helper matchers like `a_string_starting_with` and `a_value <`. + # But maybe in the future it will. + it { is_expected.to have_attributes(name: "Jim") } + # it { is_expected.to have_attributes(name: a_string_starting_with("J") ) } + it { is_expected.to have_attributes(age: 32) } + # it { is_expected.to have_attributes(age: (a_value > 30) ) } + it { is_expected.to have_attributes(name: "Jim", age: 32) } + # it { is_expected.to have_attributes(name: a_string_starting_with("J"), age: (a_value > 30) ) } + it { is_expected.not_to have_attributes(name: "Bob") } + it { is_expected.not_to have_attributes(age: 10) } + # it { is_expected.not_to have_attributes(age: (a_value < 30) ) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.to have_attributes(name: "Bob") } + xit { is_expected.to have_attributes(name: 10) } + + # fails if any of the attributes don't match + xit { is_expected.to have_attributes(name: "Bob", age: 32) } + xit { is_expected.to have_attributes(name: "Jim", age: 10) } + xit { is_expected.to have_attributes(name: "Bob", age: 10) } + end + end +end From b43351120197dde0fd95c711d525b590b7573c2c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 6 Jan 2020 22:51:47 -0700 Subject: [PATCH 37/78] Add RSpec `include/contain` matcher spec --- .../expectations/contain_matcher_spec.cr | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 spec/rspec/expectations/contain_matcher_spec.cr diff --git a/spec/rspec/expectations/contain_matcher_spec.cr b/spec/rspec/expectations/contain_matcher_spec.cr new file mode 100644 index 0000000..a75897c --- /dev/null +++ b/spec/rspec/expectations/contain_matcher_spec.cr @@ -0,0 +1,99 @@ +require "../../spec_helper" + +# In Ruby, this is the `include` matcher. +# However, `include` is a reserved keyword in Crystal. +# So instead, it is `contain` in Spectator. +Spectator.describe "`contain` matcher" do + context "array usage" do + describe [1, 3, 7] do + it { is_expected.to contain(1) } + it { is_expected.to contain(3) } + it { is_expected.to contain(7) } + it { is_expected.to contain(1, 7) } + it { is_expected.to contain(1, 3, 7) } + + # Utility matcher method `a_kind_of` is not supported. + # it { is_expected.to contain(a_kind_of(Int)) } + + # TODO: Compound matchers aren't supported. + # it { is_expected.to contain(be_odd.and be < 10) } + + # TODO: Fix behavior and cleanup output. + # This syntax is allowed, but produces a wrong result and bad output. + xit { is_expected.to contain(be_odd) } + xit { is_expected.not_to contain(be_even) } + + it { is_expected.not_to contain(17) } + it { is_expected.not_to contain(43, 100) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.to contain(4) } + xit { is_expected.to contain(be_even) } + xit { is_expected.not_to contain(1) } + xit { is_expected.not_to contain(3) } + xit { is_expected.not_to contain(7) } + xit { is_expected.not_to contain(1, 3, 7) } + + # both of these should fail since it contains 1 but not 9 + xit { is_expected.to contain(1, 9) } + xit { is_expected.not_to contain(1, 9) } + end + end + + context "string usage" do + describe "a string" do + it { is_expected.to contain("str") } + it { is_expected.to contain("a", "str", "ng") } + it { is_expected.not_to contain("foo") } + it { is_expected.not_to contain("foo", "bar") } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.to contain("foo") } + xit { is_expected.not_to contain("str") } + xit { is_expected.to contain("str", "foo") } + xit { is_expected.not_to contain("str", "foo") } + end + end + + context "hash usage" do + # A hash can't be described inline here for some reason. + # So it is placed in the subject instead. + describe ":a => 7, :b => 5" do + subject { {:a => 7, :b => 5} } + + # Hash syntax is changed here from `:a => 7` to `a: 7`. + xit { is_expected.to contain(:a) } + xit { is_expected.to contain(:b, :a) } + + # TODO: This hash-like syntax isn't supported. + # it { is_expected.to contain(a: 7) } + # it { is_expected.to contain(b: 5, a: 7) } + xit { is_expected.not_to contain(:c) } + xit { is_expected.not_to contain(:c, :d) } + # it { is_expected.not_to contain(d: 2) } + # it { is_expected.not_to contain(a: 5) } + # it { is_expected.not_to contain(b: 7, a: 5) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to contain(:a) } + xit { is_expected.not_to contain(:b, :a) } + # it { is_expected.not_to contain(a: 7) } + # it { is_expected.not_to contain(a: 7, b: 5) } + xit { is_expected.to contain(:c) } + xit { is_expected.to contain(:c, :d) } + # it { is_expected.to contain(d: 2) } + # it { is_expected.to contain(a: 5) } + # it { is_expected.to contain(a: 5, b: 7) } + + # Mixed cases--the hash contains one but not the other. + # All 4 of these cases should fail. + xit { is_expected.to contain(:a, :d) } + xit { is_expected.not_to contain(:a, :d) } + # it { is_expected.to contain(a: 7, d: 3) } + # it { is_expected.not_to contain(a: 7, d: 3) } + end + end +end From f11b548f4e79a1cf283d99b57b3345ca169fa7aa Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 6 Jan 2020 23:01:45 -0700 Subject: [PATCH 38/78] Mimic RSpec behavior of `match` matcher The code: expect(/foo/).to match("food") would normally evaluate: "food" === /foo/ which is false. However, in RSpec, this expectation is true. --- src/spectator/matchers/case_matcher.cr | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/spectator/matchers/case_matcher.cr b/src/spectator/matchers/case_matcher.cr index e7f0891..fcb5c23 100644 --- a/src/spectator/matchers/case_matcher.cr +++ b/src/spectator/matchers/case_matcher.cr @@ -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. From 6a0a73ca7674cf0519b1a9d81aee3f0dc8ead880 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 6 Jan 2020 23:05:31 -0700 Subject: [PATCH 39/78] Add RSpec `match` matcher spec --- spec/rspec/expectations/match_matcher_spec.cr | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 spec/rspec/expectations/match_matcher_spec.cr diff --git a/spec/rspec/expectations/match_matcher_spec.cr b/spec/rspec/expectations/match_matcher_spec.cr new file mode 100644 index 0000000..28b4f72 --- /dev/null +++ b/spec/rspec/expectations/match_matcher_spec.cr @@ -0,0 +1,30 @@ +require "../../spec_helper" + +# Examples taken from: +# https://relishapp.com/rspec/rspec-expectations/v/3-8/docs/built-in-matchers/match-matcher +# and modified to fit Spectator and Crystal. +Spectator.describe "`match` matcher" do + context "string usage" do + describe "a string" do + it { is_expected.to match(/str/) } + it { is_expected.not_to match(/foo/) } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to match(/str/) } + xit { is_expected.to match(/foo/) } + end + end + + context "regular expression usage" do + describe /foo/ do + it { is_expected.to match("food") } + it { is_expected.not_to match("drinks") } + + # deliberate failures + # TODO: Add support for expected failures. + xit { is_expected.not_to match("food") } + xit { is_expected.to match("drinks") } + end + end +end From d5c5a82395127a675c8244b17b3ed19f2b2bf438 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 6 Jan 2020 23:46:38 -0700 Subject: [PATCH 40/78] Add with_message modifier to raise_error matcher --- src/spectator/matchers/exception_matcher.cr | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/spectator/matchers/exception_matcher.cr b/src/spectator/matchers/exception_matcher.cr index f94c3f2..82d67ab 100644 --- a/src/spectator/matchers/exception_matcher.cr +++ b/src/spectator/matchers/exception_matcher.cr @@ -90,6 +90,11 @@ module Spectator::Matchers 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 From c48b9ed9e1e8787138f8e73b78b6a14990db0794 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 14 Jan 2020 20:07:56 -0700 Subject: [PATCH 41/78] Resolve let! not working on uninferable types Resolves https://github.com/icy-arctic-fox/spectator/issues/4 Use ValueWrapper trick again and initialize immediately with before_each hook. --- src/spectator/dsl/values.cr | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/spectator/dsl/values.cr b/src/spectator/dsl/values.cr index ea9fdf5..6b1ba51 100644 --- a/src/spectator/dsl/values.cr +++ b/src/spectator/dsl/values.cr @@ -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 From d807f9866aebe9065b39411c3bc55c68657711f5 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Tue, 14 Jan 2020 20:09:15 -0700 Subject: [PATCH 42/78] Bump version to 0.9.2 --- shard.yml | 4 ++-- src/spectator.cr | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/shard.yml b/shard.yml index 3b3e779..661dd1b 100644 --- a/shard.yml +++ b/shard.yml @@ -1,12 +1,12 @@ name: spectator -version: 0.9.1 +version: 0.9.2 description: | A feature-rich spec testing framework for Crystal with similarities to RSpec. authors: - Michael Miller -crystal: 0.31.1 +crystal: 0.32.1 license: MIT diff --git a/src/spectator.cr b/src/spectator.cr index 1d43e65..273ef10 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -6,7 +6,7 @@ module Spectator extend self # Current version of the Spectator library. - VERSION = "0.9.1" + VERSION = "0.9.2" # Top-level describe method. # All specs in a file must be wrapped in this call. From cf448576e22b20e1a80a4059dbc04ae2471633b8 Mon Sep 17 00:00:00 2001 From: Arina Shmeleva Date: Fri, 17 Jan 2020 00:49:59 +0300 Subject: [PATCH 43/78] Add test for subject --- spec/rspec/expectations/subject_spec.cr | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 spec/rspec/expectations/subject_spec.cr diff --git a/spec/rspec/expectations/subject_spec.cr b/spec/rspec/expectations/subject_spec.cr new file mode 100644 index 0000000..a0fea19 --- /dev/null +++ b/spec/rspec/expectations/subject_spec.cr @@ -0,0 +1,12 @@ +require "../../spec_helper" +class Base; end + +Spectator.describe "Subject" do + subject() { Base.new } + + describe "#foo" do + it "bar" do + expect(subject).to be_a(Base) + end + end +end \ No newline at end of file From 13a0b9e2d36cc4be5605dbc3184e49eca86f59da Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 16 Jan 2020 18:36:40 -0700 Subject: [PATCH 44/78] Fix issue with implicit vs. explicit subject Addresses the issue pointed out in !25 There was a recent change that set a subject to the first argument of `describe` and `context`. This prevented an explicitly defined subject from being accessible in nested groups. --- src/spectator/dsl/groups.cr | 4 ++-- src/spectator_test.cr | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index bce9215..227c224 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -24,11 +24,11 @@ module Spectator {{what}} end - def subject(*args) + def _spectator_implicit_subject(*args) described_class.new(*args) end {% else %} - def subject + def _spectator_implicit_subject(*args) {{what}} end {% end %} diff --git a/src/spectator_test.cr b/src/spectator_test.cr index 364d79e..ca0e2e0 100644 --- a/src/spectator_test.cr +++ b/src/spectator_test.cr @@ -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 From 2b2fb5af3afec922f64305e0bfe13bd10e226469 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 16 Jan 2020 18:37:04 -0700 Subject: [PATCH 45/78] Bump version to 0.9.3 --- shard.yml | 2 +- src/spectator.cr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shard.yml b/shard.yml index 661dd1b..63b83e8 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: spectator -version: 0.9.2 +version: 0.9.3 description: | A feature-rich spec testing framework for Crystal with similarities to RSpec. diff --git a/src/spectator.cr b/src/spectator.cr index 273ef10..9dad54d 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -6,7 +6,7 @@ module Spectator extend self # Current version of the Spectator library. - VERSION = "0.9.2" + VERSION = "0.9.3" # Top-level describe method. # All specs in a file must be wrapped in this call. From f825a34771704098c3331027b2c8e96029ee75a6 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 16 Jan 2020 18:36:40 -0700 Subject: [PATCH 46/78] Fix issue with implicit vs. explicit subject Addresses the issue pointed out in !25 There was a recent change that set a subject to the first argument of `describe` and `context`. This prevented an explicitly defined subject from being accessible in nested groups. --- src/spectator/dsl/groups.cr | 4 ++-- src/spectator_test.cr | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index bce9215..227c224 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -24,11 +24,11 @@ module Spectator {{what}} end - def subject(*args) + def _spectator_implicit_subject(*args) described_class.new(*args) end {% else %} - def subject + def _spectator_implicit_subject(*args) {{what}} end {% end %} diff --git a/src/spectator_test.cr b/src/spectator_test.cr index 364d79e..ca0e2e0 100644 --- a/src/spectator_test.cr +++ b/src/spectator_test.cr @@ -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 From e8a437dc0a68bf7412fb853a60323e1068dea3b3 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Thu, 16 Jan 2020 18:37:04 -0700 Subject: [PATCH 47/78] Bump version to 0.9.3 --- shard.yml | 2 +- src/spectator.cr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shard.yml b/shard.yml index 661dd1b..63b83e8 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: spectator -version: 0.9.2 +version: 0.9.3 description: | A feature-rich spec testing framework for Crystal with similarities to RSpec. diff --git a/src/spectator.cr b/src/spectator.cr index 273ef10..9dad54d 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -6,7 +6,7 @@ module Spectator extend self # Current version of the Spectator library. - VERSION = "0.9.2" + VERSION = "0.9.3" # Top-level describe method. # All specs in a file must be wrapped in this call. From 596f7e87c883cb984bc983e75508299703cce830 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 17 Jan 2020 09:27:48 -0700 Subject: [PATCH 48/78] Formatting --- spec/rspec/expectations/subject_spec.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/rspec/expectations/subject_spec.cr b/spec/rspec/expectations/subject_spec.cr index a0fea19..f0d78da 100644 --- a/spec/rspec/expectations/subject_spec.cr +++ b/spec/rspec/expectations/subject_spec.cr @@ -1,4 +1,5 @@ require "../../spec_helper" + class Base; end Spectator.describe "Subject" do @@ -9,4 +10,4 @@ Spectator.describe "Subject" do expect(subject).to be_a(Base) end end -end \ No newline at end of file +end From ed48b80d58085befb0491f960b72528322eb0c16 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 17 Jan 2020 21:58:29 -0700 Subject: [PATCH 49/78] Add RSpec `raise_error` matcher spec --- .../expectations/raise_error_matcher_spec.cr | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 spec/rspec/expectations/raise_error_matcher_spec.cr diff --git a/spec/rspec/expectations/raise_error_matcher_spec.cr b/spec/rspec/expectations/raise_error_matcher_spec.cr new file mode 100644 index 0000000..85678a7 --- /dev/null +++ b/spec/rspec/expectations/raise_error_matcher_spec.cr @@ -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 From a2508d5f6bda4574569f1048f21494a2b402cfff Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 17 Jan 2020 22:08:52 -0700 Subject: [PATCH 50/78] Fix negation case for respond_to matcher --- src/spectator/matchers/respond_matcher.cr | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/spectator/matchers/respond_matcher.cr b/src/spectator/matchers/respond_matcher.cr index 3f26041..59a955f 100644 --- a/src/spectator/matchers/respond_matcher.cr +++ b/src/spectator/matchers/respond_matcher.cr @@ -16,7 +16,7 @@ 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) + if snapshot.values.all? SuccessfulMatchData.new(description) else FailedMatchData.new(description, "#{actual.label} does not respond to #{label}", **values(snapshot)) @@ -27,7 +27,7 @@ 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) + if snapshot.values.any? FailedMatchData.new(description, "#{actual.label} responds to #{label}", **values(snapshot)) else SuccessfulMatchData.new(description) @@ -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 %} From e3a4dedfc6b08cb19958e55b27ac1ce72bdb4f2e Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 17 Jan 2020 22:14:25 -0700 Subject: [PATCH 51/78] Add RSpec `respond_to` matcher spec --- .../expectations/respond_to_matcher_spec.cr | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 spec/rspec/expectations/respond_to_matcher_spec.cr diff --git a/spec/rspec/expectations/respond_to_matcher_spec.cr b/spec/rspec/expectations/respond_to_matcher_spec.cr new file mode 100644 index 0000000..771f8f6 --- /dev/null +++ b/spec/rspec/expectations/respond_to_matcher_spec.cr @@ -0,0 +1,29 @@ +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 + # TODO: Add support for expected failures. + xit { is_expected.to respond_to(:to_model) } + xit { is_expected.to respond_to(:compact, :flatten) } + xit { is_expected.not_to respond_to(:size) } + xit { is_expected.not_to respond_to(:hash, :class, :to_s) } + + # mixed examples--String responds to :length but not :flatten + # both specs should fail + xit { is_expected.to respond_to(:size, :flatten) } + xit { is_expected.not_to respond_to(:size, :flatten) } + end + end + + # Spectator doesn't support argument matching with respond_to. +end From 15f211c4b6ee863a187ec8a73d80f2c0bdbaed5c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 17 Jan 2020 22:41:38 -0700 Subject: [PATCH 52/78] Add `it_fails` to handle expected failures --- spec/rspec/expectations/all_matcher_spec.cr | 8 ++-- spec/rspec/expectations/be_matchers_spec.cr | 44 ++++++++--------- .../expectations/be_within_matcher_spec.cr | 9 ++-- .../rspec/expectations/change_matcher_spec.cr | 8 ++-- .../expectations/comparison_matchers_spec.cr | 19 ++++---- .../contain_exactly_matcher_spec.cr | 6 +-- .../expectations/contain_matcher_spec.cr | 47 +++++++++---------- spec/rspec/expectations/cover_matcher_spec.cr | 15 +++--- .../expectations/end_with_matcher_spec.cr | 10 ++-- .../have_attributes_matcher_spec.cr | 11 ++--- spec/rspec/expectations/match_matcher_spec.cr | 10 ++-- .../expectations/predicate_matchers_spec.cr | 17 +++---- .../expectations/respond_to_matcher_spec.cr | 13 +++-- .../expectations/start_with_matcher_spec.cr | 10 ++-- spec/rspec/expectations/type_matchers_spec.cr | 41 ++++++++-------- spec/spec_helper.cr | 12 +++++ 16 files changed, 130 insertions(+), 150 deletions(-) diff --git a/spec/rspec/expectations/all_matcher_spec.cr b/spec/rspec/expectations/all_matcher_spec.cr index 35ad484..1d910e9 100644 --- a/spec/rspec/expectations/all_matcher_spec.cr +++ b/spec/rspec/expectations/all_matcher_spec.cr @@ -11,10 +11,9 @@ Spectator.describe "`all` matcher" do it { is_expected.to all(be < 10) } # deliberate failures - # TODO: Add support for expected failures. - xit { is_expected.to all(be_even) } - xit { is_expected.to all(be_a(String)) } - xit { is_expected.to all(be > 2) } + 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 @@ -29,7 +28,6 @@ Spectator.describe "`all` matcher" do xit { is_expected.to all(start_with("s")) } # .or contain("y") ) } # deliberate failures - # TODO: Add support for expected failures. # TODO: Add support for compound matchers. xit { is_expected.to all(contain("foo")) } # .and contain("bar") ) } xit { is_expected.to all(be_a(String)) } # .and start_with("a") ) } diff --git a/spec/rspec/expectations/be_matchers_spec.cr b/spec/rspec/expectations/be_matchers_spec.cr index 79636f2..1e4ee63 100644 --- a/spec/rspec/expectations/be_matchers_spec.cr +++ b/spec/rspec/expectations/be_matchers_spec.cr @@ -12,12 +12,11 @@ Spectator.describe "`be` matchers" do specify { expect(false).not_to be_truthy } # deliberate failures - # TODO: Add support for expected failures. - pending { expect(true).not_to be_truthy } - pending { expect(7).not_to be_truthy } - pending { expect("foo").not_to be_truthy } - pending { expect(nil).to be_truthy } - pending { expect(false).to be_truthy } + 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 @@ -28,12 +27,11 @@ Spectator.describe "`be` matchers" do specify { expect("foo").not_to be_falsey } # deliberate failures - # TODO: Add support for expected failures. - pending { expect(nil).not_to be_falsey } - pending { expect(false).not_to be_falsey } - pending { expect(true).to be_falsey } - pending { expect(7).to be_falsey } - pending { expect("foo").to be_falsey } + 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 @@ -44,12 +42,11 @@ Spectator.describe "`be` matchers" do specify { expect("foo").not_to be_nil } # deliberate failures - # TODO: Add support for expected failures. - pending { expect(nil).not_to be_nil } - pending { expect(false).to be_nil } - pending { expect(true).to be_nil } - pending { expect(7).to be_nil } - pending { expect("foo").to be_nil } + 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 @@ -60,11 +57,10 @@ Spectator.describe "`be` matchers" do specify { expect(false).not_to be } # deliberate failures - # TODO: Add support for expected failures. - pending { expect(true).not_to be } - pending { expect(7).not_to be } - pending { expect("foo").not_to be } - pending { expect(nil).to be } - pending { expect(false).to be } + 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 diff --git a/spec/rspec/expectations/be_within_matcher_spec.cr b/spec/rspec/expectations/be_within_matcher_spec.cr index 0307d1b..2a99b51 100644 --- a/spec/rspec/expectations/be_within_matcher_spec.cr +++ b/spec/rspec/expectations/be_within_matcher_spec.cr @@ -15,11 +15,10 @@ Spectator.describe "`be_within` matcher" do it { is_expected.not_to be_within(0.5).of(26.9) } # deliberate failures - # TODO: Add support for expected failures. - xit { is_expected.not_to be_within(0.5).of(28) } - xit { is_expected.not_to be_within(0.5).of(27) } - xit { is_expected.to be_within(0.5).of(28.1) } - xit { is_expected.to be_within(0.5).of(26.9) } + 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 diff --git a/spec/rspec/expectations/change_matcher_spec.cr b/spec/rspec/expectations/change_matcher_spec.cr index c59423e..9e712a7 100644 --- a/spec/rspec/expectations/change_matcher_spec.cr +++ b/spec/rspec/expectations/change_matcher_spec.cr @@ -26,8 +26,7 @@ Spectator.describe "`change` matcher" do end # deliberate failure - # TODO: Add support for expected failures. - xit "should increment the count by 2" do + it_fails "should increment the count by 2" do expect { Counter.increment }.to change { Counter.count }.by(2) end end @@ -36,12 +35,11 @@ Spectator.describe "`change` matcher" do context "expect no change" do describe "Counter#increment" do # TODO: Allow multiple arguments to context/describe. # deliberate failures - # TODO: Add support for expected failures. - xit "should not increment the count by 1 (using not_to)" do + it_fails "should not increment the count by 1 (using not_to)" do expect { Counter.increment }.not_to change { Counter.count } end - xit "should not increment the count by 1 (using to_not)" do + it_fails "should not increment the count by 1 (using to_not)" do expect { Counter.increment }.to_not change { Counter.count } end end diff --git a/spec/rspec/expectations/comparison_matchers_spec.cr b/spec/rspec/expectations/comparison_matchers_spec.cr index aef4194..bb201d7 100644 --- a/spec/rspec/expectations/comparison_matchers_spec.cr +++ b/spec/rspec/expectations/comparison_matchers_spec.cr @@ -12,11 +12,10 @@ Spectator.describe "Comparison matchers" do it { is_expected.to be >= 17 } # deliberate failures - # TODO: Add support for expected failures. - xit { is_expected.to be < 15 } - xit { is_expected.to be > 20 } - xit { is_expected.to be <= 17 } - xit { is_expected.to be >= 19 } + it_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 @@ -24,7 +23,6 @@ Spectator.describe "Comparison matchers" do it { is_expected.to be < 'b' } # deliberate failures - # TODO: Add support for expected failures. # it { is_expected.to be < 18 } # Removed because Crystal doesn't support Char#<(Int32) end end @@ -37,11 +35,10 @@ Spectator.describe "Comparison matchers" do it { is_expected.to be >= "Banana" } # deliberate failures - # TODO: Add support for expected failures. - xit { is_expected.to be < "Cranberry" } - xit { is_expected.to be > "Zuchini" } - xit { is_expected.to be <= "Potato" } - xit { is_expected.to be >= "Tomato" } + 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 diff --git a/spec/rspec/expectations/contain_exactly_matcher_spec.cr b/spec/rspec/expectations/contain_exactly_matcher_spec.cr index 14d3c33..c4d7d3c 100644 --- a/spec/rspec/expectations/contain_exactly_matcher_spec.cr +++ b/spec/rspec/expectations/contain_exactly_matcher_spec.cr @@ -14,8 +14,7 @@ Spectator.describe "`contain_exactly` matcher" do it { is_expected.to contain_exactly(3, 2, 1) } # deliberate failures - # TODO: Add support for expected failures. - xit { is_expected.to contain_exactly(1, 2, 1) } + it_fails { is_expected.to contain_exactly(1, 2, 1) } end end @@ -25,8 +24,7 @@ Spectator.describe "`contain_exactly` matcher" do it { is_expected.to_not contain_exactly(1, 2) } # deliberate failures - # TODO: Add support for expected failures. - xit { is_expected.to_not contain_exactly(1, 3, 2) } + it_fails { is_expected.to_not contain_exactly(1, 3, 2) } end end end diff --git a/spec/rspec/expectations/contain_matcher_spec.cr b/spec/rspec/expectations/contain_matcher_spec.cr index a75897c..6c8e558 100644 --- a/spec/rspec/expectations/contain_matcher_spec.cr +++ b/spec/rspec/expectations/contain_matcher_spec.cr @@ -27,17 +27,16 @@ Spectator.describe "`contain` matcher" do it { is_expected.not_to contain(43, 100) } # deliberate failures - # TODO: Add support for expected failures. - xit { is_expected.to contain(4) } - xit { is_expected.to contain(be_even) } - xit { is_expected.not_to contain(1) } - xit { is_expected.not_to contain(3) } - xit { is_expected.not_to contain(7) } - xit { is_expected.not_to contain(1, 3, 7) } + 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 - xit { is_expected.to contain(1, 9) } - xit { is_expected.not_to contain(1, 9) } + it_fails { is_expected.to contain(1, 9) } + it_fails { is_expected.not_to contain(1, 9) } end end @@ -49,11 +48,10 @@ Spectator.describe "`contain` matcher" do it { is_expected.not_to contain("foo", "bar") } # deliberate failures - # TODO: Add support for expected failures. - xit { is_expected.to contain("foo") } - xit { is_expected.not_to contain("str") } - xit { is_expected.to contain("str", "foo") } - xit { is_expected.not_to contain("str", "foo") } + 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 @@ -64,34 +62,33 @@ Spectator.describe "`contain` matcher" do subject { {:a => 7, :b => 5} } # Hash syntax is changed here from `:a => 7` to `a: 7`. - xit { is_expected.to contain(:a) } - xit { is_expected.to contain(:b, :a) } + # 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) } - xit { is_expected.not_to contain(:c) } - xit { is_expected.not_to contain(:c, :d) } + # 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 - # TODO: Add support for expected failures. - xit { is_expected.not_to contain(:a) } - xit { is_expected.not_to contain(:b, :a) } + # it { is_expected.not_to contain(:a) } + # 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) } - xit { is_expected.to contain(:c) } - xit { is_expected.to contain(:c, :d) } + # 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. - xit { is_expected.to contain(:a, :d) } - xit { is_expected.not_to contain(:a, :d) } + # 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 diff --git a/spec/rspec/expectations/cover_matcher_spec.cr b/spec/rspec/expectations/cover_matcher_spec.cr index fb1a74a..cf32929 100644 --- a/spec/rspec/expectations/cover_matcher_spec.cr +++ b/spec/rspec/expectations/cover_matcher_spec.cr @@ -15,16 +15,15 @@ Spectator.describe "`cover` matcher" do it { is_expected.not_to cover(11, 12) } # deliberate failures - # TODO: Add support for expected failures. - xit { is_expected.to cover(11) } - xit { is_expected.not_to cover(4) } - xit { is_expected.not_to cover(6) } - xit { is_expected.not_to cover(8) } - xit { is_expected.not_to cover(4, 6, 8) } + 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 - xit { is_expected.to cover(5, 11) } - xit { is_expected.not_to cover(5, 11) } + it_fails { is_expected.to cover(5, 11) } + it_fails { is_expected.not_to cover(5, 11) } end end end diff --git a/spec/rspec/expectations/end_with_matcher_spec.cr b/spec/rspec/expectations/end_with_matcher_spec.cr index c19720f..75e6fd1 100644 --- a/spec/rspec/expectations/end_with_matcher_spec.cr +++ b/spec/rspec/expectations/end_with_matcher_spec.cr @@ -10,9 +10,8 @@ Spectator.describe "`end_with` matcher" do it { is_expected.not_to end_with "stringy" } # deliberate failures - # TODO: Add support for expected failures. - xit { is_expected.not_to end_with "string" } - xit { is_expected.to end_with "stringy" } + it_fails { is_expected.not_to end_with "string" } + it_fails { is_expected.to end_with "stringy" } end end @@ -25,9 +24,8 @@ Spectator.describe "`end_with` matcher" do # it { is_expected.not_to end_with 0, 1, 2, 3, 4, 5 } # deliberate failures - # TODO: Add support for expected failures. - xit { is_expected.not_to end_with 4 } - xit { is_expected.to end_with 3 } + it_fails { is_expected.not_to end_with 4 } + it_fails { is_expected.to end_with 3 } end end end diff --git a/spec/rspec/expectations/have_attributes_matcher_spec.cr b/spec/rspec/expectations/have_attributes_matcher_spec.cr index dfa273b..81c11dc 100644 --- a/spec/rspec/expectations/have_attributes_matcher_spec.cr +++ b/spec/rspec/expectations/have_attributes_matcher_spec.cr @@ -24,14 +24,13 @@ Spectator.describe "`have_attributes` matcher" do # it { is_expected.not_to have_attributes(age: (a_value < 30) ) } # deliberate failures - # TODO: Add support for expected failures. - xit { is_expected.to have_attributes(name: "Bob") } - xit { is_expected.to have_attributes(name: 10) } + 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 - xit { is_expected.to have_attributes(name: "Bob", age: 32) } - xit { is_expected.to have_attributes(name: "Jim", age: 10) } - xit { is_expected.to have_attributes(name: "Bob", age: 10) } + 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 diff --git a/spec/rspec/expectations/match_matcher_spec.cr b/spec/rspec/expectations/match_matcher_spec.cr index 28b4f72..dac2a0c 100644 --- a/spec/rspec/expectations/match_matcher_spec.cr +++ b/spec/rspec/expectations/match_matcher_spec.cr @@ -10,9 +10,8 @@ Spectator.describe "`match` matcher" do it { is_expected.not_to match(/foo/) } # deliberate failures - # TODO: Add support for expected failures. - xit { is_expected.not_to match(/str/) } - xit { is_expected.to match(/foo/) } + it_fails { is_expected.not_to match(/str/) } + it_fails { is_expected.to match(/foo/) } end end @@ -22,9 +21,8 @@ Spectator.describe "`match` matcher" do it { is_expected.not_to match("drinks") } # deliberate failures - # TODO: Add support for expected failures. - xit { is_expected.not_to match("food") } - xit { is_expected.to match("drinks") } + it_fails { is_expected.not_to match("food") } + it_fails { is_expected.to match("drinks") } end end end diff --git a/spec/rspec/expectations/predicate_matchers_spec.cr b/spec/rspec/expectations/predicate_matchers_spec.cr index f2dac5c..8a1cbda 100644 --- a/spec/rspec/expectations/predicate_matchers_spec.cr +++ b/spec/rspec/expectations/predicate_matchers_spec.cr @@ -11,8 +11,7 @@ Spectator.describe "Predicate matchers" do describe 7 do # deliberate failure - # TODO: Add support for expected failures. - xit { is_expected.to be_zero } + it_fails { is_expected.to be_zero } end end @@ -23,8 +22,7 @@ Spectator.describe "Predicate matchers" do describe [] of Int32 do # deliberate failure - # TODO: Add support for expected failures. - xit { is_expected.not_to be_empty } + it_fails { is_expected.not_to be_empty } end end @@ -35,8 +33,7 @@ Spectator.describe "Predicate matchers" do it { is_expected.to have_key(:foo) } # deliberate failure - # TODO: Add support for expected failures. - xit { is_expected.to have_key(:bar) } + it_fails { is_expected.to have_key(:bar) } end end @@ -58,8 +55,7 @@ Spectator.describe "Predicate matchers" do subject { {"foo" => 7, "bar" => 5} } # deliberate failure - # TODO: Add support for expected failures. - xit { is_expected.not_to have_all_string_keys } + it_fails { is_expected.not_to have_all_string_keys } end end end @@ -76,9 +72,8 @@ Spectator.describe "Predicate matchers" do it { is_expected.not_to be_multiple_of(7) } # deliberate failures - # TODO: Add support for expected failures. - xit { is_expected.not_to be_multiple_of(4) } - xit { is_expected.to be_multiple_of(5) } + it_fails { is_expected.not_to be_multiple_of(4) } + it_fails { is_expected.to be_multiple_of(5) } end end diff --git a/spec/rspec/expectations/respond_to_matcher_spec.cr b/spec/rspec/expectations/respond_to_matcher_spec.cr index 771f8f6..ff52285 100644 --- a/spec/rspec/expectations/respond_to_matcher_spec.cr +++ b/spec/rspec/expectations/respond_to_matcher_spec.cr @@ -12,16 +12,15 @@ Spectator.describe "`respond_to` matcher" do it { is_expected.not_to respond_to(:compact, :flatten) } # deliberate failures - # TODO: Add support for expected failures. - xit { is_expected.to respond_to(:to_model) } - xit { is_expected.to respond_to(:compact, :flatten) } - xit { is_expected.not_to respond_to(:size) } - xit { is_expected.not_to respond_to(:hash, :class, :to_s) } + 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 - xit { is_expected.to respond_to(:size, :flatten) } - xit { is_expected.not_to respond_to(:size, :flatten) } + it_fails { is_expected.to respond_to(:size, :flatten) } + it_fails { is_expected.not_to respond_to(:size, :flatten) } end end diff --git a/spec/rspec/expectations/start_with_matcher_spec.cr b/spec/rspec/expectations/start_with_matcher_spec.cr index c2942cc..471f1d6 100644 --- a/spec/rspec/expectations/start_with_matcher_spec.cr +++ b/spec/rspec/expectations/start_with_matcher_spec.cr @@ -10,9 +10,8 @@ Spectator.describe "`start_with` matcher" do it { is_expected.not_to start_with "that" } # deliberate failures - # TODO: Add support for expected failures. - xit { is_expected.not_to start_with "this" } - xit { is_expected.to start_with "that" } + it_fails { is_expected.not_to start_with "this" } + it_fails { is_expected.to start_with "that" } end end @@ -25,9 +24,8 @@ Spectator.describe "`start_with` matcher" do # it { is_expected.not_to start_with(0, 1, 2, 3, 4, 5) } # deliberate failures - # TODO: Add support for expected failures. - xit { is_expected.not_to start_with 0 } - xit { is_expected.to start_with 3 } + it_fails { is_expected.not_to start_with 0 } + it_fails { is_expected.to start_with 3 } end end end diff --git a/spec/rspec/expectations/type_matchers_spec.cr b/spec/rspec/expectations/type_matchers_spec.cr index c2b1a2d..35bec18 100644 --- a/spec/rspec/expectations/type_matchers_spec.cr +++ b/spec/rspec/expectations/type_matchers_spec.cr @@ -40,19 +40,18 @@ Spectator.describe "Type matchers" do it { is_expected.not_to be_a(String) } # deliberate failures - # TODO: Add support for expected failures. - xit { is_expected.not_to be_kind_of(Derived) } - xit { is_expected.not_to be_a_kind_of(Derived) } - xit { is_expected.not_to be_a(Derived) } - xit { is_expected.not_to be_kind_of(Base) } - xit { is_expected.not_to be_a_kind_of(Base) } - xit { is_expected.not_to be_an(Base) } - xit { is_expected.not_to be_kind_of(MyModule) } - xit { is_expected.not_to be_a_kind_of(MyModule) } - xit { is_expected.not_to be_a(MyModule) } - xit { is_expected.to be_kind_of(String) } - xit { is_expected.to be_a_kind_of(String) } - xit { is_expected.to be_a(String) } + 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 @@ -87,14 +86,14 @@ Spectator.describe "Type matchers" do it { is_expected.not_to be_an_instance_of(String) } # deliberate failures - xit { is_expected.not_to be_instance_of(Derived) } - xit { is_expected.not_to be_an_instance_of(Derived) } - xit { is_expected.to be_instance_of(Base) } - xit { is_expected.to be_an_instance_of(Base) } - xit { is_expected.to be_instance_of(MyModule) } - xit { is_expected.to be_an_instance_of(MyModule) } - xit { is_expected.to be_instance_of(String) } - xit { is_expected.to be_an_instance_of(String) } + 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 diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index bca1453..f8047da 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -1 +1,13 @@ require "../src/spectator" + +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 From 272ebcd69320744fdccada554f5c7131a802d836 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 19 Jan 2020 09:52:12 -0700 Subject: [PATCH 53/78] Fix subject with group describing a type An explicit subject should be used when describing a type. This mimics RSpec. --- src/spectator/dsl/groups.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 227c224..33c12a1 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -24,7 +24,7 @@ module Spectator {{what}} end - def _spectator_implicit_subject(*args) + def subject(*args) described_class.new(*args) end {% else %} From 1825026af7aed436a934190261ee01bfd9be246c Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 19 Jan 2020 09:53:20 -0700 Subject: [PATCH 54/78] Bump version to 0.9.4 --- shard.yml | 2 +- src/spectator.cr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shard.yml b/shard.yml index 63b83e8..9f25812 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: spectator -version: 0.9.3 +version: 0.9.4 description: | A feature-rich spec testing framework for Crystal with similarities to RSpec. diff --git a/src/spectator.cr b/src/spectator.cr index 9dad54d..52c4e52 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -6,7 +6,7 @@ module Spectator extend self # Current version of the Spectator library. - VERSION = "0.9.3" + VERSION = "0.9.4" # Top-level describe method. # All specs in a file must be wrapped in this call. From e60c287216122bfec576dc80b72f5e78f56a3e96 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 19 Jan 2020 10:05:21 -0700 Subject: [PATCH 55/78] Fix a dumb with merge --- shard.yml | 2 +- src/spectator.cr | 2 +- src/spectator/dsl/groups.cr | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/shard.yml b/shard.yml index 9f25812..ce851ea 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: spectator -version: 0.9.4 +version: 0.9.5 description: | A feature-rich spec testing framework for Crystal with similarities to RSpec. diff --git a/src/spectator.cr b/src/spectator.cr index 52c4e52..6ad00a3 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -6,7 +6,7 @@ module Spectator extend self # Current version of the Spectator library. - VERSION = "0.9.4" + VERSION = "0.9.5" # Top-level describe method. # All specs in a file must be wrapped in this call. diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 227c224..33c12a1 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -24,7 +24,7 @@ module Spectator {{what}} end - def _spectator_implicit_subject(*args) + def subject(*args) described_class.new(*args) end {% else %} From 8381c08b05616bb25ecd42c8a9a49cd2425a5796 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 19 Jan 2020 22:05:38 -0700 Subject: [PATCH 56/78] Add subject specs --- spec/rspec/core/explicit_subject_spec.cr | 140 ++++++++++++++++++++++ spec/rspec/core/implicit_subject_spec.cr | 43 +++++++ spec/rspec/core/one_liner_subject_spec.cr | 32 +++++ spec/rspec/expectations/subject_spec.cr | 13 -- spec/subject_spec.cr | 13 ++ 5 files changed, 228 insertions(+), 13 deletions(-) create mode 100644 spec/rspec/core/explicit_subject_spec.cr create mode 100644 spec/rspec/core/implicit_subject_spec.cr create mode 100644 spec/rspec/core/one_liner_subject_spec.cr delete mode 100644 spec/rspec/expectations/subject_spec.cr create mode 100644 spec/subject_spec.cr diff --git a/spec/rspec/core/explicit_subject_spec.cr b/spec/rspec/core/explicit_subject_spec.cr new file mode 100644 index 0000000..6086bdd --- /dev/null +++ b/spec/rspec/core/explicit_subject_spec.cr @@ -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 eq(subject) # TODO: `be` matcher with value (no same? method). + end + + it "is set to the block return value (i.e. the global $count)" do + expect(global_count).to eq(@@count) # TODO: `be` matcher with value (no same? method). + 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 diff --git a/spec/rspec/core/implicit_subject_spec.cr b/spec/rspec/core/implicit_subject_spec.cr new file mode 100644 index 0000000..f6e1d40 --- /dev/null +++ b/spec/rspec/core/implicit_subject_spec.cr @@ -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 diff --git a/spec/rspec/core/one_liner_subject_spec.cr b/spec/rspec/core/one_liner_subject_spec.cr new file mode 100644 index 0000000..15aa6ad --- /dev/null +++ b/spec/rspec/core/one_liner_subject_spec.cr @@ -0,0 +1,32 @@ +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 + + # TODO: Define `should` method on example to be `is_expected`. + # 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] } + # TODO: Define `should_not` method on example to be `is_expected.not_to`. + # it { should_not be_empty } + # or + it { is_expected.not_to be_empty } + end + end + end +end diff --git a/spec/rspec/expectations/subject_spec.cr b/spec/rspec/expectations/subject_spec.cr deleted file mode 100644 index f0d78da..0000000 --- a/spec/rspec/expectations/subject_spec.cr +++ /dev/null @@ -1,13 +0,0 @@ -require "../../spec_helper" - -class Base; end - -Spectator.describe "Subject" do - subject() { Base.new } - - describe "#foo" do - it "bar" do - expect(subject).to be_a(Base) - end - end -end diff --git a/spec/subject_spec.cr b/spec/subject_spec.cr new file mode 100644 index 0000000..aa02a39 --- /dev/null +++ b/spec/subject_spec.cr @@ -0,0 +1,13 @@ +require "./spec_helper" + +class Base; 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 +end From b1984b343a5cf2abe6102b5ede60f8e557324ca3 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 19 Jan 2020 22:12:06 -0700 Subject: [PATCH 57/78] Add `should` keywords to examples Allows short-hand like: it { should be_empty } --- spec/rspec/core/one_liner_subject_spec.cr | 7 +++---- src/spectator/dsl/assertions.cr | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/spec/rspec/core/one_liner_subject_spec.cr b/spec/rspec/core/one_liner_subject_spec.cr index 15aa6ad..c1e124d 100644 --- a/spec/rspec/core/one_liner_subject_spec.cr +++ b/spec/rspec/core/one_liner_subject_spec.cr @@ -11,8 +11,7 @@ Spectator.describe "One-liner syntax" do # subject.should be_empty # end - # TODO: Define `should` method on example to be `is_expected`. - # it { should be_empty } + it { should be_empty } # or it { is_expected.to be_empty } end @@ -22,8 +21,8 @@ Spectator.describe "One-liner syntax" do describe Array(Int32) do describe "with 3 items" do subject { [1, 2, 3] } - # TODO: Define `should_not` method on example to be `is_expected.not_to`. - # it { should_not be_empty } + + it { should_not be_empty } # or it { is_expected.not_to be_empty } end diff --git a/src/spectator/dsl/assertions.cr b/src/spectator/dsl/assertions.cr index f315a63..fc6f84b 100644 --- a/src/spectator/dsl/assertions.cr +++ b/src/spectator/dsl/assertions.cr @@ -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_eventuall(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. From 3d08949c942b1dabaf3ae884c6a576cbf28b8ba9 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 19 Jan 2020 22:24:28 -0700 Subject: [PATCH 58/78] Add helper method specs from RSpec docs --- spec/rspec/core/helper_methods_spec.cr | 29 +++++++++++++++++ spec/rspec/core/let_spec.cr | 45 ++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 spec/rspec/core/helper_methods_spec.cr create mode 100644 spec/rspec/core/let_spec.cr diff --git a/spec/rspec/core/helper_methods_spec.cr b/spec/rspec/core/helper_methods_spec.cr new file mode 100644 index 0000000..a5981de --- /dev/null +++ b/spec/rspec/core/helper_methods_spec.cr @@ -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 eq(:available) # TODO: `be` matcher with value (no same? method). + 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 eq(:available) # TODO: `be` matcher with value (no same? method). + end + end + end + end +end diff --git a/spec/rspec/core/let_spec.cr b/spec/rspec/core/let_spec.cr new file mode 100644 index 0000000..815306d --- /dev/null +++ b/spec/rspec/core/let_spec.cr @@ -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 From 60f1c3091b8ac00f013436528b0cf282da19b968 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 19 Jan 2020 22:39:21 -0700 Subject: [PATCH 59/78] Allow `be` matcher to be used on structs --- spec/rspec/core/explicit_subject_spec.cr | 4 ++-- spec/rspec/core/helper_methods_spec.cr | 4 ++-- src/spectator/matchers/reference_matcher.cr | 8 +++++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/spec/rspec/core/explicit_subject_spec.cr b/spec/rspec/core/explicit_subject_spec.cr index 6086bdd..bc102e8 100644 --- a/spec/rspec/core/explicit_subject_spec.cr +++ b/spec/rspec/core/explicit_subject_spec.cr @@ -115,11 +115,11 @@ Spectator.describe "Explicit Subject" do end it "the subject and named helpers return the same object" do - expect(global_count).to eq(subject) # TODO: `be` matcher with value (no same? method). + 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 eq(@@count) # TODO: `be` matcher with value (no same? method). + expect(global_count).to be(@@count) end end end diff --git a/spec/rspec/core/helper_methods_spec.cr b/spec/rspec/core/helper_methods_spec.cr index a5981de..3fc6802 100644 --- a/spec/rspec/core/helper_methods_spec.cr +++ b/spec/rspec/core/helper_methods_spec.cr @@ -8,7 +8,7 @@ Spectator.describe "Arbitrary helper methods" do end it "has access to methods define in its group" do - expect(help).to eq(:available) # TODO: `be` matcher with value (no same? method). + expect(help).to be(:available) end end end @@ -21,7 +21,7 @@ Spectator.describe "Arbitrary helper methods" do describe "in a nested group" do it "has access to methods defined in its parent group" do - expect(help).to eq(:available) # TODO: `be` matcher with value (no same? method). + expect(help).to be(:available) end end end diff --git a/src/spectator/matchers/reference_matcher.cr b/src/spectator/matchers/reference_matcher.cr index 3586e14..dc6c476 100644 --- a/src/spectator/matchers/reference_matcher.cr +++ b/src/spectator/matchers/reference_matcher.cr @@ -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. From 1e3d89884bc74c5e68db656934e358774bb8dc9d Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 20 Jan 2020 20:11:19 -0700 Subject: [PATCH 60/78] Add some RSpec docs tests for hooks --- .../rspec/core/before_and_after_hooks_spec.cr | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 spec/rspec/core/before_and_after_hooks_spec.cr diff --git a/spec/rspec/core/before_and_after_hooks_spec.cr b/spec/rspec/core/before_and_after_hooks_spec.cr new file mode 100644 index 0000000..2820677 --- /dev/null +++ b/spec/rspec/core/before_and_after_hooks_spec.cr @@ -0,0 +1,113 @@ +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 + + xit "gets run in order" do + # TODO: The after_all hook isn't evaluated yet because the group hasn't "finished." + expect(@@order).to_eventually eq(%i[before_all before_each after_each after_all]) + end + end + end +end From 721425c3c802349bbfad8e9c52346189a17ec6dc Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 20 Jan 2020 20:15:49 -0700 Subject: [PATCH 61/78] Run deferred expectations after all hooks --- spec/rspec/core/before_and_after_hooks_spec.cr | 3 +-- src/spectator/runnable_example.cr | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/rspec/core/before_and_after_hooks_spec.cr b/spec/rspec/core/before_and_after_hooks_spec.cr index 2820677..f3c8523 100644 --- a/spec/rspec/core/before_and_after_hooks_spec.cr +++ b/spec/rspec/core/before_and_after_hooks_spec.cr @@ -104,8 +104,7 @@ Spectator.describe "`before` and `after` hooks" do @@order << :after_all end - xit "gets run in order" do - # TODO: The after_all hook isn't evaluated yet because the group hasn't "finished." + it "gets run in order" do expect(@@order).to_eventually eq(%i[before_all before_each after_each after_all]) end end diff --git a/src/spectator/runnable_example.cr b/src/spectator/runnable_example.cr index 02086d0..fbfb40e 100644 --- a/src/spectator/runnable_example.cr +++ b/src/spectator/runnable_example.cr @@ -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 From e3ad92f785256028f61fcc82f88e9818b50b76bc Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 20 Jan 2020 20:31:47 -0700 Subject: [PATCH 62/78] Move style checks to their own job --- .gitlab-ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f89c555..8a58eae 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -15,6 +15,9 @@ before_script: spec: script: - crystal spec --error-on-warnings + +style: + script: - bin/ameba - crystal tool format --check From cacca99c0b74a075326554621d1fb5e550ee3c8f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 20 Jan 2020 20:32:14 -0700 Subject: [PATCH 63/78] Formatting --- spec/rspec/core/before_and_after_hooks_spec.cr | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/rspec/core/before_and_after_hooks_spec.cr b/spec/rspec/core/before_and_after_hooks_spec.cr index f3c8523..6bebe4d 100644 --- a/spec/rspec/core/before_and_after_hooks_spec.cr +++ b/spec/rspec/core/before_and_after_hooks_spec.cr @@ -19,7 +19,7 @@ Spectator.describe "`before` and `after` hooks" do 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. + expect(widgets.size).to eq(0) # Use size instead of count. end it "can accept new widgets" do @@ -28,8 +28,8 @@ Spectator.describe "`before` and `after` hooks" do 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. + 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 @@ -55,7 +55,7 @@ Spectator.describe "`before` and `after` hooks" 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. + expect(widgets.size).to eq(0) # Use size instead of count. end it "can accept new widgets" do @@ -64,8 +64,8 @@ Spectator.describe "`before` and `after` hooks" do 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. + 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 From d30e65ee2dcb091beba09e463bfc4df20ace22b8 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 20 Jan 2020 20:42:16 -0700 Subject: [PATCH 64/78] Missing ? on method name check --- src/spectator/matchers/reference_matcher.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/matchers/reference_matcher.cr b/src/spectator/matchers/reference_matcher.cr index dc6c476..e7907ae 100644 --- a/src/spectator/matchers/reference_matcher.cr +++ b/src/spectator/matchers/reference_matcher.cr @@ -14,7 +14,7 @@ module Spectator::Matchers # Checks whether the matcher is satisifed with the expression given to it. private def match?(actual : TestExpression(T)) : Bool forall T value = expected.value - if value.responds_to?(:same) + if value.responds_to?(:same?) value.same?(actual.value) else # Value type (struct) comparison. From a7db9ff0bb698513c9c5710b90ba1c819a1434ee Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 10 Feb 2020 17:46:36 -0700 Subject: [PATCH 65/78] Don't force previous_def on class methods --- src/spectator/mocks/stubs.cr | 1 - 1 file changed, 1 deletion(-) diff --git a/src/spectator/mocks/stubs.cr b/src/spectator/mocks/stubs.cr index 00677f8..04b8e10 100644 --- a/src/spectator/mocks/stubs.cr +++ b/src/spectator/mocks/stubs.cr @@ -40,7 +40,6 @@ module Spectator::Mocks :super end.id receiver = if receiver == :self.id - original = :previous_def.id "self." else "" From 63ff59688b3a14c1893414f5d69efe65c6033982 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 10 Feb 2020 17:47:02 -0700 Subject: [PATCH 66/78] Bump version to 0.9.6 --- shard.yml | 2 +- src/spectator.cr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shard.yml b/shard.yml index ce851ea..ecf6408 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: spectator -version: 0.9.5 +version: 0.9.6 description: | A feature-rich spec testing framework for Crystal with similarities to RSpec. diff --git a/src/spectator.cr b/src/spectator.cr index 6ad00a3..4fcac90 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -6,7 +6,7 @@ module Spectator extend self # Current version of the Spectator library. - VERSION = "0.9.5" + VERSION = "0.9.6" # Top-level describe method. # All specs in a file must be wrapped in this call. From 7a8e570bea265e065c1647ef347fb837779a43bc Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Mon, 10 Feb 2020 17:50:35 -0700 Subject: [PATCH 67/78] Fix typo with should_eventually --- src/spectator/dsl/assertions.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spectator/dsl/assertions.cr b/src/spectator/dsl/assertions.cr index fc6f84b..a9f1516 100644 --- a/src/spectator/dsl/assertions.cr +++ b/src/spectator/dsl/assertions.cr @@ -192,7 +192,7 @@ module Spectator is_expected.to_not({{matcher}}) end - macro should_eventuall(matcher) + macro should_eventually(matcher) is_expected.to_eventually({{matcher}}) end From 2aba67edae0974656c3c92eb2391aabdfd8d622b Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 14 Feb 2020 18:14:32 -0700 Subject: [PATCH 68/78] Fix idiosyncrasies around class method stubs The new method misbehaves here. It always appeared as defined, even though it isn't explicitly defined in the class. --- src/spectator/mocks/stubs.cr | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/spectator/mocks/stubs.cr b/src/spectator/mocks/stubs.cr index 04b8e10..fceefc5 100644 --- a/src/spectator/mocks/stubs.cr +++ b/src/spectator/mocks/stubs.cr @@ -34,16 +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 + 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) %} From dd48bf6f2548395845794cb71c7335870d1cbf91 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 Feb 2020 19:34:48 -0700 Subject: [PATCH 69/78] Fix subject not being memoized when using described_class --- src/spectator/dsl/groups.cr | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 33c12a1..937e052 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -24,8 +24,16 @@ module Spectator {{what}} end + @%wrapper : ::Spectator::TypedValueWrapper({{what}})? + def subject(*args) - described_class.new(*args) + if (wrapper = @%wrapper) + wrapper.value + else + described_class.new(*args).tap do |value| + @%wrapper = ::Spectator::TypedValueWrapper.new(value) + end + end end {% else %} def _spectator_implicit_subject(*args) From 5cb74ebe95b101c1893d744c837de1f5d33e123d Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 Feb 2020 19:36:09 -0700 Subject: [PATCH 70/78] Bump version to 0.9.7 --- shard.yml | 2 +- src/spectator.cr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shard.yml b/shard.yml index ecf6408..0b07adc 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: spectator -version: 0.9.6 +version: 0.9.7 description: | A feature-rich spec testing framework for Crystal with similarities to RSpec. diff --git a/src/spectator.cr b/src/spectator.cr index 4fcac90..295f091 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -6,7 +6,7 @@ module Spectator extend self # Current version of the Spectator library. - VERSION = "0.9.6" + VERSION = "0.9.7" # Top-level describe method. # All specs in a file must be wrapped in this call. From 86c8d6549a8e81a2552f4c317bc72bcf93511485 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 Feb 2020 19:36:18 -0700 Subject: [PATCH 71/78] Update shard versions --- shard.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shard.yml b/shard.yml index 0b07adc..0ef85ad 100644 --- a/shard.yml +++ b/shard.yml @@ -6,11 +6,11 @@ description: | authors: - Michael Miller -crystal: 0.32.1 +crystal: 0.33.0 license: MIT development_dependencies: ameba: github: crystal-ameba/ameba - version: ~> 0.10 + version: ~> 0.11.0 From 3aaab0e74f59d825d4257f78455143f36321a072 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sun, 16 Feb 2020 19:50:20 -0700 Subject: [PATCH 72/78] Fix memoized subject from context description another way The other method gave a strange compiler error when running specs. Something to do with "K" not having an inspect method. In /usr/share/crystal/src/hash.cr:1826:13 1826 | key.inspect(io) ^------ Error: undefined method 'inspect' for K --- src/spectator/dsl/groups.cr | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 937e052..73c130f 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -24,17 +24,7 @@ module Spectator {{what}} end - @%wrapper : ::Spectator::TypedValueWrapper({{what}})? - - def subject(*args) - if (wrapper = @%wrapper) - wrapper.value - else - described_class.new(*args).tap do |value| - @%wrapper = ::Spectator::TypedValueWrapper.new(value) - end - end - end + subject { described_class.new } {% else %} def _spectator_implicit_subject(*args) {{what}} From ce9bf918c1dd6102f20fc701c04f2b0a81d70e37 Mon Sep 17 00:00:00 2001 From: Davide Paolo Tua Date: Fri, 21 Feb 2020 12:48:22 +0100 Subject: [PATCH 73/78] Fix syntax errors in be_between matcher --- src/spectator/dsl/matchers.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/spectator/dsl/matchers.cr b/src/spectator/dsl/matchers.cr index 2108a88..b533c5a 100644 --- a/src/spectator/dsl/matchers.cr +++ b/src/spectator/dsl/matchers.cr @@ -342,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. From 83ac420273fd83d259fcfee1ac065cccb7cd812d Mon Sep 17 00:00:00 2001 From: Davide Paolo Tua Date: Fri, 21 Feb 2020 13:19:27 +0100 Subject: [PATCH 74/78] Add test for fix, fix test for range_matcher --- .../expectations/be_between_matcher_spec.cr | 17 +++++++++++++++++ src/spectator/matchers/range_matcher.cr | 5 +++-- 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 spec/rspec/expectations/be_between_matcher_spec.cr diff --git a/spec/rspec/expectations/be_between_matcher_spec.cr b/spec/rspec/expectations/be_between_matcher_spec.cr new file mode 100644 index 0000000..913db0b --- /dev/null +++ b/spec/rspec/expectations/be_between_matcher_spec.cr @@ -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 diff --git a/src/spectator/matchers/range_matcher.cr b/src/spectator/matchers/range_matcher.cr index 32d491f..21737ae 100644 --- a/src/spectator/matchers/range_matcher.cr +++ b/src/spectator/matchers/range_matcher.cr @@ -13,13 +13,14 @@ module Spectator::Matchers # Returns a new matcher, with the same bounds, but uses an inclusive range. def inclusive - new_range = Range.new(range.begin, range.end, exclusive: false) - expected = TestValue.new(new_range, label) + label = expected.label + new_range = Range.new(range.begin, label) RangeMatcher.new(expected) end # 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) From 2bf66828162687d4844581bb216d605f20da4280 Mon Sep 17 00:00:00 2001 From: Davide Paolo Tua Date: Fri, 21 Feb 2020 13:23:33 +0100 Subject: [PATCH 75/78] Fix accidental deletion --- src/spectator/matchers/range_matcher.cr | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/spectator/matchers/range_matcher.cr b/src/spectator/matchers/range_matcher.cr index 21737ae..fe54b7e 100644 --- a/src/spectator/matchers/range_matcher.cr +++ b/src/spectator/matchers/range_matcher.cr @@ -14,7 +14,8 @@ 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, label) + new_range = Range.new(range.begin, range.end, exclusive: false) + expected = TestValue.new(new_range, label) RangeMatcher.new(expected) end From f5f1361477ee0ca9da0199caef70c93ce3f508d5 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Fri, 21 Feb 2020 09:57:09 -0700 Subject: [PATCH 76/78] Bump version to 0.9.8 Fixed GitHub Issue https://github.com/icy-arctic-fox/spectator/issues/5 - Thanks @jinn999 --- shard.yml | 2 +- src/spectator.cr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shard.yml b/shard.yml index 0ef85ad..53116a5 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: spectator -version: 0.9.7 +version: 0.9.8 description: | A feature-rich spec testing framework for Crystal with similarities to RSpec. diff --git a/src/spectator.cr b/src/spectator.cr index 295f091..8e58106 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -6,7 +6,7 @@ module Spectator extend self # Current version of the Spectator library. - VERSION = "0.9.7" + VERSION = "0.9.8" # Top-level describe method. # All specs in a file must be wrapped in this call. From e92aa7ed575946a165faadba3a8a7522936da99f Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 22 Feb 2020 22:59:09 -0700 Subject: [PATCH 77/78] Implicit subject should not use .new with modules Fixes GitHub issue https://github.com/icy-arctic-fox/spectator/issues/6 --- spec/subject_spec.cr | 10 ++++++++++ src/spectator/dsl/groups.cr | 12 +++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/spec/subject_spec.cr b/spec/subject_spec.cr index aa02a39..43c5018 100644 --- a/spec/subject_spec.cr +++ b/spec/subject_spec.cr @@ -2,6 +2,8 @@ require "./spec_helper" class Base; end +module SomeModule; end + Spectator.describe "Subject" do subject { Base.new } @@ -10,4 +12,12 @@ Spectator.describe "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 diff --git a/src/spectator/dsl/groups.cr b/src/spectator/dsl/groups.cr index 73c130f..5d112ac 100644 --- a/src/spectator/dsl/groups.cr +++ b/src/spectator/dsl/groups.cr @@ -19,12 +19,18 @@ 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)) && what.resolve? %} + {% if (what.is_a?(Path) || what.is_a?(Generic)) && (described_type = what.resolve?) %} macro described_class - {{what}} + {{described_type.name}} end - subject { described_class.new } + subject do + {% if described_type < Reference || described_type < Value %} + described_class.new + {% else %} + described_class + {% end %} + end {% else %} def _spectator_implicit_subject(*args) {{what}} From 5543c07766ae765481e4a99a20e4f57018d834d7 Mon Sep 17 00:00:00 2001 From: Michael Miller Date: Sat, 22 Feb 2020 22:59:42 -0700 Subject: [PATCH 78/78] Bump version to 0.9.9 --- shard.yml | 2 +- src/spectator.cr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/shard.yml b/shard.yml index 53116a5..4dc4b29 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: spectator -version: 0.9.8 +version: 0.9.9 description: | A feature-rich spec testing framework for Crystal with similarities to RSpec. diff --git a/src/spectator.cr b/src/spectator.cr index 8e58106..4a62896 100644 --- a/src/spectator.cr +++ b/src/spectator.cr @@ -6,7 +6,7 @@ module Spectator extend self # Current version of the Spectator library. - VERSION = "0.9.8" + VERSION = "0.9.9" # Top-level describe method. # All specs in a file must be wrapped in this call.