ExUnit Testing FKey Constraint Error

Trying to practice good testing habits with an example project.

In my test I’m creating an example order:

def order_fixture(attrs \\ %{}) do
    {:ok, order} =
      attrs
      |> Enum.into(@valid_attrs)  
      |> Pharmacies.create_order()

    order
  end

which has a couple associations:

defmodule Sdpharm.Pharmacies.Order do
  use Ecto.Schema
  import Ecto.Changeset
  alias Sdpharm.Pharmacies.Patient
  alias Sdpharm.Pharmacies.Location
  alias Sdpharm.Pharmacies.Order

  schema "orders" do
    field :prescription_id, :id

    belongs_to :location, Location
    belongs_to :patient, Patient

    timestamps()
  end

  @doc false
  def changeset(%Order{} = order, attrs) do
    order
    |> cast(attrs, [:location_id, :prescription_id, :patient_id])
    |> validate_required([:location_id, :prescription_id, :patient_id])
  end
end

when I run the test I get an error:

7) test orders list_orders/0 returns all orders (Sdpharm.PharmaciesTest)
   test/sdpharm/pharmacies/pharmacies_test.exs:145
   ** (Ecto.ConstraintError) constraint error when attempting to insert struct:

       * orders_location_id_fkey (foreign_key_constraint)

   If you would like to stop this constraint violation from raising an
   exception and instead add it as an error to your changeset, please
   call `foreign_key_constraint/3` on your changeset with the constraint
   `:name` as an option.

   The changeset has not defined any constraint.

   code: order = order_fixture()

I’m using the changeset in the context:

def create_order(attrs \\ %{}) do
  %Order{}
  |> Order.changeset(attrs)
  |> Repo.insert()
end

and passing it @valid_attrs %{prescription_id: 1, location_id: 1, patient_id: 1}
Why is there a constraint error? It seems to only care about the location_fkey?

and passing it @valid_attrs %{prescription_id: 1, location_id: 1, patient_id: 1} Why is there a constraint error?

Your database is probably checking if these references exist, which they don’t (unless you happen to have a location with id 1 in your db).

It seems to only care about the location_fkey?

Chances are it cares about all the foreign keys, but the location_id is the first to be checked, and an error is being raised. You should probably define the constraints, as the error message suggests:

If you would like to stop this constraint violation from raising an
  exception and instead add it as an error to your changeset, please
  call `foreign_key_constraint/3` on your changeset with the constraint
  `:name` as an option.

In your schema:

@doc false
def changeset(%Order{} = order, attrs) do
  order
  |> cast(attrs, [:location_id, :prescription_id, :patient_id])
  |> validate_required([:location_id, :prescription_id, :patient_id])
  |> foreign_key_constraint(:location, name: :orders_location_id_fkey)

This will give you an error in your changeset, instead of raising an error.

You have to actually create the data you are referring to, otherwise your database will not let it pass (which is a good thing, but it makes testing a little more tedious.

2 Likes

The test still fails, now it just returns the :error case.

4) test orders list_orders/0 returns all orders (Sdpharm.PharmaciesTest)
  test/sdpharm/pharmacies/pharmacies_test.exs:144
  ** (MatchError) no match of right hand side value: {:error, #Ecto.Changeset<action: :insert, changes: % {location_id: 1, patient_id: 1, prescription_id: 1}, errors: [location: {"does not exist", [constraint: :foreign, 

constraint_name: “orders_location_id_fkey”]}], data: #Sdpharm.Pharmacies.Order<>, valid?: false>}
code: order = order_fixture()
stacktrace:
test/sdpharm/pharmacies/pharmacies_test.exs:136: Sdpharm.PharmaciesTest.order_fixture/1
test/sdpharm/pharmacies/pharmacies_test.exs:145: (test)

I don’t think I want to depend on a seed here, right?
So say I have a location_fixture as well, how would I implement that?
Something along these lines?

test "list_orders/0 returns all orders" do
  order = order_fixture()
  location = location_fixture()
  assert Pharmacies.list_orders() == [order]
end

Take a look at ex_machina. You can easily implement what it does yourself, but if you are still just dipping your toes into ecto (and phoenix, judging from the test example), it can help you getting started :slight_smile:

3 Likes