Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Generative Testing in Elixir

Generative Testing in Elixir

Automated test suites are invaluable. They provide protection against regressions and can serve as a design tool when building new apis. But, despite this protection bugs still slip through. We could try to write more tests but attempting to cover every edge case is an untenable problem. Luckily, we can use property based testing to generate edge cases for us.

Originally developed in Haskell, property tests have spread to many other languages. In this talk we’ll discuss the basics of property testing, demonstrate how we can determine properties for our system, and look at real world examples of property tests using elixir.

Chris Keathley

January 13, 2017
Tweet

More Decks by Chris Keathley

Other Decks in Programming

Transcript

  1. TDD

  2. 1. Write a failing test 2. Write enough code to

    make that test pass 3. Refactor Test Driven Development
  3. test "adding 2 numbers" do assert add(1, 1) == 2

    assert add(0, 2) == 2 end def add(x, y) do end
  4. test "adding 2 numbers" do assert add(1, 1) == 2

    assert add(0, 2) == 2 end def add(x, y) do 2 end
  5. test "adding 2 numbers" do assert add(1, 1) == 2

    assert add(0, 2) == 2 end def add(_x, _y) do 2 end
  6. test "adding 2 numbers" do assert add(1, 1) == 2

    assert add(0, 2) == 2 assert add(3, 4) == 7 end def add(_x, _y) do 2 end
  7. test "adding 2 numbers" do assert add(1, 1) == 2

    assert add(0, 2) == 2 assert add(3, 4) == 7 end def add(_x, _y) do 2 end
  8. test "adding 2 numbers" do assert add(1, 1) == 2

    assert add(0, 2) == 2 assert add(3, 4) == 7 end def add(_x, _y), do: 2
  9. test "adding 2 numbers" do assert add(1, 1) == 2

    assert add(0, 2) == 2 assert add(3, 4) == 7 end def add(3, _), do: 7 def add(_x, _y), do: 2
  10. test "adding 2 numbers" do assert add(1, 1) == 2

    assert add(0, 2) == 2 assert add(3, 4) == 7 end def add(3, _), do: 7 def add(_x, _y), do: 2
  11. test "adding 2 numbers" do assert add(1, 1) == 2

    assert add(0, 2) == 2 assert add(3, 4) == 7 assert add(-1, 4) == 3 end def add(3, _), do: 7 def add(_x, _y), do: 2
  12. test "adding 2 numbers" do assert add(1, 1) == 2

    assert add(0, 2) == 2 assert add(3, 4) == 7 assert add(-1, 4) == 3 end def add(3, _), do: 7 def add(_x, _y), do: 2
  13. Guard clauses def user_name(%{name: name}) when is_binary(name) do name end

    def user_name(%{name: name, age: age}) when age < 20
  14. test "adding 2 numbers" do assert add(1, 1) == 2

    assert add(0, 2) == 2 assert add(3, 4) == 7 assert add(-1, 4) == 3 end def add(3, _), do: 7 def add(_x, _y), do: 2
  15. test "adding 2 numbers" do assert add(1, 1) == 2

    assert add(0, 2) == 2 assert add(3, 4) == 7 assert add(-1, 4) == 3 end def add(x, _) when x < 0, do: 3 def add(3, _), do: 7 def add(_x, _y), do: 2
  16. test "addition with zero returns the same number" do ptest

    x: int() do end end def add(_x, _y) do end
  17. test "addition with zero returns the same number" do ptest

    x: int() do assert add(x, 0) == x end end def add(_x, _y) do end
  18. test "addition with zero returns the same number" do ptest

    x: int() do assert add(x, 0) == x end end def add(_x, _y) do end
  19. test "addition with zero returns the same number" do ptest

    x: int() do assert add(x, 0) == x end end def add(x, _y) do x end
  20. test "addition is commutative" do ptest x: int(), y: int()

    do assert add(x, y) == add(y, x) end end def add(x, _y) do x end
  21. test "addition is commutative" do ptest x: int(), y: int()

    do assert add(x, y) == add(y, x) end end def add(x, _y) do x end
  22. test "addition is commutative" do ptest x: int(), y: int()

    do assert add(x, y) == add(y, x) end end def add(x, y) do x * y end
  23. test "addition is commutative" do ptest x: int(), y: int()

    do assert add(x, y) == add(y, x) end end def add(x, y) do x * y end
  24. test "addition is commutative" do ptest x: int(), y: int()

    do assert add(x, y) == add(y, x) end end def add(x, y) do x * y end def add(x, 0), do: x
  25. def add(x, y) do x * y end def add(x,

    0), do: x test "addition is asociative" do end
  26. def add(x, y) do x * y end def add(x,

    0), do: x test "addition is asociative" do ptest x: int(), y: int(), z: int() do end end
  27. def add(x, y) do x * y end def add(x,

    0), do: x test "addition is asociative" do ptest x: int(), y: int(), z: int() do assert add(x, add(y, z)) == add(add(x, y), z) end end
  28. def add(x, y) do x * y end def add(x,

    0), do: x test "addition is asociative" do ptest x: int(), y: int(), z: int() do assert add(x, add(y, z)) == add(add(x, y), z) end end
  29. Generated Commands [{:vote, "chris", 1}, {:vote, "chris", 2}, {:vote, "jane",

    1}, {:vote, "jane", 1}, {:vote, "jane", 3} {:vote, "chris", 2}]
  30. Generated Commands [{:vote, "chris", 1}, {:vote, "chris", 2}, {:vote, "jane",

    1}, {:vote, "jane", 1}, {:vote, "jane", 3} {:vote, "chris", 2}] [{:vote, "chris", 1}, {:vote, "jane", 1}]
  31. Property: Users votes should increase test “users votes increase after

    voting" do ptest [commands: gen_commands("chris")] do end end
  32. Property: Users votes should increase test “users votes increase after

    voting" do ptest [commands: gen_commands("chris")] do VoteCounter.reset() end end
  33. Property: Users votes should increase test “users votes increase after

    voting" do ptest [commands: gen_commands("chris")] do VoteCounter.reset() {_state, result} = run_commands(commands, Client) end end
  34. Property: Users votes should increase test “users votes increase after

    voting" do ptest [commands: gen_commands("chris")] do VoteCounter.reset() {_state, result} = run_commands(commands, Client) assert result end end
  35. Property: Users votes should increase test “users votes increase after

    voting" do ptest [commands: gen_commands("chris")] do VoteCounter.reset() {_state, result} = run_commands(commands, Client) assert result end end def run_commands(commands, module) do Enum.reduce( commands, {0, true}, & run_command(module, &1, &2) ) end
  36. def gen_commands(name) do list(of: gen_vote(name), max: 20) end def gen_vote(name)

    do tuple(like: { value(:vote), )}) end Command Generators
  37. def gen_commands(name) do list(of: gen_vote(name), max: 20) end def gen_vote(name)

    do tuple(like: { value(:vote), value(name), )}) end Command Generators
  38. def gen_commands(name) do list(of: gen_vote(name), max: 20) end def gen_vote(name)

    do tuple(like: { value(:vote), value(name), choose(from: [value(1), value(2), value(3)])}) end Command Generators
  39. defmodule ClientStateMachine do def vote(name, id) do %{"votes" => new_votes}

    = post(id, name) {:ok, new_votes} end def vote_next(state, [id, name], _result) do {:ok, update_in(state, [name, to_string(id)], &(&1 + 1))} end end
  40. defmodule ClientStateMachine do def vote(name, id) do %{"votes" => new_votes}

    = post(id, name) {:ok, new_votes} end def vote_next(state, [id, name], _result) do {:ok, update_in(state, [name, to_string(id)], &(&1 + 1))} end def vote_post(state, [id, name], actual_result) do expected_result = get_in(state, [name, to_string(id)]) + 1 {:ok, actual_result == expected_result} end end
  41. Property: Users votes should increase test “users votes increase after

    voting" do ptest [commands: gen_commands("chris")] do VoteCounter.reset() {_state, result} = run_commands(commands, Client) assert result end end
  42. Property: Users should get the correct votes test "users don't

    effect each others votes" do ptest [chris: gen_commands("chris"), jane: gen_commands("jane")] do end end
  43. Property: Users should get the correct votes test "users don't

    effect each others votes" do ptest [chris: gen_commands("chris"), jane: gen_commands("jane")] do VoteCounter.reset() end end
  44. Property: Users should get the correct votes test "users don't

    effect each others votes" do ptest [chris: gen_commands("chris"), jane: gen_commands("jane")] do VoteCounter.reset() {_state, result} = run_commands([chris, jane], Client) end end
  45. Property: Users should get the correct votes test "users don't

    effect each others votes" do ptest [chris: gen_commands("chris"), jane: gen_commands("jane")] do VoteCounter.reset() {_state, result} = run_commands([chris, jane], Client) assert result end end
  46. Property: Users should get the correct votes test "users don't

    effect each others votes" do ptest [chris: gen_commands("chris"), jane: gen_commands("jane")] do VoteCounter.reset() {_state, result} = run_parallel_commands([chris, jane], Client) assert result end end
  47. Running parallel tests def run_parallel_commands([l1, l2], module) do t1 =

    Task.async(fn -> run_commands(l1, module) end) t2 = Task.async(fn -> run_commands(l2, module) end) {_, ra} = Task.await(t1) {_, rb} = Task.await(t2) {:ok, ra && rb} end
  48. The Bug def new(conn, %{"id" => id, "name" => name})

    do {:ok, current_votes} = VoteCounter.get(id) new_votes = [name | current_votes] VoteCounter.put(id, new_votes) # Other nonsense end
  49. The Bug def new(conn, %{"id" => id, "name" => name})

    do {:ok, current_votes} = VoteCounter.get(id) new_votes = [name | current_votes] VoteCounter.put(id, new_votes) # Other nonsense end
  50. The Bug def new(conn, %{"id" => id, "name" => name})

    do {:ok, new_votes} = VoteCounter.incr(id, name) # Other nonsense end
  51. Resources: “Finding Race conditions in Erlang with QuickCheck and PULSE”

    http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.724.3518&rep=rep1&type=pdf Testing Async apis with QuickCheck https://www.youtube.com/watch?v=iW2J7Of8jsE&t=272s “QuickCheck: A lightweight tool for Random Testing of Haskell Programs” http://www.cs.tufts.edu/~nr/cs257/archive/john-hughes/quick.pdf Composing Test Generators https://www.youtube.com/watch?v=4-sPhFtGwZk Property based testing for better code https://www.youtube.com/watch?v=shngiiBfD80