From e2a0ac93516ada9bf2fbdc3a1efa3b3735e05659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Busqu=C3=A9?= Date: Mon, 2 Aug 2021 08:57:18 +0200 Subject: [PATCH] Add stubbing test helpers for the event bus This commit adds a `Spree::TestingSupport::EventHelpers` module which adds stubbing helpers for `Spree::Event` when included in an RSpec file. It adds a RSpec matcher, `have_been_fired`, which can be used after the event bus has been stubbed through `stub_spree_events`. Internally, it leans on the `have_received` built-in `rspec-mocks` matcher. Unfortunately, RSpec's matcher lacks good extensibility, so we can't provide OOTB with all its bells and whistles (like `once`, `exactly(2).times`), and the effort is not worth it at this point. However, we do support a `with` modifier for `have_been_fired` to match the published payload. Example: ```ruby require 'rails_helper' require 'spree/testing_support/event_helpers' RSpec.describe MyClass do include Spree::TestingSupport::EventHelpers it 'does stuff with events' do stub_spree_events described_class.new.do_stuff_with_events expect('foo').to have_been_fired expect('bar').to have_been_fired.with(a_hash_containing(foo: :bar)) end end ``` --- .../spree/testing_support/event_helpers.rb | 103 ++++++++++++++++ .../testing_support/event_helpers_spec.rb | 110 ++++++++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 core/lib/spree/testing_support/event_helpers.rb create mode 100644 core/spec/lib/spree/core/testing_support/event_helpers_spec.rb diff --git a/core/lib/spree/testing_support/event_helpers.rb b/core/lib/spree/testing_support/event_helpers.rb new file mode 100644 index 00000000000..4dd45e060b6 --- /dev/null +++ b/core/lib/spree/testing_support/event_helpers.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require 'spree/event' + +module Spree + module TestingSupport + # RSpec test helpers for the event bus + # + # If you want to use the methods defined in this module, include it in your + # specs: + # + # @example + # require 'rails_helper' + # require 'spree/testing_support/event_helpers' + # + # RSpec.describe MyClass do + # include Spree::TestingSupport::EventHelpers + # end + # + # or, globally, in your `spec_helper.rb`: + # + # @example + # require 'spree/testing_support/event_helpers' + # + # RSpec.configure do |config| + # config.include Spree::TestingSupport::EventHelpers + # end + module EventHelpers + extend RSpec::Matchers::DSL + + # Stubs {Spree::Event} + # + # After you have called this method in an example, {Spree::Event} will no + # longer listen to any event for the duration of that example. All the + # method invocations on it will be spied but not performed. + # + # Internally, it stubs {Spree::Event} to a class spy of itself. + # + # After you call this method, probably you'll want to call some of the + # matchers defined in this module. + def stub_spree_events + stub_const('Spree::Event', class_spy(Spree::Event)) + end + + # @!method have_been_fired(event_name) + # Matcher to test that an event has been fired via {Spree::Event#fire} + # + # Before using this matcher, you need to call {#stub_spree_events}. + # + # Remember that the event listeners won't be performed. + # + # @example + # it 'fires foo event' do + # stub_spree_events + # + # Spree::Event.fire 'foo' + # + # expect('foo').to have_been_fired + # end + # + # It can be chain through `with` to match with the published payload: + # + # @example + # it 'fires foo event with the expected payload' do + # stub_spree_events + # + # Spree::Event.fire 'foo', bar: :baz, qux: :quux + # + # expect('foo').to have_been_fired.with(a_hash_including(bar: :baz)) + # end + # + # @param [String, Symbol] event_name + matcher :have_been_fired do + chain :with, :payload + + match do |expected_event| + expected_event = normalize_name(expected_event) + arguments = payload ? [expected_event, payload] : [expected_event, any_args] + expect(Spree::Event).to have_received(:fire).with(*arguments) + end + + failure_message do |expected_event| + <<~MSG + expected #{expected_event.inspect} to have been fired. + Make sure that provided payload, if any, also matches. + MSG + end + + def normalize_name(event_name) + if event_name.is_a?(String) + eq(event_name).or(eq(event_name.to_sym)) + elsif event_name.is_a?(Symbol) + eq(event_name).or(eq(event_name.to_s)) + else + raise ArgumentError, <<~MSG + "#{event_name.inspect} is not a valid event name. It must be a String or a Symbol." + MSG + end + end + end + end + end +end diff --git a/core/spec/lib/spree/core/testing_support/event_helpers_spec.rb b/core/spec/lib/spree/core/testing_support/event_helpers_spec.rb new file mode 100644 index 00000000000..db5c3960ff9 --- /dev/null +++ b/core/spec/lib/spree/core/testing_support/event_helpers_spec.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'spree/testing_support/event_helpers' + +RSpec.describe Spree::TestingSupport::EventHelpers do + include described_class + + describe '#stub_spree_events' do + it 'creates a spy class from Spree::Event and assigns to itself' do + stub_spree_events + + expect(Spree::Event.inspect).to include('ClassDouble') + + Spree::Event.fire 'foo' + + expect(Spree::Event).to have_received(:fire) + end + end + + describe '#have_been_fired' do + it "matches when the event has been fired without payload and there's no expectation on it" do + stub_spree_events + + Spree::Event.fire 'foo' + + expect('foo').to have_been_fired + end + + it "matches when the event has been fired with payload but there's no expectation on it" do + stub_spree_events + + Spree::Event.fire 'foo', bar: :baz + + expect('foo').to have_been_fired + end + + it "matches when the event has been fired with payload and the expectation on it matches" do + stub_spree_events + + Spree::Event.fire 'foo', bar: :baz + + expect('foo').to have_been_fired.with(bar: :baz) + end + + it "matches when fired as string and matched as string" do + stub_spree_events + + Spree::Event.fire 'foo' + + expect('foo').to have_been_fired + end + + it "matches when fired as string but matched as symbol" do + stub_spree_events + + Spree::Event.fire 'foo' + + expect(:foo).to have_been_fired + end + + it "matches when fired as symbol but matched as string" do + stub_spree_events + + Spree::Event.fire :foo + + expect('foo').to have_been_fired + end + + it "matches when fired as symbol and matched as symbol" do + stub_spree_events + + Spree::Event.fire :foo + + expect(:foo).to have_been_fired + end + + it "can match payload with an inner matcher" do + stub_spree_events + + Spree::Event.fire 'foo', bar: :baz, tar: :tar + + expect('foo').to have_been_fired.with(a_hash_including(bar: :baz)) + end + + it "doesn't match when the event hasn't been fired" do + stub_spree_events + + expect { + expect('foo').to have_been_fired + }.to raise_error /expected "foo" to have been fired/ + end + + it "doesn't match when the event has been fired but the payload doesn't match" do + stub_spree_events + + Spree::Event.fire 'foo', foo: :bar + + expect { + expect('foo').to have_been_fired.with(bar: :baz) + }.to raise_error /Make sure that provided payload.*also matches/ + end + + it "raises when expected event is not a valid name" do + expect { + expect([]).to have_been_fired.with(bar: :baz) + }.to raise_error /not a valid event name/ + end + end +end