Practical Properties

A Case Study

Firstly

What this talk is not...

# A very "useful" example
property "reverse should reverse" do
  check all a_string <- string() do
    assert a_string |> reverse() |> reverse() == string 
  end
end

Instead

I wish to share a case study into how I have approached the design and the testing strategy of a project called bond ๐Ÿธ.

Specifically, I want to highlight some of the patterns which have enabled me to effectively test this system and manipulate it into states
through what I call
"Generative Test Fixtures".

At lambda days conf this year, there were a couple of talks which inspired me to leverage property testing.

ย 

ย 

ย 

ย 

ย 

ย 

The one in particular that stands out was:
ย 

John Hughes - Building on developers' intuitions to Create Effective Property Based Tests

But there was one thing I could not accept...

...the supporting code is often many times larger that the production code when leveraging property based testing

...and itself will require suites of tests to ensure the correctness of the supporting test systems...

Yeah...... No.


It is my experience that if the test suite is too complex it falls into disrepair very quitely indeed.

Not to mention the practical nightmare that having test suites for your testing infrastructure,
I couldn't do that to the future me
or the poor soul coming to the project next.

Command Architecture

Generative Test Fixtures

Unit to Property Evolution

3 Areas I wish to cover...

Command Architecture

Bond has 3 Phases

  • Registration
    • Register Application
      ย 
  • Account Linking Handshake Protocol
    • Initiate Account Link
    • Authorise Account Link
      ย 
  • Communication
    • Send Message
    • Acknowledge Message

The Domain

โ”œโ”€โ”€ bond
โ”‚ย ย  โ”œโ”€โ”€ application.ex
โ”‚ย ย  โ”œโ”€โ”€ callback.ex
โ”‚ย ย  โ”œโ”€โ”€ commands
โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ acknowledge_message.ex
โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ authorise_account_link.ex
โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ initiate_account_link.ex
โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ register_application.ex
โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ send_message.ex
โ”‚ย ย  โ”œโ”€โ”€ domain
โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ account_link.ex
โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ account_link_handshake.ex
โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ account_link_reference.ex
โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ account_link_session_secret.ex
โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ application.ex
โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ application_public_reference.ex
โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ bond_id.ex
โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ integration_secret.ex
โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ message.ex
โ”‚ย ย  โ”œโ”€โ”€ events
โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ account_link_established.ex
โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ account_link_initiated.ex
โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ application_registered.ex
โ”‚ย ย  โ”‚ย ย  โ”œโ”€โ”€ message_acknowledged.ex
โ”‚ย ย  โ”‚ย ย  โ””โ”€โ”€ message_sent.ex
โ”‚ย ย  โ”œโ”€โ”€ repo.ex
โ”‚ย ย  โ””โ”€โ”€ schema.ex
โ””โ”€โ”€ bond.ex

App 1

Bond

register(..)

: secret

Register

App 1

Bond

initiate_account_link(
ย  app_1_secret,
ย  app_2_ref
)

: account_link_reference

Account Link

App 2

authorise_account_link(
ย  account_link_reference
)

established_callback(
ย  account_link_reference,
ย  account_link_session_secret
)

established_callback(
ย  account_link_reference,
ย  account_link_session_secret
)

App 1

Bond

send_message(
ย  account_link_session_secret,

ย  message
)

: message_id

Communication

App 2

get_messages(
ย  account_link_session_secret
)

acknowledge_message(
ย  account_link_session_secret,

ย  message_id
)

Defining Command

In object-oriented programming, the command pattern is a behavioral design pattern in which an object is used to encapsulate all information needed to perform an action and/or trigger an event at a later time.

Example Command

defmodule Bond.Commands.RegisterApplication 
  defstruct [
    :application_name, :authorisation_endpoint,
    :account_link_initiation_callback, :account_link_callback
  ]

  # Creates a struct of type RegisterApplication...
  def create(
        application_name,
        authorisation_endpoint,
        account_link_initiation_callback,
        account_link_callback
      ) do
    %__MODULE__{
      application_name: application_name,
      authorisation_endpoint: authorisation_endpoint,
      account_link_initiation_callback: account_link_initiation_callback,
      account_link_callback: account_link_callback
    }
  end

  # Executes the Command...
  defdelegate execute(cmd), to: Bond.Domain.Application, as: :register
end

