Search code examples
elixirphoenix-frameworkectoex-unit

Ecto Sandbox use checked out connection for existing process


I have a Phoenix Test Application with a Product schema. I have a GenServer started by the main application supervisor that gets a list of the products with handle_call.

def handle_call(:get_products, _from, _state)
  products = Repo.all(Product)
  {:reply, products, products}
end

Now I want to write a test for this GenServer.

I tried to do something like this in the test

setup do
  pid = Process.whereis(MyGenServer)
  Ecto.Adapters.SQL.Sandbox.allow(Repo, self(), pid)
  ProductFactory.insert_list(3, :product) # using ExMachina for factories
end

The 3 products get created, I can find them in the test with Repo.all(Product), however running the MyGenServer.get_products() will return an empty array.

I am not getting any error, but just returns an empty array, as if no products exist.

Is there any way to allow the existing PID to use the checkout sandbox connection, and retrieve my products in the GenServer process?

PS. I managed to run the test by restarting the GenServer process in the test setup, but I was wondering if there is a more "elegant" way to solve the issue.

setup do
  Supervisor.terminate_child(MyApp.Supervisor, MyGenServer)
  Supervisor.restart_child(MyApp.Supervisor, MyGenServer)
  ProductFactory.insert_list(3, :product)
end

Thanks


Solution

  • Here's a minimal phoenix application that works with a GenServer started in the application supervisor, using :shared mode for database interactions.

    Application Module:

    defmodule TestGenServers.Application do
      use Application
    
      def start(_type, _args) do
        import Supervisor.Spec
    
        children = [
          supervisor(TestGenServers.Repo, []),
          supervisor(TestGenServers.Web.Endpoint, []),
          worker(TestGenServers.MyServer, [])
        ]
    
        opts = [strategy: :one_for_one, name: TestGenServers.Supervisor]
        Supervisor.start_link(children, opts)
      end
    end
    

    Product Module:

    defmodule TestGenServers.Model.Product do
      use Ecto.Schema
      import Ecto.Changeset
      alias TestGenServers.Model.Product
    
    
      schema "model_products" do
        field :name, :string
    
        timestamps()
      end
    
      @doc false
      def changeset(%Product{} = product, attrs) do
        product
        |> cast(attrs, [:name])
        |> validate_required([:name])
      end
    end
    

    GenServer Module:

    defmodule TestGenServers.MyServer do
      use GenServer
      alias TestGenServers.Repo
    
      def start_link() do
        GenServer.start_link(__MODULE__, nil, name: __MODULE__)
      end
    
      def handle_call({:get_product, id}, _caller, state) do
        {:reply, TestGenServers.Repo.get(TestGenServers.Model.Product, id), state}
      end
    
    end
    

    Test Module:

    defmodule TestGenServers.TestMyServer do
      use TestGenServers.DataCase
    
      setup do
        product = Repo.insert!(%TestGenServers.Model.Product{name: "widget123"})
        %{product_id: product.id}
      end
    
      test "talk to gen server", %{product_id: id} do
        assert %{id: ^id, name: "widget123"} = GenServer.call(TestGenServers.MyServer, {:get_product, id})
      end
    end
    

    DataCase Module

    defmodule TestGenServers.DataCase do
      use ExUnit.CaseTemplate
    
      using do
        quote do
          alias TestGenServers.Repo
          import TestGenServers.DataCase
        end
      end
    
      setup tags do
        :ok = Ecto.Adapters.SQL.Sandbox.checkout(TestGenServers.Repo)
        unless tags[:async] do
          Ecto.Adapters.SQL.Sandbox.mode(TestGenServers.Repo, {:shared, self()})
        end
        :ok
      end
    end
    

    test_helper:

    ExUnit.start()
    
    Ecto.Adapters.SQL.Sandbox.mode(TestGenServers.Repo, :manual)