0

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

Afshin Moazami
  • 2,092
  • 5
  • 33
  • 55
iacobSon
  • 133
  • 10
  • What is the purpose of calling a `GenServer` to do the work? Why not just call a function, that way you won't block for every request to the `GenServer`. – Justin Wood Jul 17 '17 at 02:04
  • Hi, sorry I was not very clear. That is just a test example. I put the `products = Repo.all(Product)` just to have a simple interaction with the database. So this is the question about Ecto Sandbox behaviour, not about the GenServer results – iacobSon Jul 17 '17 at 09:32
  • Is `Ecto.Adapters.SQL.Sandbox.mode(Repo, :manual)` is called in the `test_helper.exs` ? Are you using `ConnCase` or `DataCase` templates from the phoenix generator ? Are you running the tests with `async: true`? If not, then it should work automatically in `:shared` mode. – Mike Buhot Jul 17 '17 at 10:46
  • yes for `test_helper.exs` using DataCase tried both with `async: true` and without, so it is not working in shared mode **unless** I restart the process as stated above. Please note that MyGenServer process is started together with the app, so before the: `:ok = Ecto.Adapters.SQL.Sandbox.checkout(MyApp.Repo)` All the examples on Ecto Sandbox are starting the processes in the test, so after the owner process (test process) – iacobSon Jul 17 '17 at 13:25

1 Answers1

0

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)
Mike Buhot
  • 4,790
  • 20
  • 31
  • Thanks Mike! This is very close to my example, and mine still does not work this way. The only differences I can spot now is that I use ExMachina to generate the data in the tests, and that I am running some Repo related queries in the `init` function of the GenServer. I will try this evening to remove ExMachina to see if it makes any difference. Do you have a Github repo with your example above? I could try to work on it as well. – iacobSon Jul 18 '17 at 08:58
  • Ah, accessing the Repo in the GenServer init callback is the problem. The Sandbox will associate a connection with an open transaction with the GenServer process before the test_helper can switch it to manual mode. – Mike Buhot Jul 18 '17 at 11:06
  • An explicit call to `Ecto.Adapters.SQL.Sandbox.checkin(Repo)` at the end of `init` fixes the test, but I'm not sure if that is the best way to deal with it. – Mike Buhot Jul 18 '17 at 11:15
  • Thank you. Now it starts to make sense. Do you know of any way to actually see/find this info? something like `<#my_gen_server_pid> => is using this connection` `<#my_test_process_pid> => is using another connection` – iacobSon Jul 18 '17 at 14:57