-
Notifications
You must be signed in to change notification settings - Fork 257
Testing your application
To test your application using an event store and read model projection you can take advantage of ExUnit's case template feature to have the databases reset between each test execution. This guarantees that each test starts from a known good state and one test won't affect any other.
First, define a DataCase module which is used to reset the event store and read store databases after each test run using the on_exit/0 callback:
# test/support/data_case.ex
defmodule MyApp.DataCase do
use ExUnit.CaseTemplate
using do
quote do
import Ecto
import Ecto.Changeset
import Ecto.Query
import Commanded.Assertions.EventAssertions
end
end
setup do
{:ok, _} = Application.ensure_all_started(:my_app)
on_exit(fn ->
:ok = Application.stop(:my_app)
MyApp.Storage.reset!()
end)
:ok
end
endThe DataCase module uses the following Storage.reset!/0 function to:
- Reset the Postgres EventStore database.
- Truncate the listed tables in the read store database.
Rename table1, table2, and table3 to your own table names and remember to include any new tables when added to your app.
# test/support/storage.ex
defmodule MyApp.Storage do
@doc """
Clear the event store and read store databases
"""
def reset! do
reset_eventstore()
reset_readstore()
end
defp reset_eventstore do
config = MyEventStore.config()
{:ok, conn} = Postgrex.start_link(config)
EventStore.Storage.Initializer.reset!(conn, config)
end
defp reset_readstore do
config = Application.get_env(:my_app, MyApp.Repo)
{:ok, conn} = Postgrex.start_link(config)
Postgrex.query!(conn, truncate_readstore_tables(), [])
end
defp truncate_readstore_tables do
"""
TRUNCATE TABLE
table1,
table2,
table3
RESTART IDENTITY
CASCADE;
"""
end
endYou need to include the test/support files in the test environment Elixir paths by adding the following elixirc_paths/1 function to your app's mix.exs file:
# mix.exs
defmodule MyApp.Mixfile do
use Mix.Project
# Include `test/support` files in test environment
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
endFinally, you can use the MyApp.DataCase case template within any test modules that require access to the databases:
# test/example_test.exs
defmodule MyApp.ExampleTest do
use MyApp.DataCase
# Each test will be run against clean read and write databases.
# After test execution (regardless of success or failure) the databases will be reset.
endRun your tests using: mix test
When these tests run they will execute against empty event store and read store databases. The caveat with the approach is that the databases will be reset after your tests run; it won't be possible to look at data contained within database after a test fails to debug the failure. The workaround is to temporarily disable the reset operation, run a single failing test, and then you will be able to look at the data. Note that the next time you run any test it will fail because the databases won't have been reset. Subsequent tests will behave normally, assuming you reinstate the reset behaviour.
You can use the in-memory event store to test your application. You can also use an ExUnit CaseTemplate to have the in-memory event store restarted between each test run.
You can configure environment specific consistency setting for Commanded event handlers:
# config/config.exs
use Mix.Config
config :my_app, consistency: :eventual# config/test.exs
use Mix.Config
config :my_app, consistency: :strongThen read the setting when defining your event handlers and process managers:
defmodule ExampleEventHandler do
use Commanded.Event.Handler,
name: __MODULE__,
consistency: Application.get_env(:my_app, :consistency, :eventual)
endIn your test you can append events to the aggregate's event stream to setup its given state. Use Commanded.EventStore.append_to_stream/3 to append events directly to the event store you've configured to use with Commanded. This allows you to configure a different event store for each environment (e.g. in-memory event store for test env).
You need to map your app's domain events to Commanded.EventStore.EventData structs as follows:
causation_id = UUID.uuid4()
correlation_id = UUID.uuid4()
event_data =
Enum.map(events, fn -> event
%Commanded.EventStore.EventData{
causation_id: causation_id,
correlation_id: correlation_id,
event_type: Commanded.EventStore.TypeProvider.to_string(event),
data: event,
metadata: %{},
}
)
{:ok, _} = Commanded.EventStore.append_to_stream(stream_uuid, expected_version, event_data)The stream_uuid will be your aggregate's identity and expected_version is the aggregate version (count of events already appended to its stream, use 0 when creating a new aggregate).
Once you've appended the events, you can dispatch the command via your router. The aggregate process will be started, it'll fetch its events, including those you just appended, and then handle the command.