# A very "useful" example
property "reverse should reverse" do
check all a_string <- string() do
assert a_string |> reverse() |> reverse() == string
end
end
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
...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...
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.
3 Areas I wish to cover...
โโโ 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
App 1
Bond
initiate_account_link(
ย app_1_secret,
ย app_2_ref
)
: account_link_reference
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
App 2
get_messages(
ย account_link_session_secret
)
acknowledge_message(
ย account_link_session_secret,
ย message_id
)
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.
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
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.
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.
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
ย
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 little more complex but still dealing with primitives...
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
@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}"
def account_link_initiation_callback do
url()
end
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
ย
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
...
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
@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()
}
ย
# 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
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
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
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
ย