Testing models that cannot be created without long chain of dependancies. Tips?

Hi all,

How do you write your context tests (create_thingy(attrs)) when there are a lot of associations that need to be created first?

Here is a contrived example. Let’s says I have a Door model that I’d like to test the color, material, price, etc…

A Door cannot be created without having to first create a Wall
A Wall cannot be created without having to first create a House
A House cannot be created without having to first create a Blueprint
A Blueprint cannot be created without having to first create a Contract
A Contract cannot be created without having to first create a Client
A Client cannot be created without having to first create a Prospect
and on and on… :man_facepalming:

I have a method in my Building context that sort of looks like this:

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

Ideally in my test I’d like to write something like:

{:ok, door} = create_door(%{price: 1_234, material: "wood", color: "white"})
assert door.color == "white"

However I cannot create the Door without passing in a wall_id and since that field cannot be null I’d need to create a Wall. The creation process then has to go all the way up to … create_user. Surely I’ve missed something huge I can do that will still make me confident in the production code, but won’t make testing difficult.

Any tips or suggestions are greatly appreciated.

I am not very familiar with the approach in phoenix specifically, as I am pretty new to elixir. But this situation is very common in any other language.

I come from python world where this can be generally mitigated by 2 approaches:

  1. Data migrations. You basically create json or similar files with data, which you add to your test database before running the tests. I don’t really like this, as this is fairly fragile in my opinion
  2. You create factory classes (or methods|functions, etc.) that generate required objects with required parameters. So if you need a wall for a door your factory function will either get a wall as an argument of create a new one inside it. If to create a wall you need a house, it will create it inside its factory function, etc.

With second approach you will have to do a huge effort of creating this factories if your codebase is fairly big already. But they are easier to maintain as well as way more flexible for your tests )

One way to go about it is separating the door from the relationship to walls. Instead of door <- wall have door <- wall_door -> wall in the db. With a unique constraint on wall_door.door_id you can make sure a way still only belongs to a single wall at most. Having done that on the db level you can do the same separation on the domain level: instead of just create_door you’ll also have e.g. create_door_for_wall. Once that is done you can test a door without needing a wall.

On a more concrete example. I’ve recently did contacts handling in an app, which was initially scoped to tenants. For less coupling I did introduce the concept of a contacts directory. I can easily create a directory for each tenant to use, but I don’t need a tenant, as they’re only coupled by name. I can easily create directories in tests without needing tenants and if I ever need contacts scoped to something else than tenants that’s fine as well.

One thing you can do is to define helper functions in your test_helper that you can use with ExUnit’s setup callback.

If there’s not one in your context, you can define a test only API that handles all the setup for you:

def create_door_with_dependencies() do
  user = create_user()
  #...
  door = Building.create_door(...)
  {:ok, door: door}
end

Instead of calling Building.create_door directly in your test you’d do something like this:

describe "some door feature when a door exists" do
  setup [:create_door]

  test "it does something", %{door: door} do
  end
end

Or you may prefer to keep your setup functions more atomic and explicit and use context to store the dependencies:

def create_user(%{}) do
  create_user()
  {:ok, user: user}
end
...

def create_door(%{user: user, ...}) do
  create_door(user_id: user.id, ...)
  {:ok, door: door}
end

Then you just need to specify each dependency in the setup, which is pretty manageable even with the large number in your contrived example.

describe "some door feature when a door exists" do
  setup [:create_user, ..., :create_door]

  test "it does something", %{door: door} do
  end
end