In Essence, all
State Mutations
which the System can perform are modelled explicitly as

Commands

Command Context

Commands contain data fields and references.

ย 

The references specifically are what provide the context in which the Commands will be run.

ย 

A SendMessage Command requires an
AccountLinkSessionSecret field which provides the context for execution.

ย 

An AccountLinkSessionSecret contains a reference to an AccountLink, which in turn contains reference to the Accounts which are Linked and their Registered Applications.

For a Command to be "Valid" the aforementioned context must be in place.

As such to achieve desired
States
I can do so through the
currated execution of

Commands

ย 


And in practical

terms this boils down to "Setup" prior to test execution...

And down the rabit hole we go...

Generative Tests Fixtures
(System State as a Lazy Stream)

A test fixture is a fixed state of a set of objects used as a baseline for running tests.

A generative test fixture is a dynamically generated valid state used as a baseline for running a test.

Data Generators

defmodule Bond.Commands.RegisterApplication do
  @moduledoc """
  Register Application Command

  Creates a command that when executed will create and application
  integration registration and will return references that will be
  used by the registered application in order to interact with the API.
  """

  @typedoc """
  `RegisterApplication` struct which holds the required information needed to
  regsister an application.
  """
  @type t :: %__MODULE__{
          application_name: Bond.application_name(),
          authorisation_endpoint: Bond.authorisation_endpoint(),
          account_link_initiation_callback: Bond.account_link_initiation_callback(),
          account_link_callback: Bond.account_link_callback()
        }

  def create...
  def execute(cmd)...
end

Register Application
Command

What do we

need to create a

"valid" RegisterApplication Command

ย 

We need to consider its component parts and what makes them "valid"

A "valid" Application Name

  • Alphanumeric String
  • Min Length 10
  • Max Length 64

A nice simple primitive, ez pz...
ย 

But this process already had provided value with regards to "design thinking"

def application_name do
  StreamData.string(:alphanumeric, min_length: 10, max_length: 64)
end

A "valid" Account Link Callback

  • Is a URL

A little more complex but still dealing with primitives...

A "valid" URL

Protocol (From list)

+ "://"

+ย Domain (Non-empty String)

+ย "."

+ย Top Level Domain (From TLD list)

+ <optional> Path

@protocols ~w(
  http
  https
)

# Read File of TLDs are compile time
@tlds Path.join(:code.priv_dir(:bond), "domains.txt")
      |> File.read!()
      |> String.split("\n")

def url(min_length \\ 10) do
  url_generator =
    ExUnitProperties.gen all(
                         protocol <- StreamData.member_of(@protocols),
                         tld <- StreamData.member_of(@tlds),
                         domain <- StreamData.string(:alphanumeric, min_length: min_length),
                         url = "#{protocol}://#{domain}.#{tld}"
                       ) do
      url
    end

  StreamData.resize(url_generator, 60)
end

A "valid" URL Generator

@protocols ~w(http https)

@tlds # List of Top Level Domains

protocol <- StreamData.member_of(@protocols),
tld <- StreamData.member_of(@tlds),
domain <- StreamData.string(:alphanumeric, min_length: 10),
url = "#{protocol}://#{domain}.#{tld}"

A "valid" URL Generator

def account_link_initiation_callback do
  url()
end    

A "valid" Account Link Callback

def register_application() do
  ExUnitProperties.gen all(
                         app_name <- application_name(),
                         auth_ep <- authorisation_endpoint(),
                         acc_link_init_cb <- account_link_initiation_callback(),
                         acc_link_cb <- account_link_callback(),
                         cmd =
                           RegisterApplication.create(
                             app_name,
                             auth_ep,
                             acc_link_init_cb,
                             acc_link_cb
                           )
                       ) do
    cmd
  end
end

A "valid" Register Application Command

So far we have

  • Built a Command Generator
  • Built Generators for the Primitives
  • Using StreamData (a property testing tool)
  • Provided a function which returns a "valid" Command

ย 

Let's have a ๐Ÿ‘€ at this in action

Simple Register Application Fixture Function

i.e. return a single valid command for use in unit tests

defmodule BondTest.Fixtures.RegisterApplicationFixture do
  alias __MODULE__.Generators

  def valid() do
    Generators.register_application()
    |> Enum.take(1)
    |> hd()
  end
...

Simple Register Application Unit Test

using the Generative Test Fixture

