Request for code review - property based test

Greetings! I am relatively new to property-based testing. I am practicing writing property tests and hoping for a code review if any practitioner has any comments or suggestions on the test.

defmodule Academy.UserPropTest do alias Academy.UserShim use Academy.TestCase, async: true use PropCheck use PropCheck.StateM require ExUnitProperties # Generators def first_name() do StreamData.string(:alphanumeric, length: 10..20) end def email() do StreamData.string(:alphanumeric, length: 10) end def email(s) do attrs = Map.get(s, :existing) StreamData.constant(attrs.email) end def uid() do StreamData.string(:alphanumeric, length: 10) end def uid(s) do attrs = Map.get(s, :existing) StreamData.constant(attrs.uid) end def role() do StreamData.member_of(["admin", "editor", "tester", "support", "user"]) end def profile() do StreamData.nonempty(StreamData.map_of(StreamData.constant("lang"), StreamData.string(:ascii))) end def attrs() do Enum.take( StreamData.fixed_map(%{ first_name: first_name(), email: email(), uid: uid(), role: role(), profile: profile() }), 1 ) end def attrs(s) do Enum.take( StreamData.fixed_map(%{ first_name: first_name(), email: email(s), uid: uid(), role: role(), profile: profile() }), 1 ) end # Helpers def like_email(map, email) do Enum.any?( Map.values(map), fn attrs -> attrs.email == email end ) end property "user stateful operations", [:verbose] do forall cmds <- commands(__MODULE__) do {history, state, result} = run_commands(__MODULE__, cmds) (result == :ok) |> aggregate(command_names(cmds)) |> when_fail( IO.puts(""" History: #{inspect(history)} State: #{inspect(state)} Result: #{inspect(result)} """) ) end end # initial model value at system start. Should be deterministic. def initial_state(), do: %{} def command(state) do always_possible = [ {:call, UserShim, :create, attrs()} ] relies_on_state = case Map.equal?(state, %{}) do # no values yet true -> [] # values from which to work false -> s = state [ {:call, UserShim, :create_existing, attrs(s)} ] end oneof(always_possible ++ relies_on_state) end # Picks whether a command should be valid under the current state. def precondition(s, {:call, _, :create, [attrs | _]}) do not like_email(s, attrs.email) end # - all calls with known emails def precondition(s, {:call, _mod, _fun, [attrs | _]}) do like_email(s, attrs.email) end # Given the state *prior* to the call {:call, mod, fun, args}, # determine whether the result (coming from the actual system) # makes sense. def postcondition(_state, {_, _mod, :create, _args}, {:ok, _}) do true end def postcondition(_state, {_, _mod, :create_existing, _args}, {:error, _}) do true end # Assuming the postcondition for a call was true, update the model # accordingly for the test to proceed def next_state( state, _, {:call, _, :create, [attrs]} ) do Map.put(state, :existing, attrs) end def next_state(state, _res, {:call, _mod, _fun, _args}) do new_state = state new_state end end 
defmodule Academy.UserShim do def create(attrs) do Academy.User.create(attrs) end def create_existing(attrs) do Academy.User.create(attrs) end end 
3 Likes

Nothing really wrong pokes me in the eye. Do you want to add to this code but are not able to? That second module is a bit strange though, what’s its purpose?

This is my first time writing a property test. I hope to know if the test has some bad practices.

The second module is a Shim. In my case, I would like to test a different scenario, such as create a user who already exists in the system. By using the shim, I have :create and :create_existing to indicate different scenarios. So, if calling :create_existing, an error is expecting instead of success.