Ash.Signals.Test (ash v3.36.1)

Copy Markdown View Source

Test helpers for asserting on emitted signals, and for testing listeners.

assert_emits_signal/4 and refute_emits_signal/4 check the signals emitted while running a function, and need no setup:

import Ash.Signals.Test

test "creating a post emits post_created" do
  post =
    assert_emits_signal MyApp.Blog.Signals, :post_created, %{title: "Hello"}, fn ->
      MyApp.Blog.create_post!("Hello")
    end

  assert post.title == "Hello"
end

assert_signal_emitted/4 and refute_signal_emitted/4 check the signals emitted so far, and require calling capture_signals/1 first:

setup :capture_signals

test "creating a post emits post_created" do
  %{id: post_id} = MyApp.Blog.create_post!("Hello")

  assert_signal_emitted MyApp.Blog.Signals, :post_created, %{post_id: ^post_id}
end

Only signals emitted by the test process, or by processes it started (that have it in their $callers, like tasks and LiveViews under test), are seen. No other process is sent anything, and listeners still run as usual.

Patterns match against the signal struct, and may use pins or bind variables.

To test listeners on their own, react_to_signal/4 delivers a signal to them as if it had been emitted:

test "placing an order awards points" do
  :ok =
    react_to_signal(
      MyApp.Shop.Signals,
      :order_placed,
      %{order_id: order.id, customer_id: customer.id},
      only: MyApp.Loyalty.Account
    )

  assert MyApp.Loyalty.points!(customer) == 100
end

To also match on who emitted a signal, receive the messages sent after capture_signals/1 directly. They have the shape {:ash_signal_emitted, signal_module, signal_name, signal, metadata}, where metadata is the metadata of the [:ash, :signal, :emitted] telemetry event, see Ash.Signals.

Summary

Functions

Runs the function, asserts that it emitted a signal matching the given pattern, and returns the function's result. Variables bound by the pattern are available afterwards.

Asserts that a signal matching the given pattern was emitted since capture_signals/1 was called, and returns the signal.

Sends signals emitted by the current test, or by processes it started, to the test process, for assert_signal_emitted/4 and refute_signal_emitted/4.

Delivers a signal to its listeners as if it had been emitted with the given payload, so listeners can be tested without the action that emits it. Pass a list of payloads to deliver signals emitted together, like a bulk action does.

Runs the function, asserts that it did not emit a signal matching the given pattern, and returns the function's result.

Asserts that no signal matching the given pattern was emitted since capture_signals/1 was called.

Functions

assert_emits_signal(signal_module, signal_name, pattern \\ quote do _ end, fun)

(macro)

Runs the function, asserts that it emitted a signal matching the given pattern, and returns the function's result. Variables bound by the pattern are available afterwards.

assert_emits_signal MyApp.Blog.Signals, :post_created, fn ->
  MyApp.Blog.create_post!("Hello")
end

post =
  assert_emits_signal MyApp.Blog.Signals, :post_created, %{post_id: post_id}, fn ->
    MyApp.Blog.create_post!("Hello")
  end

assert post.id == post_id

assert_signal_emitted(signal_module, signal_name, pattern \\ quote do _ end, timeout \\ 0)

(macro)

Asserts that a signal matching the given pattern was emitted since capture_signals/1 was called, and returns the signal.

assert_signal_emitted MyApp.Blog.Signals, :post_created
assert_signal_emitted MyApp.Blog.Signals, :post_created, %{title: "Hello"}

%{post_id: post_id} = assert_signal_emitted MyApp.Blog.Signals, :post_created

The optional timeout, defaulting to 0, waits for signals emitted by other processes.

capture_signals(context \\ %{})

@spec capture_signals(map()) :: :ok

Sends signals emitted by the current test, or by processes it started, to the test process, for assert_signal_emitted/4 and refute_signal_emitted/4.

Can be used as setup :capture_signals. Stops when the test exits.

react_to_signal(signal_module, signal_name, payload_or_payloads, opts \\ [])

@spec react_to_signal(
  Ash.Signals.t(),
  atom(),
  map() | Keyword.t() | [map()],
  Keyword.t()
) ::
  :ok | {:error, term()}

Delivers a signal to its listeners as if it had been emitted with the given payload, so listeners can be tested without the action that emits it. Pass a list of payloads to deliver signals emitted together, like a bulk action does.

Each listener receives the signal through its own delivery, as it would if the signal had been emitted, so a listener that runs in a job only enqueues it. Nothing is emitted, so the signal isn't seen by the assertions above.

Returns :ok, or the error of the first delivery that fails.

Options

  • :only - Only delivers to the listeners on these resources, or to these {resource, listener_name} pairs. Raises if no listener matches.
  • :inline? - Runs every listener immediately, whatever its delivery. Defaults to false.
  • :actor, :tenant, :authorize?, :tracer, :context and :scope - What listeners run with, as for Ash.Signals.emit/5.
react_to_signal(
  MyApp.Shop.Signals,
  :order_placed,
  %{order_id: order_id, customer_id: customer_id},
  only: {MyApp.Loyalty.Account, :award_points},
  actor: customer
)

refute_emits_signal(signal_module, signal_name, pattern \\ quote do _ end, fun)

(macro)

Runs the function, asserts that it did not emit a signal matching the given pattern, and returns the function's result.

refute_emits_signal MyApp.Blog.Signals, :post_created, fn ->
  MyApp.Blog.create_draft!("Hello")
end

refute_signal_emitted(signal_module, signal_name, pattern \\ quote do _ end, timeout \\ 0)

(macro)

Asserts that no signal matching the given pattern was emitted since capture_signals/1 was called.

refute_signal_emitted MyApp.Blog.Signals, :post_created
refute_signal_emitted MyApp.Blog.Signals, :post_created, %{title: "Draft"}

The optional timeout, defaulting to 0, waits for signals emitted by other processes.