woohaaha

woohaaha

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.

Showing Posts 1 to 3

Mpanarin

Mpanarin

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 )

LostKobrakai

LostKobrakai

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.

tfwright

tfwright

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
— All posts loaded —

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
asweet-confluent
I recently noticed that Elixir’s Logger defaults its primary log level to :debug when no :logger, :level application configuration is pre...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews