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
ย
- 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