Testing my Ecto Repo

I’m fumbling my way through testing in Elixir. The pure functional tests using ExUnit are great. But trying to test all the functions that interact with my Ecto Repo is another matter.

First off, the tests that Phoenix generates get pretty mangled by the time you’ve combed over your model schema and altered the columns. But that aside, trying to get tests to work has been tough. Items are being inserted into the test database, but the assertions fail. I’m not clear what the Ecto.Adapters.SQL.Sandbox pool does, and I don’t know how pattern matching is supposed to work when the fixture function uses a hard-coded primary key and it omits the timestamp columns.

An example test is something like:

    test "get_thing!/1 returns the thing with given id" do
      thing = thing_fixture()
      assert ThingCtx.get_thing!(thing.id) == thing
    end

The error is invariably:
** (MatchError) no match of right hand side value:

I don’t see much in the way of documentation when it comes to this, so I’m mostly trying to fumble my way through the tests that Phoenix generates automatically. I don’t understand how the matching operator is supposed to deal with the timestamp columns or the associations that are defined in my schema.

Does anyone have a working example of how to test something as simple as getting a single record? Or fetch all records?

Thanks!

1 Like

It may help to provide some concrete code with a concrete error, because I have code that is in essence the exact same thing you have there that provides perfectly lovely error messages.

The sandbox pool ensures that each test case runs in isolation. This is means that if you run Test case A and create some records, the next case that runs won’t see those records. In fact you can even run test cases A and B at the same time and they won’t see each other’s records. Yay transactions!

Looks like the error is in the fixture.

def thing_fixture(attrs \\ %{}) do
  {:ok, thing} =
    attrs
    |> Enum.into(@valid_attrs)
    |> ThingCtx.create_thing()

  thing
end

The error traces down to the pattern matching attempted with {:ok, thing}

Ack, Ok, I see what I did – I filtered out the :ok, thing in the context function instead of returning the {:status, result} bit in its entirety.

I’m not following exactly how the test pool thing works… I’m sorta expecting that the database gets wiped prior to test runs… it doesn’t look like that’s the case though, is it?

If you have an error in your program in general then you’re gonna get the usual errors. It looks like your create_thing function isn’t returning {:ok, thing} so it’ll blow up. That’s just a guess, you didn’t include the error.

If you assert {:ok, thing} = whatever() then you’ll get a nice error message.

The test pool works by creating a transaction within postgres at a specific isolation level. Then when the test ends it rolls back the transaction. This does leave behind certain affects (auto incrementing ids stay incremented) but the records are never committed.

2 Likes

Thanks for the clarification… makes sense.

So, I’m on to the “nicer” error messages… the next problem is that my list_things() function preloads an association… (Is that a bad idea?):

def list_things do
    Repo.all(Thing)
    |> Repo.preload(:related_table)
  end

So when the test uses the fixture, the assertion fails. E.g.

test "list_things/0 returns all things" do
      thing = thing_fixture()
      assert Resource.list_things() == [thing]
end

The create method (used by the fixture) does NOT include the related table, but the list_things() method DOES, so two sides don’t match.

Well so this sort of boils down to determining what it is you want to actually test. If you want to assert that it loads things and loads their preloads, then you should probably preload the fixtured’d entity. If you’re just trying to assert that it gets all things, you probably want a test where you just compare ids.

Makes sense, thank you. It looks like the transactions blew up or something because records are not getting removed from the testing database…

That’s odd, records only persist if they are committed, and they can’t be committed if it blows up. Maybe the sandbox is misconfigured?

It seems to have sorted itself out. Maybe I just need sleep…

On that note: Comparing ecto schema structs for equality (==/2) is hardly ever somethings you actually want to test for. E.g. the data coming out of the db will have an id, the input might not. The data coming out of the db have inserted-/updated_at timestamps set, the input might not. There might be defaults for db columns that your runtime doesn’t know of. So total equality is not really to be expected.

Tests are rather concerned with:

  • “are those the same entities” a.k.a. do they hold the same identity.
    The identity of those structs is described by the primary key alone (id field in most cases), so assert on them matching.
  • “are the fields of a struct saved correcty”
    Test just those fields like assert %{…} = Map.take(entity, [:name, :body, :link]) or even one assert per field.

I can see that those might require more elaborate tests than Module.func() == [entity], but that’s the nature of the beast.

5 Likes