Preface

An argument for the idea

Many people have noticed that a lot of programming–especially functional programming–involves taking structured data, transforming it, producing a result, and possibly persisting that result (to, for example, a database).

Because of that, we work a lot with libraries (like Phoenix or Ecto) that help in writing such transformation code. But we generally take less advantage of that when testing or test-driving such code. That is: the product code we write is clichéd, full of recognizable idioms, patterns, and names. We build on that. But test code doesn't take similar advantage of the structure of the problem. In these pages, I'll describe test code that does.

Such test code can be simpler–more declarative–than the code it's testing. The reason is that conventional[1] test code is essentially a list of specific examples, together with results checking that's specific to the examples. Product code must handle all possible inputs, which is more work. In particular, product code is "branchy", whereas that's vanishingly rare in test code.

[1] I'm going to leave aside generated or property-based tests like PropCheck. Because they don't have a fixed set of inputs, their checks have to be more general-purpose, though still not as complicated as product code.

A Teaser

A whole lot of assertions are generated from this:

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

  def create_test_data do 
    start(
      module_under_test: Named,
      format: :phoenix
    )

    |> field_transformations(
      as_cast: [:name, :date_string, :lock_uuid],
      date: on_success(
        Date.from_iso8601!(:date_string)),
      days_since_2000: on_success(
        Date.diff(:date, ~D[2000-01-01])
    ))

    |> category(                           :success,
      ok: [
        params(      name: "Bossie", date_string:   "2001-01-01"),
      ]
    )

    |> category(                           :validation_failure,
      format: [
        params_like(:ok, except: [date_string: "2001-01-0"]),
        changeset(
          no_changes: [:date, :days_since_2000],
          error: [date_string: "is not a valid date"]
        ),
      ],
    
      too_early: [
        params_like(:ok, except: [date_string: "1999-12-30"]),
        changeset(
          no_changes: [:days_since_2000],
          error: [date_string: "must be this century"]
        ),
      ]
    )
  end
end

Last updated