Fixtures in tests

Hi there…

Are fixtures given any special treatment by elixir or is it just a name that everyone is familiar with?

I am going through the auto generated tests that Elixir creates and see it creates a fixture() function, but wasn’t sure this is a familiar name or something that is evaluated by ExUnit.

I see that fixtures are commonly defined as fixture(:some_atom)… what is the purpose of this? What advantage does this have over defining the function fixture_create_some_atom()?

I am trying to up my game when it comes to Elixir testing, so here is my first test that makes sure my function selects items between certain dates (including start and end date):

defmodule InterimCrmWeb.ExportControllerTest do

  use InterimCrmWeb.ConnCase

  alias InterimCrm.Data

  @create_start_attrs %{address: "some address", company: "some company", email: "some email", first_name: "some first_name", hear_about_us: "some hear_about_us", invite_to_feefo: true, invoice_number: "some invoice_number start", last_name: "some last_name", order_date: ~D[2010-05-01], phone: "some phone", post_code: "some post_code", state: "some state", suburb: "some suburb"}
  @create_middle_attrs %{address: "some address", company: "some company", email: "some email", first_name: "some first_name", hear_about_us: "some hear_about_us", invite_to_feefo: true, invoice_number: "some invoice_number middle", last_name: "some last_name", order_date: ~D[2010-05-01], phone: "some phone", post_code: "some post_code", state: "some state", suburb: "some suburb"}
  @create_end_attrs %{address: "some address", company: "some company", email: "some email", first_name: "some first_name", hear_about_us: "some hear_about_us", invite_to_feefo: true, invoice_number: "some invoice_number end", last_name: "some last_name", order_date: ~D[2010-05-31], phone: "some phone", post_code: "some post_code", state: "some state", suburb: "some suburb"}
  @create_attrs %{address: "some address", company: "some company", email: "some email", first_name: "some first_name", hear_about_us: "some hear_about_us", invite_to_feefo: true, invoice_number: "some invoice_number", last_name: "some last_name", order_date: ~D[2010-04-01], phone: "some phone", post_code: "some post_code", state: "some state", suburb: "some suburb"}

  def fixture_create_dated_orders() do
    Data.create_order(@create_start_attrs)
    Data.create_order(@create_middle_attrs)
    Data.create_order(@create_end_attrs)
    Data.create_order(@create_attrs)
  end

  test "gets orders between specific dates" do
    fixture_create_dated_orders()
    orders = Data.list_orders_between(@create_start_attrs.order_date,@create_end_attrs.order_date)
    assert length(orders) == 3
  end

end

Cheers,
Nathan

1 Like

:wave:

Just a name.

I see that fixtures are commonly defined as fixture(:some_atom)… what is the purpose of this? What advantage does this have over defining the function fixture_create_some_atom()?

fixture(:some_atom) seems shorter.

Ok - wanted to make sure I wasn’t missing out on something.

Yes, you are right, it does seem shorter (maybe because it is!) :slightly_smiling_face:

Appreciate it.

It is more of an example of “how-to”, in fact I almost always have a dedicated Fixture module containing fixtures for all of my db entities which are often used in tests

1 Like

The fixture function phoenix is generating is imho the minimal sane approach for a generator to not duplicate fixtures while at the same time not clash helper functions (multiple generations for the same file) and not introducing yet another complex thing a beginner needs to learn. For a generator it’s imho the approach to take, but it’s not one I’d like to maintain for long in a project.

1 Like

I just tried to use associations in fixtures (don’t even know if that would be the right way to do it), like:

@valid_attrs %{
  name: "jane",
  surname: "doe",
  cats: [Repo.get_by(Project.Cat, name: "mittens")]
}

And I notice it doesn’t work.
Could anyone be kind enough to explain why?
I’ve been banging my head trying to understand ExUnit as it mostly doesn’t work as I’d expect it to—which is fair, I’m know there’s a good reason for it all, I’d just like to understand it better so I can predict it better too :slight_smile:

1 Like

In that case the @var is called a module attribute and is compiled into the binary that the beam runs. Because the repo and data aren’t actually running at compile time, the data isn’t there.

Typically I use a function with the attributes defined as a static map, and any overrides taken as an additional map. I sometimes make associations required via the function parameters as well, by changing the number of parameters.

2 Likes

Ohhh, that works really nicely! I like it better, even. Thanks!!

If you don’t mind me asking, why is @var compiled into the binary? I thought everything .exs was not compiled?

1 Like

Good point there. It’s not in a binary if it’s exs.

I’m a little more outside of my comfort zone on this answer, but I believe the code is compiled when it loads the test, which would be before the repo is around. If the repo is around, and the error is that the record can’t be found, then I’d guess it’s an ordering of when data is created.

I generally about module attributes in tests unless I have a compelling reason not to. I’ve often found I’ll start there and then quickly move to functions, so now I just start at functions.

1 Like

Thank you for your answer!
Just out of curiosity, do you find any value in using libraries such as ExMachina for fixtures?
I didn’t want to introduce more things to the mix (no pun intended) while I’m still learning the platform, but I read about it quickly and it seems to ease a lot of boilerplate in writing tests…

Personally, I don’t. Here’s an example of a recent fixture file I worked with:

defmodule Test.ActivityFactory do
  def create!(type, overrides) do
    params = params(type, overrides)
    ActivityService.Activity.upsert_activity!(params, tenant_id: params.tenant_id)
  end

  defp base_params,
    do: %{
      tenant_id: 1,
      user_guid: Ecto.UUID.generate(),
      resource_id: 2,
      resource_type: "person",
      occurred_at: DateTime.utc_now(),
      idempotency_key: to_string(:erlang.unique_integer())
    }

  def params(_, overrides \\ %{})

  def params(:call, overrides) do
    %{
      type: "call",
      static_data: %{
        "call_id" => 10,
        "to" => "+12223334444",
        "note_id" => 35,
        "note_content" => "Note content",
        "disposition" => "disposition",
        "sentiment" => "sentiment",
        "recording_urls" => []
      }
    }
    |> Map.merge(base_params())
    |> Map.merge(overrides)
  end

  def params(:completed_action, overrides) do
    %{
      type: "completed_action",
      static_data:
        %{
          "action_id" => 20,
          "cadence_id" => 12,
          "step_id" => 13,
          "instigator" => %{opaque: true},
          "action_outcome" => "completed",
          "due_date" => "2019-01-01",
          "type" => "call",
          "was_overdue" => true,
          "integration_integration_id" => 10,
          "integration_name" => "name",
          "integration_subtype" => "subtype",
          "integration_subtype_guid" => "1111-2222"
        }
        |> Map.merge(Map.get(overrides, :static_extra, %{}))
    }
    |> Map.merge(base_params())
    |> Map.merge(overrides)
  end

This one doesn’t have other associations on it, but you can get the picture. I have found that the fixture helper libraries can hide complexity in your codebase. I’d rather have that pain be up-front so people are more likely to write code that isn’t painful (don’t nest a bunch of dependencies).

5 Likes

That’s excellent, thank you so much for sharing!