-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2 from chriscz/feature/add-minitest-assertions
Implement assertions for Minitest
- Loading branch information
Showing
10 changed files
with
471 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
name: Build | ||
|
||
on: | ||
push: | ||
branches: | ||
- master | ||
pull_request: | ||
|
||
jobs: | ||
minitest: | ||
runs-on: ubuntu-latest | ||
env: | ||
BUNDLE_JOBS: 4 | ||
BUNDLE_RETRY: 3 | ||
BUNDLE_GEMFILE: ${{ matrix.gemfile }} | ||
CI: true | ||
strategy: | ||
fail-fast: false | ||
matrix: | ||
ruby: ["3.0"] | ||
gemfile: [ | ||
"gemfiles/rails6.gemfile", | ||
] | ||
include: | ||
# BLOCKED: https://github.com/zdennis/activerecord-import/issues/736 | ||
# - ruby: "3.0" | ||
# gemfile: "gemfiles/railsmaster.gemfile" | ||
- ruby: "2.6" | ||
gemfile: "gemfiles/rails6.gemfile" | ||
steps: | ||
- uses: actions/checkout@v2 | ||
- name: Install system deps | ||
run: | | ||
sudo apt-get update | ||
sudo apt-get install libsqlite3-dev | ||
- uses: ruby/setup-ruby@v1 | ||
with: | ||
ruby-version: ${{ matrix.ruby }} | ||
bundler-cache: true | ||
- name: Run Minitest | ||
run: | | ||
bundle exec rake test |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,3 +40,5 @@ Gemfile.local | |
|
||
tmp/ | ||
.rbnext/ | ||
|
||
gemfiles/*.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
# frozen_string_literal: true | ||
|
||
require "active_event_store/test_helper/event_published_matcher" | ||
|
||
module ActiveEventStore | ||
module TestHelper | ||
extend ActiveSupport::Concern | ||
|
||
included do | ||
include ActiveJob::TestHelper | ||
end | ||
|
||
# Asserts that the given event was published `exactly`, `at_least` or `at_most` number of times | ||
# to a specific `store` `with` a particular hash of attributes. | ||
def assert_event_published(expected_event, store: nil, with: nil, exactly: nil, at_least: nil, at_most: nil, &block) | ||
matcher = EventPublishedMatcher.new( | ||
expected_event, | ||
store: store, | ||
with: with, | ||
exactly: exactly, | ||
at_least: at_least, | ||
at_most: at_most | ||
) | ||
|
||
if (msg = matcher.matches?(block)) | ||
fail(msg) | ||
end | ||
|
||
matcher.matching_events | ||
end | ||
|
||
# Asserts that the given event was *not* published `exactly`, `at_least` or `at_most` number of times | ||
# to a specific `store` `with` a particular hash of attributes. | ||
def refute_event_published(expected_event, store: nil, with: nil, exactly: nil, at_least: nil, at_most: nil, &block) | ||
matcher = EventPublishedMatcher.new( | ||
expected_event, | ||
store: store, | ||
with: with, | ||
exactly: exactly, | ||
at_least: at_least, | ||
at_most: at_most, | ||
refute: true | ||
) | ||
|
||
if (msg = matcher.matches?(block)) | ||
fail(msg) | ||
end | ||
end | ||
|
||
def assert_async_event_subscriber_enqueued(subscriber_class, event: nil, queue: "events_subscribers", &block) | ||
subscriber_job = ActiveEventStore::SubscriberJob.for(subscriber_class) | ||
if subscriber_job.nil? | ||
fail("No such async subscriber: #{subscriber_class.name}") | ||
end | ||
|
||
expected_event = event | ||
event_matcher = ->(actual_event) { EventPublishedMatcher.event_matches?(expected_event, expected_event.data, actual_event) } | ||
|
||
expected_args = if expected_event | ||
event_matcher | ||
end | ||
|
||
assert_enqueued_with(job: subscriber_job, queue: queue, args: expected_args) do | ||
ActiveRecord::Base.transaction do | ||
block.call | ||
end | ||
end | ||
end | ||
end | ||
end |
150 changes: 150 additions & 0 deletions
150
lib/active_event_store/test_helper/event_published_matcher.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
# frozen_string_literal: true | ||
|
||
module ActiveEventStore | ||
module TestHelper | ||
class EventPublishedMatcher | ||
attr_reader :attributes, | ||
:matching_events | ||
|
||
def initialize(expected_event_class, store: nil, with: nil, exactly: nil, at_least: nil, at_most: nil, refute: false) | ||
@event_class = expected_event_class | ||
@store = store || ActiveEventStore.event_store | ||
@attributes = with | ||
@refute = refute | ||
|
||
count_expectations = { | ||
exactly: exactly, | ||
at_most: at_most, | ||
at_least: at_least | ||
}.reject { |_, v| v.nil? } | ||
|
||
if count_expectations.length > 1 | ||
raise ArgumentError("Only one of :exactly, :at_least or :at_most can be specified") | ||
elsif count_expectations.length == 0 | ||
@count_expectation_kind = :at_least | ||
@expected_count = 1 | ||
else | ||
@count_expectation_kind = count_expectations.keys.first | ||
@expected_count = count_expectations.values.first | ||
end | ||
end | ||
|
||
def with_published_events(&block) | ||
original_count = @store.read.count | ||
block.call | ||
in_block_events(original_count, @store.read.count) | ||
end | ||
|
||
def matches?(block) | ||
raise ArgumentError, "#{assertion_name} only support block assertions" if block.nil? | ||
|
||
events = with_published_events do | ||
block.call | ||
end | ||
|
||
@matching_events, @unmatching_events = partition_events(events) | ||
|
||
mismatch_message = count_mismatch_message(@matching_events.size) | ||
|
||
if mismatch_message | ||
expectations = [ | ||
"Expected #{mismatch_message} #{@event_class.identifier}" | ||
] | ||
|
||
expectations << if refute? | ||
report_events = @matching_events | ||
"not to have been published" | ||
else | ||
report_events = @unmatching_events | ||
"to have been published" | ||
end | ||
|
||
expectations << "with attributes #{attributes.inspect}" unless attributes.nil? | ||
|
||
expectations << expectations.pop + ", but" | ||
|
||
expectations << if report_events.any? | ||
report_events.inject("published the following events instead:") do |msg, event| | ||
msg + "\n #{event.inspect}" | ||
end | ||
else | ||
"hasn't published anything" | ||
end | ||
|
||
return expectations.join(" ") | ||
end | ||
|
||
nil | ||
end | ||
|
||
private | ||
|
||
def refute? | ||
@refute | ||
end | ||
|
||
def assertion_name | ||
if refute? | ||
"refute_event_published" | ||
else | ||
"assert_event_published" | ||
end | ||
end | ||
|
||
def negate_on_refute(cond) | ||
if refute? | ||
!cond | ||
else | ||
cond | ||
end | ||
end | ||
|
||
def in_block_events(before_block_count, after_block_count) | ||
count_difference = after_block_count - before_block_count | ||
if count_difference.positive? | ||
@store.read.backward.limit(count_difference).to_a | ||
else | ||
[] | ||
end | ||
end | ||
|
||
# Partitions events into matching and unmatching | ||
def partition_events(events) | ||
events.partition { |e| self.class.event_matches?(@event_class, @attributes, e) } | ||
end | ||
|
||
def count_mismatch_message(actual_count) | ||
case @count_expectation_kind | ||
when :exactly | ||
if negate_on_refute(actual_count != @expected_count) | ||
"exactly #{@expected_count}" | ||
end | ||
when :at_most | ||
if negate_on_refute(actual_count > @expected_count) | ||
"at most #{@expected_count}" | ||
end | ||
when :at_least | ||
if negate_on_refute(actual_count < @expected_count) | ||
"at least #{@expected_count}" | ||
end | ||
else | ||
raise ArgumentError, "Unrecognized expectation kind: #{@count_expectation_kind}" | ||
end | ||
end | ||
|
||
class << self | ||
def event_matches?(event_class, attributes, event) | ||
event_type_matches?(event_class, event) && event_data_matches?(attributes, event) | ||
end | ||
|
||
def event_type_matches?(event_class, event) | ||
event_class.identifier == event.event_type | ||
end | ||
|
||
def event_data_matches?(attributes, event) | ||
(attributes.nil? || attributes.all? { |k, v| v == event.public_send(k) }) | ||
end | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.