commanded / recipes

Commanded recipes
12 stars 1 forks source link

[Recipe] Testing read model projectors #18

Open slashdotdash opened 4 years ago

slashdotdash commented 4 years ago

How can I test a read model projector so that tests wait until events have been projected?

Using Elixir telemetry and the after_update/3 callback function

Commanded Ecto projections provides an after_update/3 callback function. This gets called after each event is projected. You can use the funtion to publish a notification whenever an event is projected, including the database changes.

Here’s an example using the Elixir/Erlang telemetry library.

In the projector:

def after_update(event, metadata, changes) do
  :telemetry.execute(
    [:projector],
    %{system_time: System.system_time()},
    %{event: event, metadata: metadata, changes: changes, projector: __MODULE__}
  )
end

In a test:

setup do
  reply_to = self()
  :ok =
    :telemetry.attach(
      "test-handler",
      [:projector],
      fn event, measurements, metadata, reply_to ->
        send(reply_to, {:telemetry, event, measurements, metadata})
      end,
      self()
    )
end

Usage in test to wait:

assert_receive {:telemetry, [:projector], _measurements, %{event: event, metadata: metadata, changes: changes}}

This approach will ensure the test blocks until the projector has done its work and then you can run either use the changes from the Ecto.Multi or run a query to verify the database changes. You can also use pattern matching on the event from the telemetry metadata to wait for a particular event type.

The performance impact at runtime is negligible if there are no handlers attached to a telemetry event.

satom99 commented 9 months ago

Commanded Ecto projectors are regular Commanded.Event.Handlers under the hood.

Recipe #15 explains how event handlers can be tested, and includes the following example:

    # Or call handler directly with an event (doesn’t require the handler process to be started)
    :ok = CustomerPromotionHandler.handle(event, metadata)

Which shows that event handlers can be tested by calling the handle/2 callback directly. Rather than dispatching an event.

Now take one of the examples in the commanded-ecto-projection documentation. We could do the following in tests:

      metadata = %{
        event_number: 1,
        handler_name: "fake-handler-name"
      }

      :ok = MyApp.ExampleProjector.handle(event, metadata)

So we can test Commanded Ecto projections directly by calling the handle/2 function on the projector. There is no need to depend on command dispatches, nor telemetry events to wait and acknowledge that the projector was called. We can simply build an event, call the projector, and that's about it.

@slashdotdash Wdyt? Would this be a recommended approach?