📔
Idiosyncratic Elixir
  • Introduction
  • Testing in General
    • Flow style tests
  • Phoenix View Models
    • Single form view models
    • Testing view models
  • Declarative testing of structured (form) input
    • Preface
    • Features, described gradually
      • Describing changeset validations
      • Running tests
      • Example prototypes
      • Shorthand for built-in transformations (`cast`)
      • Shorthand for custom transformations (on_success)
Powered by GitBook
On this page
  • The code under test
  • A simple example module
  • What that gives you

Was this helpful?

  1. Declarative testing of structured (form) input

Features, described gradually

PreviousPrefaceNextDescribing changeset validations

Last updated 4 years ago

Was this helpful?

There's a form-handling workflow that I'm going to call "Ecto Classic":

  1. Form data (strings) is given to code that converts it into Elixir values (like integers or dates) and also validates the results.

  2. If the resulting is marked invalid, the user is given another chance to fill in the form.

  3. Otherwise, the changeset encapsulates values to be inserted into a database or used to update an existing row.

  4. However, the database might complain about violated constraints like an attempt to or violate .

  5. If a constraint is violated, the user is given another chance to fix the problem.

  6. Otherwise, the task is done (though the results of the database update might be displayed to the user).

The code under test

I'll start with an absurdly simple Ecto :

defmodule App.Schemas.Basic do
  use Ecto.Schema
  import Ecto.Changeset

  embedded_schema do
    field :age, :integer
    field :date, :date
  end

  def changeset(struct, params) do
    struct
    |> cast(params, [:age, :date])
    |> validate_required([:age, :date])
  end
end

A simple example module

An example module holds a series of examples, each of which is (for the moment) just a way of writing input parameters. The module starts like this:

defmodule Examples.Schemas.Basic.Simplest do
  alias App.Schemas.Basic, as: Basic
  use TransformerTestSupport.Variants.EctoClassic

  def create_test_data do 
    start(
      module_under_test: Basic,
      format: :phoenix
    ) |> 

The create_test_data function is called (using reflection) to create a big test data structure. The starting step (line 6) is to note the module under test (so that its functions, like Basic.changeset , can be called). It also describes the :format of the parameters, which will come into play shortly.

Let's create examples that represent both successful validation and validation failure. (We'll leave aside constraint-checking and use of the database for now.) That looks like this:

    category(                   :success,
      ok: [
        params(
          age: 1,
          date: "2001-01-01")
      ]) |> 

    category(                   :validation_failure,
      invalid: [
        params(
          age: "32a",
          date: "2001-1-1"),
      ],
      empty: [
        params(
          age: "",
          date: ""),
      ],
      missing: [params()]
    )      
  end

For now, you can consider the categories purely for documentation, but they'll have a bigger role to play later.

The four examples each describe parameters to be checked by Basic.changeset. Notice that the parameters are described as Elixir values, rather than the strings you'd get from HTTP. That rarely matters, in that Elixir's cast is perfectly happy casting either the string "1" or the integer 1 to a Schema's :integer field, and it doesn't care whether that value belongs to the key "age" or the key :age.

However, I'm a stickler for realism, so the global format: :phoenix option converts the parameters into the same format Phoenix would deliver to one of its controllers. When called, Basic.changeset would actually receive %{"age" => "1", "date" => "2001-01-01"}.

What that gives you

You could now type the following:

  $ MIX_ENV=test iex -S mix
  iex> TransformerTestSupport.start
  iex> alias Examples.Schemas.Basic.SimplestParams.Tester
  iex> Tester.params(:ok)
  %{"age" => "1", "date" => "2001-01-01"}

That's not very exciting, though it's interesting that you've gotten a Basic.SimplestParams.Tester module that contains some test-support functions like params (line 4).

We could use those params in tests:

defmodule App.Schemas.Basic.SimplestParamsTest do
  use App.Case
  alias App.Schemas.Basic, as: Schema
  alias Examples.Schemas.Basic.Simplest.Tester

  test "valid dates are accepted" do
    Schema.changeset(%Schema{}, Tester.params(:ok))
    |> assert_valid
    |> assert_changes(age: 1,
                      date: ~D[2001-01-01])
  end

That's fine, but it doesn't gain much. In fact, it's arguably worse than describing the parameters in the test, because the input values are visually far separated from the expected results. So let's move the expected results.

It has two fields, :age and :date. Each is processed ("cast", line 12) by transformations built into Ecto. The result is a structure. It will be marked as invalid if the :age is a string that's not an integer or the :date is not in proper ISO8601 format. Beyond that, all that's required is that each field have some value. (Without that, a missing or empty field would be allowed.)

All the code in this tutorial can be found at .

The module names the module under test (line 2) and notes that it's assumed to use the Ecto Classic workflow variant (line 3). There are other workflows. For example, I use a -style workflow that extends the Ecto Classic workflow. It has its own variant.

(The test uses the "flow-style" assertions from .)

changeset
duplicate a unique field
optimistic locking
Schema
Changeset
https://github.com/marick/tts_ecto_example
view-model
ecto_flow_assertions