sb logoToday I Learned

3 posts by leeeggebroten

More robust access to the pipelined variable

Pipelines are a fantastic syntax to clarify intent when doing variable mutations.

The default syntax |> passes the pipelined variable to the invoked function as the first parameter but does not provide a means to reference the variable.

This means that mutating a pipelined variable while introspecting its state cannot be done with default syntax.

If the transform is simple, consider using an anonymous function and Capture operator as in this example where a field in a map is “moved” to a new key

map_with_moved_keys =
  %{foo: "bar"}
  |> (&Map.put_new(&1, :new_foo, &1.foo)).()
  |> Map.drop([:foo])

 %{new_foo: "bar"}

Parameterized ExUnit tests

In ExUnit, it is not immediately obvious how to do the same “test” using different parameters.

It can be tedious to write individual tests for each required field asserting the validation. It’s also difficult for future-you to determine if you have complete coverage.

The cheating way

Remove the all the required fields from the source map before calling changeset and make one massive assert

The better way

The solution I use here is to set the @tag test attribute as two properties of the test. The first @tag field: field_name is the property I’m testing against The second @tag message_attr: %{attr_name => nil} is the value to assign that field before running the test.

You’ll see that these @tag values are available in the test context by the given tag name

    [
      {:entity_name, :entity_name},
      {:entity_uuid, :team_uuid}
    ]
    |> Enum.each(fn {field_name, attr_name} ->
      @tag field: field_name
      @tag message_attr: %{attr_name => nil}
      test "when `#{field_name}` missing, invalid ... required", context do
        message = TestMessageHelpers.market_message(context.message_attr)

        %Changeset{valid?: false} = changeset = Subject.changeset(%Subject{}, message)

        assert changeset.errors == [{context.field, {"can't be blank", [validation: :required]}}]
      end
    end)

Ecto.Changeset.cast

Given the embedded schema

embedded_schema do
  field(:mame, :string)
end

Used by the following code raises a pattern match error that can be difficult to diagnose. Notice that the schema definition uses :mame with an “M”, and @root_fields uses correct (but mismatched) :name with an “N”

@root_fields [:name]

parsed = Jason.decode!(data)

%__MODULE__{}
|> Ecto.Changeset.cast(parsed, @root_fields)

I got sidetracked thinking that the cast was unhappy because the passed data was using string keys