defmodule OnePiece.Commanded.TestSupport.CommandHandlerCase do @moduledoc ~S""" This module helps with test cases for testing aggregate states, and command handlers. ## Usage After import the test support file, you should be able to have the module in your test files. defmodule MyAggregateTest do use OnePiece.Commanded.TestSupport.CommandHandlerCase, aggregate: MyAggregate, # You can also pass `handler` if you are using Command Handler modules # handler: MyCommandHandler, async: true describe "my aggregate" do test "should do something" do assert_events( [%InitialEvent{}] %DoSomething{}, [%SomethingHappened{}] ) end test "the state" do assert_state( [%InitialEvent{}] %DoSomething{}, %MyAggregate{} ) end test "the error" do assert_error( [%InitialEvent{}] %DoSomething{}, :already_exists ) end end end """ use ExUnit.CaseTemplate alias Commanded.Aggregate.Multi alias OnePiece.Commanded.TestSupport.CommandHandlerCase using opts do quote do @aggregate Keyword.fetch!(unquote(opts), :aggregate) @handler Keyword.get(unquote(opts), :handler, nil) defp assert_events(initial_events, command, expected_events) do CommandHandlerCase.assert_events( initial_events, command, expected_events, @aggregate, @handler ) end defp assert_state(initial_events, command, expected_state) do CommandHandlerCase.assert_state( initial_events, command, expected_state, @aggregate, @handler ) end defp assert_error(initial_events, command, expected_error) do CommandHandlerCase.assert_error( initial_events, command, expected_error, @aggregate, @handler ) end end end def assert_events( initial_events, command, expected_events, aggregate_module, command_handler_module ) do assert {:ok, _state, events} = aggregate_run( aggregate_module, command_handler_module, initial_events, command ) actual_events = List.wrap(events) expected_events = List.wrap(expected_events) assert actual_events == expected_events end def assert_state( initial_events, command, expected_state, aggregate_module, command_handler_module ) do assert {:ok, state, _events} = aggregate_run( aggregate_module, command_handler_module, initial_events, command ) assert state == expected_state end def assert_error( initial_events, command, expected_error, aggregate_module, command_handler_module ) do assert {:error, reason} = aggregate_run( aggregate_module, command_handler_module, initial_events, command ) assert reason == expected_error end defp aggregate_run(aggregate_module, command_handler_module, initial_events, command) do evolver = &aggregate_module.apply/2 decider = if command_handler_module == nil, do: &aggregate_module.execute/2, else: &command_handler_module.handle/2 aggregate_module |> struct() |> evolve(initial_events, evolver) |> execute(command, evolver, decider) end defp execute(state, command, evolver, decider) do try do {new_state, events} = state |> decider.(command) |> process_response() |> maybe_evolve(state, evolver) {:ok, new_state, events} catch {:error, _error} = reply -> reply end end defp process_response({:error, _error} = error) do throw(error) end defp process_response(%Multi{} = multi) do case Multi.run(multi) do {:error, _reason} = error -> throw(error) {state, events} -> {state, events} end end defp process_response(no_events) when no_events in [:ok, nil, []] do [] end defp process_response({:ok, events}) do events end defp process_response(events) when is_list(events) do events end defp process_response(event) when is_map(event) do [event] end defp process_response(invalid) do flunk("unexpected: " <> inspect(invalid)) end defp maybe_evolve({state, events} = _multi_response, _state, _evolver) do {state, events} end defp maybe_evolve(events, state, evolver) do {evolve(state, events, evolver), events} end defp evolve(state, events, evolver) do events |> List.wrap() |> Enum.reduce(state, &evolver.(&2, &1)) end end