Skip to content

Commit

Permalink
Merge pull request commanded#347 from commanded/feature/refute-receiv…
Browse files Browse the repository at this point in the history
…e-event

`refute_receive_event/4` only tests newly created events
  • Loading branch information
slashdotdash authored Feb 20, 2020
2 parents ea8fefb + f491ec2 commit 711593f
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 69 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- Allow `Commanded.Aggregate.Multi` to return events as `:ok` tagged tuples.
- Run the formatter in CI ([#341](https://github.com/commanded/commanded/pull/341)).
- Add stacktraces to EventHandler error logging ([#340](https://github.com/commanded/commanded/pull/340))
- `refute_receive_event/4` only tests newly created events ([#347](https://github.com/commanded/commanded/pull/347)).

### Bug fixes

Expand Down
138 changes: 84 additions & 54 deletions lib/commanded/assertions/event_assertions.ex
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ defmodule Commanded.Assertions.EventAssertions do
end)
"""
def assert_receive_event(application, event_type, assertion_fn) do
def assert_receive_event(application, event_type, assertion_fn)
when is_function(assertion_fn, 1) or is_function(assertion_fn, 2) do
assert_receive_event(application, event_type, fn _event -> true end, assertion_fn)
end

Expand All @@ -69,7 +70,8 @@ defmodule Commanded.Assertions.EventAssertions do
end)
"""
def assert_receive_event(application, event_type, predicate_fn, assertion_fn) do
def assert_receive_event(application, event_type, predicate_fn, assertion_fn)
when is_function(assertion_fn, 1) or is_function(assertion_fn, 2) do
unless Code.ensure_loaded?(event_type) do
raise ExUnit.AssertionError, "Event #{inspect(event_type)} not found"
end
Expand All @@ -87,49 +89,73 @@ defmodule Commanded.Assertions.EventAssertions do
## Examples
Refute that `ExampleEvent` is created by `some_func/0` function:
Refute that `ExampleEvent` is produced by given anonymous function:
refute_receive_event(ExampleApp, ExampleEvent) do
some_func()
end
refute_receive_event(ExampleApp, ExampleEvent, fn ->
:ok = MyApp.dispatch(command)
end)
Refute that `ExampleEvent` is produced by `some_func/0` function:
refute_receive_event(ExampleApp, ExampleEvent, &some_func/0)
Refute that `ExampleEvent` matching given predicate is created by
`some_func/0` function:
Refute that `ExampleEvent` matching given `event_matches?/1` predicate funtion
is produced by `some_func/0` function:
refute_receive_event(ExampleApp, ExampleEvent, &some_func/0,
predicate: &event_matches?/1
)
refute_receive_event(ExampleApp, ExampleEvent,
predicate: fn event -> event.foo == :foo end) do
some_func()
end
Refute that `ExampleEvent` matching given anonymous predicate funtion
is produced by `some_func/0` function:
refute_receive_event(ExampleApp, ExampleEvent, &some_func/0,
predicate: fn event -> event.value == 1 end
)
Refute that `ExampleEvent` produced by `some_func/0` function is published to
a given stream:
refute_receive_event(ExampleApp, ExampleEvent, &some_func/0,
predicate: fn event -> event.value == 1 end,
stream: "foo-1234"
)
"""
defmacro refute_receive_event(application, event_type, opts \\ [], do: block) do
predicate = Keyword.get(opts, :predicate)

def refute_receive_event(application, event_type, refute_fn, opts \\ [])
when is_function(refute_fn, 0) do
predicate_fn = Keyword.get(opts, :predicate) || fn _event -> true end
timeout = Keyword.get(opts, :timeout, default_refute_receive_timeout())
subscription_opts = Keyword.take(opts, [:stream]) |> Keyword.put(:start_from, :current)
reply_to = self()
ref = make_ref()

# Start a task to subscribe and verify received events
task =
Task.async(fn ->
with_subscription(
application,
fn subscription ->
send(reply_to, {:subscribed, ref})

quote do
task =
Task.async(fn ->
with_subscription(unquote(application), fn subscription ->
predicate = unquote(predicate) || fn _event -> true end

do_refute_receive_event(
unquote(application),
subscription,
unquote(event_type),
predicate
)
end)
end)
do_refute_receive_event(application, subscription, event_type, predicate_fn)
end,
subscription_opts
)
end)

# Wait until subscription has subscribed before executing refute function,
# otherwise we might not receive a matching event.
assert_receive {:subscribed, ^ref}, default_receive_timeout()

unquote(block)
refute_fn.()

case Task.yield(task, unquote(timeout)) || Task.shutdown(task) do
{:ok, :ok} -> :ok
{:ok, {:error, event}} -> flunk("Unexpectedly received event: " <> inspect(event))
{:error, error} -> flunk("Encountered an error: " <> inspect(error))
{:exit, error} -> flunk("Encountered an error: " <> inspect(error))
nil -> :ok
end
case Task.yield(task, timeout) || Task.shutdown(task) do
{:ok, :ok} -> :ok
{:ok, {:error, event}} -> flunk("Unexpectedly received event: " <> inspect(event))
{:exit, error} -> flunk("Encountered an error: " <> inspect(error))
nil -> :ok
end
end

Expand Down Expand Up @@ -163,19 +189,22 @@ defmodule Commanded.Assertions.EventAssertions do
end

@doc false
def with_subscription(application, callback_fun) when is_function(callback_fun, 1) do
def with_subscription(application, callback_fn, opts \\ [])
when is_function(callback_fn, 1) do
subscription_name = UUID.uuid4()
stream = Keyword.get(opts, :stream, :all)
start_from = Keyword.get(opts, :start_from, :origin)

{:ok, subscription} =
EventStore.subscribe_to(application, :all, subscription_name, self(), :origin)
EventStore.subscribe_to(application, stream, subscription_name, self(), start_from)

assert_receive {:subscribed, ^subscription}, default_receive_timeout()

try do
apply(callback_fun, [subscription])
callback_fn.(subscription)
after
:ok = EventStore.unsubscribe(application, subscription)
:ok = EventStore.delete_subscription(application, :all, subscription_name)
:ok = EventStore.delete_subscription(application, stream, subscription_name)
end
end

Expand All @@ -184,13 +213,13 @@ defmodule Commanded.Assertions.EventAssertions do

case find_expected_event(received_events, event_type, predicate_fn) do
%RecordedEvent{data: data} = expected_event ->
case assertion_fn do
assertion_fn when is_function(assertion_fn, 1) ->
apply(assertion_fn, [data])
args =
cond do
is_function(assertion_fn, 1) -> [data]
is_function(assertion_fn, 2) -> [data, expected_event]
end

assertion_fn when is_function(assertion_fn, 2) ->
apply(assertion_fn, [data, expected_event])
end
apply(assertion_fn, args)

nil ->
:ok = ack_events(application, subscription, received_events)
Expand All @@ -199,7 +228,7 @@ defmodule Commanded.Assertions.EventAssertions do
end
end

def do_refute_receive_event(application, subscription, event_type, predicate_fn) do
defp do_refute_receive_event(application, subscription, event_type, predicate_fn) do
receive do
{:events, events} ->
case find_expected_event(events, event_type, predicate_fn) do
Expand Down Expand Up @@ -230,13 +259,14 @@ defmodule Commanded.Assertions.EventAssertions do

defp find_expected_event(received_events, event_type, predicate_fn) do
Enum.find(received_events, fn
%RecordedEvent{data: %{__struct__: ^event_type} = data}
when is_function(predicate_fn, 1) ->
apply(predicate_fn, [data])

%RecordedEvent{data: %{__struct__: ^event_type} = data} = received_event
when is_function(predicate_fn, 2) ->
apply(predicate_fn, [data, received_event])
%RecordedEvent{data: %{__struct__: ^event_type} = data} = received_event ->
args =
cond do
is_function(predicate_fn, 1) -> [data]
is_function(predicate_fn, 2) -> [data, received_event]
end

apply(predicate_fn, args)

%RecordedEvent{} ->
false
Expand Down
76 changes: 61 additions & 15 deletions test/event_assertions_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -82,40 +82,86 @@ defmodule Commanded.EventAssertionsTest do

describe "refute_receive_event/1" do
test "should succeed when event not received" do
refute_receive_event(DefaultApp, Event) do
refute_receive_event(DefaultApp, Event, fn ->
append_events("stream1", [%AnotherEvent{data: 1}])
end
end)
end

test "should succeed when event not received matching predicate" do
refute_receive_event(DefaultApp, Event, predicate: fn %Event{data: data} -> data == 3 end) do
append_events("stream1", [%Event{data: 1}, %Event{data: 2}])
end
refute_receive_event(
DefaultApp,
Event,
fn ->
append_events("stream1", [%Event{data: 1}, %Event{data: 2}])
end,
predicate: fn %Event{data: data} -> data == 3 end
)
end

test "should ignore events appending to another stream" do
append_events("stream1", [%Event{data: 1}])

refute_receive_event(
DefaultApp,
Event,
fn ->
append_events("stream1", [%AnotherEvent{data: 2}])
append_events("stream2", [%Event{data: 2}])
end,
stream: "stream1"
)
end

test "should ignore events previously appended to stream" do
append_events("stream1", [%Event{data: 1}])

refute_receive_event(DefaultApp, Event, fn ->
append_events("stream1", [%AnotherEvent{data: 2}])
end)
end

test "should fail when event received" do
assert_raise ExUnit.AssertionError,
"\n\nUnexpectedly received event: #{inspect(%Event{data: 1})}\n",
fn ->
refute_receive_event(DefaultApp, Event) do
refute_receive_event(DefaultApp, Event, fn ->
append_events("stream1", [%Event{data: 1}])
end
end)
end
end

test "should fail when event received on stream" do
assert_raise ExUnit.AssertionError,
"\n\nUnexpectedly received event: #{inspect(%Event{data: 1})}\n",
fn ->
refute_receive_event(
DefaultApp,
Event,
fn ->
append_events("stream1", [%AnotherEvent{data: 1}])
append_events("stream2", [%Event{data: 1}])
end,
stream: "stream2"
)
end
end

test "should fail when event received matching predicate" do
assert_raise ExUnit.AssertionError,
"\n\nUnexpectedly received event: #{inspect(%Event{data: 3})}\n",
fn ->
refute_receive_event(DefaultApp, Event,
refute_receive_event(
DefaultApp,
Event,
fn ->
append_events("stream1", [
%Event{data: 1},
%Event{data: 2},
%Event{data: 3}
])
end,
predicate: fn %Event{data: data} -> data == 3 end
) do
append_events("stream1", [
%Event{data: 1},
%Event{data: 2},
%Event{data: 3}
])
end
)
end
end
end
Expand Down

0 comments on commit 711593f

Please sign in to comment.