test "register application should generate bond id and integration secret" do
  cmd = RegisterApplicationFixture.valid()

  assert {:ok, %ApplicationRegistered{
            bond_id: bond_id,
            integration_secret: integration_secret
          }} = RegisterApplication.execute(cmd)

  assert is_binary(bond_id)
  assert is_binary(integration_secret)
end

Let's talk about Composition

@type t :: %InitiateAccountLink{
      integration_secret: Bond.integration_secret(),
      bond_id: Bond.bond_id(),
      account_reference: Bond.account_reference(),
      post_authorisation_redirect_url: Bond.post_authorisation_redirect_url()
    }

Initiate Account Link
Command

  • Integration Secret
    • Provided by the "Initiating Application"
    • Used to prove the identity of the caller (secret token)
      ย 
  • Bond Id
    • Provided to identify the "Target Application"
    • A public reference to a registered application.

We can use to the Integration Secret and Bond Id Generators, which implicitly Register Applications.

ย 

# Delegate obtaining the integration_secret and bond_id to their respective generators
defdelegate integration_secret, to: IntegrationSecretFixture.Generators
defdelegate bond_id, to: BondIdFixture.Generators   

# Create an initiate_account_link command by composing these generators
def initiate_account_link() do
  ExUnitProperties.gen all(
                         int_sec <- integration_secret(),
                         bond_id <- bond_id(),
                         acc_ref <- account_reference(),
                         redirect_url <- redirect_url()
                       ) do
    InitiateAccountLink.create(int_sec, bond_id, acc_ref, redirect_url)
  end
end

A "valid" Initiate Account Link Command

Let's have a ๐Ÿ‘€ at this in action

Let's talk about
State and Context

We are implicitly generating the pre-requisite state needed to make our Commands valid through Command and Domain Entity Generators

Unit to Property Evolution

Let's finish by looking at some unit tests and how they can be converted into Property tests

as a result of using Generative Test Fixtures

Registering Application Public API Unit Test

test "register/1 should return registration" do
  import RegisterApplicationFixture

  application_name = application_name()
  authorisation_endpoint = authorisation_endpoint()
  account_link_initiation_callback = account_link_initiation_callback()
  account_link_callback = account_link_callback()

  assert {:ok, %ApplicationRegistered{}} =
           Bond.register(
             application_name,
             authorisation_endpoint,
             account_link_initiation_callback,
             account_link_callback
           )
end

Registering Application Public API Property

property "register/1 should return registration" do
  import RegisterApplicationFixture.Generators # <- Added .Generators

  check all(
          application_name <- application_name(),
          authorisation_endpoint <- authorisation_endpoint(),
          account_link_initiation_callback <- account_link_initiation_callback(),
          account_link_callback <- account_link_callback()
        ) do
    assert {:ok, %ApplicationRegistered{}} =
             Bond.register(
               application_name,
               authorisation_endpoint,
               account_link_initiation_callback,
               account_link_callback
             )
  end
end

To Setup the Acknowledge Message Test we need to...

  • Register initiating Application
  • Register target Application
  • Obtain Integration Secret
  • Obtain Bond Id
  • Initiate Account Link
  • Authorise Account Link
  • Send Message... to which we are acknowledging

Acknowledge Message Public API Property

property "acknolwedge/2 should return message acknowledged" do
  import BondTest.Fixtures.AcknowledgeMessageFixture.Generators

  check all(
          acknowledge_message <- acknowledge_message()
        ) do
    assert {:ok, %Bond.Events.MessageAcknowledged{
                   message_id: acknowledge_message.message_id
                  }
           } ==
             Bond.acknowledge_message(
               acknowledge_message.account_link_session_secret,
               acknowledge_message.message_id
             )
  end
end

and that is it...
all the Setup is Implicit as Commands are Valid at the point of Generation

๐ŸŽ‰

No Extensive Test Infracture
Just Generators and Composition

ย 

No Fancy System State Setup

Each Generated Entity or Command has its own Data Root

ย 

No Rabit Holes ๐Ÿฐ

ย 

Simply Write Your Properties

Thanks for Listening!

ย 

ย 

ย 




@holsee
@elixirbelfast

@belfastfp
ย 

Practical Properties

By Steven Holdsworth

Practical Properties

A Case Study on how to how explicitly modelling of commands in a systems in conjunction with generative test fixtures results in a powerful design and testing aid.

  • 248