View Source Testing

If you need to verify that your application correctly receives ecto_watch events, consider the following:

  • PostgreSQL, not Ecto, emits the notifications. This means that for your test to capture notifications, the transaction that triggers them must be committed.

  • By default, transactions in tests are not committed. The Ecto.Adapters.SQL.Sandbox ensures test transactions are rolled back unless explicitly overridden.

  • To ensure notifications are sent, disable the sandbox mode for the test:

Ecto.Adapters.SQL.Sandbox.checkout(MyRepo, sandbox: false)

This forces all database changes to be persisted, so notifications will be emitted. However, be mindful that you may need to manually clean up test records.

You don't need to disable transactions in all test modules, just those where you need to test your integration with ecto_watch.

Example

defmodule MyApp.Application do
  use Application

  alias MyApp.Records

  @impl true
  def start(_type, _args) do
    children = [
     {EctoWatch,
       repo: MyApp.Repo,
       pub_sub: MyApp.PubSub,
       watchers: [ 
        {Records.Record, :updated}
       ]},
      # Ensure that your module is started after EctoWatch
      {MyApp.UpdateCounter, []}
    ]


    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
# update_counter.ex -> module to test
defmodule MyApp.UpdateCounter do
  use GenServer
  
  alias MyApp.Records

  def start_link(_opts) do
    GenServer.start_link(__MODULE__, [], name: __MODULE__)
  end

  def init(_opts) do
    :ok = EctoWatch.subscribe({Records.Record, :updated})
    {:ok, %{}}
  end

  def get_total_counts do
    GenServer.call(__MODULE__, :get_total_counts)
  end

  def get_count(id) do
    GenServer.call(__MODULE__, {:get_count, id})
  end

  # Handle incoming notifications and update state
  def handle_info({Records.Record, :updated}, counts) do
    {:noreply, Map.update(counts, id, 1, &(&1 + 1))}
  end

  def handle_call(:get_total_counts, _from, counts) do
    total =
      counts
      |> Map.values()
      |> Enum.sum()

    {:reply, total, counts}
  end

  def handle_call({:get_count, id}, _from, counts) do
    {:reply, Map.get(counts, id, 0), counts}
  end
end

in our tests

# update_counter_test.exs
defmodule MyApp.UpdateCounterTest do
  use ExUnit.Case

  alias MyApp.Records

  setup do
    # Ensure database changes are committed during tests
    Ecto.Adapters.SQL.Sandbox.checkout(MyApp.Repo, sandbox: false)

    # Clean up database before running each test
    cleanup()
  end

  defp cleanup do
    # Add logic to remove test records from the database
  end

test "counter increments whenever a record is updated" do  
    record_1 = record_fixture()
    record_2 = record_fixture()

    assert UpdateCounter.get_count(record_1.id) == 0
    assert UpdateCounter.get_count(record_2.id) == 0
    assert UpdateCounter.get_total_counts() == 0

    # Based on our application config the following should emit the notifications
    # events if committed
    Records.update_record(record_1, %{key: "some_value"})
    Records.update_record(record_2, %{key: "some_value"})

    # Ensure core logic was executed
    assert UpdateCounter.get_count(record_1.id) == 1
    assert UpdateCounter.get_count(record_2.id) == 1
    assert UpdateCounter.get_count("non_existing_id") == 0
    assert UpdateCounter.get_total_counts() == 2
  end