$30 off During Our Annual Pro Sale. View Details »

Property-Based Testing (Railscamp Sydney, 2015)

Property-Based Testing (Railscamp Sydney, 2015)

Rob Howard

June 15, 2015
Tweet

More Decks by Rob Howard

Other Decks in Technology

Transcript

  1. Property-Based
    Testing

    View Slide

  2. Kinds of Testing

    View Slide

  3. Example-Based
    Tests

    View Slide

  4. +,  -­‐

    View Slide

  5. expect(1  +  2).to  eq  3

    View Slide

  6. expect(3  -­‐  2).to  eq  1

    View Slide

  7. expect(3  -­‐  2).to  eq  1

    View Slide

  8. Property-Based
    Tests

    View Slide

  9. For  some  number,  n:  
    !
    n  +  1  >  n

    View Slide

  10. For  some  number,  n:  
    !
    n  +  1  >  n  
    n  -­‐  1  <  n

    View Slide

  11. For  some  number,  n:  
    !
    n  +  1  >  n  
    n  -­‐  1  <  n  
    n  +  1  -­‐  1  =  n      #

    View Slide

  12. Practicalities

    View Slide

  13. it  "describes  +  and  -­‐"  do  
       property_of  {  
           integer  
       }.check  {  |n|  
           expect(n  +  1  >  n).to  be  true  
           expect(n  -­‐  1  <  n).to  be  true  
           expect(n  +  1  -­‐  1).to  eq  n  
       }  
    end

    View Slide

  14. it  "describes  +  and  -­‐"  do  
       property_of  {  
           integer  
       }.check  {  |n|  
           expect(n  +  1  >  n).to  be  true  
           expect(n  -­‐  1  <  n).to  be  true  
           expect(n  +  1  -­‐  1).to  eq  n  
       }  
    end

    View Slide

  15. it  "describes  +  and  -­‐"  do  
       property_of  {  
           integer  
       }.check  {  |n|  
           expect(n  +  1  >  n).to  be  true  
           expect(n  -­‐  1  <  n).to  be  true  
           expect(n  +  1  -­‐  1).to  eq  n  
       }  
    end

    View Slide

  16. it  "describes  +  and  -­‐"  do  
       property_of  {  
           integer  
       }.check  {  |n|  
           expect(n  +  1  >  n).to  be  true  
           expect(n  -­‐  1  <  n).to  be  true  
           expect(n  +  1  -­‐  1).to  eq  n  
       }  
    end

    View Slide

  17. success:  100  tests  
       describes  +  and  -­‐

    View Slide

  18. Something that
    breaks

    View Slide

  19. For  some  string,  x:  
    !
    x.split("  ").join("  ")  ==  x

    View Slide

  20. it  "split/join  is  reversible"  do  
       property_of  {  
           string  
       }.check  {  |x|  
           expect(

               x.split("  ").join("  ")

           ).to  eq(x)  
       }  
    end

    View Slide

  21. it  "split/join  is  reversible"  do  
       property_of  {  
           string  
       }.check  {  |x|  
           expect(

               x.split("  ").join("  ")

           ).to  eq(x)  
       }  
    end

    View Slide

  22. it  "split/join  is  reversible"  do  
       property_of  {  
           string  
       }.check  {  |x|  
           expect(

               x.split("  ").join("  ")

           ).to  eq(x)  
       }  
    end

    View Slide

  23. it  "split/join  is  reversible"  do  
       property_of  {  
           string  
       }.check  {  |x|  
           expect(

               x.split("  ").join("  ")

           ).to  eq(x)  
       }  
    end

    View Slide

  24. failure  after  7  tests,  on:  
    "  &2M1`"  
    found  a  reduced  failure  case:  
    "  &M1`"  
    found  a  reduced  failure  case:  
    "  M1`"  
    found  a  reduced  success:  
    "M1`"  
    minimal  failed  data  is:  
    "  M1`"  
       split/join  is  reversible  (FAILED  -­‐  1)

    View Slide

  25. failure  after  7  tests,  on:  
    "  &2M1`"  
    found  a  reduced  failure  case:  
    "  &M1`"  
    found  a  reduced  failure  case:  
    "  M1`"  
    found  a  reduced  success:  
    "M1`"  
    minimal  failed  data  is:  
    "  M1`"  
       split/join  is  reversible  (FAILED  -­‐  1)

    View Slide

  26. failure  after  7  tests,  on:  
    "  &2M1`"  
    found  a  reduced  failure  case:  
    "  &M1`"  
    found  a  reduced  failure  case:  
    "  M1`"  
    found  a  reduced  success:  
    "M1`"  
    minimal  failed  data  is:  
    "  M1`"  
       split/join  is  reversible  (FAILED  -­‐  1)

    View Slide

  27. failure  after  7  tests,  on:  
    "  &2M1`"  
    found  a  reduced  failure  case:  
    "  &M1`"  
    found  a  reduced  failure  case:  
    "  M1`"  
    found  a  reduced  success:  
    "M1`"  
    minimal  failed  data  is:  
    "  M1`"  
       split/join  is  reversible  (FAILED  -­‐  1)

    View Slide

  28. failure  after  7  tests,  on:  
    "  &2M1`"  
    found  a  reduced  failure  case:  
    "  &M1`"  
    found  a  reduced  failure  case:  
    "  M1`"  
    found  a  reduced  success:  
    "M1`"  
    minimal  failed  data  is:  
    "  M1`"  
       split/join  is  reversible  (FAILED  -­‐  1)

    View Slide

  29. Three Things:
    Data Generation,
    Testing with the Data,
    Data Reduction

    View Slide

  30. Uses

    View Slide

  31. Edge Cases

    View Slide

  32. Treating the code like
    an adversary

    View Slide

  33. Honest TDD;

    No fudging code to
    pass an example test.

    View Slide

  34. Kinds of Properties

    View Slide

  35. Reversible

    View Slide

  36. n  +  1  -­‐  1  ==  n
    Reversible

    View Slide

  37. n  +  1  -­‐  1  ==  n  
    !
    #  where  y  !=  "  "

    x.split(y).join(y)  ==  x
    Reversible

    View Slide

  38. n  +  1  -­‐  1  ==  n  
    !
    #  where  y  !=  "  "

    x.split(y).join(y)  ==  x  
    !
    decompress(compress(d))  ==  d
    Reversible

    View Slide

  39. n  +  1  -­‐  1  ==  n  
    !
    #  where  y  !=  "  "

    x.split(y).join(y)  ==  x  
    !
    decompress(compress(d))  ==  d  
    !
           t.to_zone('UTC')

             .to_zone(t.zone)  ==  t.zone
    Reversible

    View Slide

  40. n  +  1  -­‐  1  ==  n  
    !
    #  where  y  !=  "  "

    x.split(y).join(y)  ==  x  
    !
    decompress(compress(d))  ==  d  
    !
           t.to_zone('UTC')

             .to_zone(t.zone)  ==  t.zone
    Reversible
    I goofed this in the original; I've
    fixed this example. Pardon my
    onstage embarrassment. :-)

    View Slide

  41. Repeatable

    View Slide

  42. list.sort.sort  ==  list.sort
    Repeatable

    View Slide

  43. list.sort.sort  ==  list.sort  
    !
    handler.handle(evt).handle(evt)  
    ==  handler.handle(evt)
    Repeatable

    View Slide

  44. Unbreakable Rules

    View Slide

  45.    list.sort.count  ==  list.count
    Unbreakable Rules

    View Slide

  46.    list.sort.count  ==  list.count  
    !
       list.sort.all?  {|x|  
           list.find_index(x)  !=  nil  
       }
    Unbreakable Rules

    View Slide

  47. Swapping the Ordering

    View Slide

  48. a.map{|n|  n  +  1}.sort  
    ==  
    a.sort.map{|n|  n  +  1}
    Swapping the Ordering

    View Slide

  49. Prove a Small Part

    View Slide

  50.      pairs(list.sort).all?{|(x,y)|  
             x  <=  y  
         }


         #  pairs([1,2,3])  
         #    =>  [[1,2],  [2,3]]  
    Prove a Small Part

    View Slide

  51. Hard to Solve, Easy to Check

    View Slide

  52.  solve(solvable_maze)      !=  nil  
     solve(unsolvable_maze)  ==  nil
    Hard to Solve, Easy to Check

    View Slide

  53.  solve(solvable_maze)      !=  nil  
     solve(unsolvable_maze)  ==  nil
    Hard to Solve, Easy to Check
    I really punted on this example,
    particularly how you generate a maze
    you know is solvable. Sorry.

    View Slide

  54. Consult an Oracle

    View Slide

  55. list.hypersort  ==  list.sort
    Consult an Oracle

    View Slide

  56. list.hypersort  ==  list.sort  
    !
    new_code(input)  ==  old_code(input)
    Consult an Oracle

    View Slide

  57. SHOW US SOME
    REAL EXAMPLES

    View Slide

  58. it  "can  round-­‐trip  last-­‐logged-­‐in"  do  
       property_of  {  
           (Time.current  -­‐  float.abs)  
       }.check  {  |time|  
           user  =  User.create(

               username:  "Sam",  
               last_logged_in_at:  time,  
           )  
           expect(  
               User.find(user.id).last_logged_in_at  
           ).to  eq(time)  
       }  
    end

    View Slide

  59. it  "can  round-­‐trip  last-­‐logged-­‐in"  do  
       property_of  {  
           (Time.current  -­‐  float.abs)  
       }.check  {  |time|  
           user  =  User.create(

               username:  "Sam",  
               last_logged_in_at:  time,  
           )  
           expect(  
               User.find(user.id).last_logged_in_at  
           ).to  eq(time)  
       }  
    end

    View Slide

  60. it  "can  round-­‐trip  last-­‐logged-­‐in"  do  
       property_of  {  
           (Time.current  -­‐  float.abs)  
       }.check  {  |time|  
           user  =  User.create(

               username:  "Sam",  
               last_logged_in_at:  time,  
           )  
           expect(  
               User.find(user.id).last_logged_in_at  
           ).to  eq(time)  
       }  
    end

    View Slide

  61. failure:  0  tests,  on:  
    Sat,  13  Jun  2015  04:39:52  UTC  +00:00  
           can  round-­‐trip  last-­‐logged-­‐in  (FAILED  -­‐  1)  
    !
    Failures:  
    !
       1)  User  round-­‐trip  can  round-­‐trip  last-­‐logged-­‐in  
             Failure/Error:  
    expect(User.find(user.id).last_logged_in_at).to  eq  
    time  
    !
                 expected:  2015-­‐06-­‐13  04:39:52.835645641  +0000  
                           got:  2015-­‐06-­‐13  04:39:52.835645000  +0000

    View Slide

  62. it  "after_transition  args"  do  
       property_of  {  
           array  {  choose  boolean,  string,  integer}  
       }.check  {  |args|  
           test  =  -­‐>  (a)  {  expect(a).to  eq(args)  }  
    !
           machine  =  Class.new  do  
               state_machine  initial:  :stopped  do  
                   event  :go  do  
                       transition  :stopped  =>  :going  
                   end  
                   after_transition(:stopped  =>  :going,  
                                                     :do  =>  proc  {  |machine,transition|  
                                                         test.call(transition.args)  
                                                     })  
               end  
           end  
    !
           machine.new.go(*args)  
       }  
    end

    View Slide

  63. it  "after_transition  args"  do  
       property_of  {  
           array  {  choose  boolean,  string,  integer}  
       }.check  {  |args|  
           test  =  -­‐>  (a)  {  expect(a).to  eq(args)  }  
    !
           machine  =  Class.new  do  
               state_machine  initial:  :stopped  do  
                   event  :go  do  
                       transition  :stopped  =>  :going  
                   end  
                   after_transition(:stopped  =>  :going,  
                                                     :do  =>  proc  {  |machine,transition|  
                                                         test.call(transition.args)  
                                                     })  
               end  
           end  
    !
           machine.new.go(*args)  
       }  
    end

    View Slide

  64. it  "after_transition  args"  do  
       property_of  {  
           array  {  choose  boolean,  string,  integer}  
       }.check  {  |args|  
           test  =  -­‐>  (a)  {  expect(a).to  eq(args)  }  
    !
           machine  =  Class.new  do  
               state_machine  initial:  :stopped  do  
                   event  :go  do  
                       transition  :stopped  =>  :going  
                   end  
                   after_transition(:stopped  =>  :going,  
                                                     :do  =>  proc  {  |machine,transition|  
                                                         test.call(transition.args)  
                                                     })  
               end  
           end  
    !
           machine.new.go(*args)  
       }  
    end

    View Slide

  65. it  "after_transition  args"  do  
       property_of  {  
           array  {  choose  boolean,  string,  integer}  
       }.check  {  |args|  
           test  =  -­‐>  (a)  {  expect(a).to  eq(args)  }  
    !
           machine  =  Class.new  do  
               state_machine  initial:  :stopped  do  
                   event  :go  do  
                       transition  :stopped  =>  :going  
                   end  
                   after_transition(:stopped  =>  :going,  
                                                     :do  =>  proc  {  |machine,transition|  
                                                         test.call(transition.args)  
                                                     })  
               end  
           end  
    !
           machine.new.go(*args)  
       }  
    end

    View Slide

  66. failure:  1  tests,  on:  
    ["y}K'ID",  "aR/-­‐xm",  "^H:/_B",  true,  false,  true]  
    found  a  reduced  failure  case:  
    ["y}K'D",  "aR/-­‐xm",  "^H:/_B",  true,  false,  true]  
    found  a  reduced  failure  case:  
    ["}K'D",  "aR/-­‐xm",  "^H:/_B",  true,  false,  true]  
    ...  
    found  a  reduced  failure  case:  
    ["",  "",  "",  true,  false,  true]  
    found  a  reduced  failure  case:  
    ["",  "",  "",  true,  false]  
    found  a  reduced  failure  case:  
    ["",  "",  "",  false]  
    found  a  reduced  success:  
    ["",  "",  ""]  
    minimal  failed  data  is:  
    ["",  "",  "",  false]

    View Slide

  67. failure:  1  tests,  on:  
    ["y}K'ID",  "aR/-­‐xm",  "^H:/_B",  true,  false,  true]  
    found  a  reduced  failure  case:  
    ["y}K'D",  "aR/-­‐xm",  "^H:/_B",  true,  false,  true]  
    found  a  reduced  failure  case:  
    ["}K'D",  "aR/-­‐xm",  "^H:/_B",  true,  false,  true]  
    ...  
    found  a  reduced  failure  case:  
    ["",  "",  "",  true,  false,  true]  
    found  a  reduced  failure  case:  
    ["",  "",  "",  true,  false]  
    found  a  reduced  failure  case:  
    ["",  "",  "",  false]  
    found  a  reduced  success:  
    ["",  "",  ""]  
    minimal  failed  data  is:  
    ["",  "",  "",  false]

    View Slide

  68. failure:  1  tests,  on:  
    ["y}K'ID",  "aR/-­‐xm",  "^H:/_B",  true,  false,  true]  
    found  a  reduced  failure  case:  
    ["y}K'D",  "aR/-­‐xm",  "^H:/_B",  true,  false,  true]  
    found  a  reduced  failure  case:  
    ["}K'D",  "aR/-­‐xm",  "^H:/_B",  true,  false,  true]  
    ...  
    found  a  reduced  failure  case:  
    ["",  "",  "",  true,  false,  true]  
    found  a  reduced  failure  case:  
    ["",  "",  "",  true,  false]  
    found  a  reduced  failure  case:  
    ["",  "",  "",  false]  
    found  a  reduced  success:  
    ["",  "",  ""]  
    minimal  failed  data  is:  
    ["",  "",  "",  false]

    View Slide

  69. Random
    vs
    Exhaustive

    View Slide

  70. Refs, Credits and
    Things to Look At
    • fsharpforfunandprofit.com

    (Property-Based Testing Posts)

    • github.com/charleso/property-testing-preso
    (LambdaJam talk)

    • Rantly (Ruby, used in examples)

    • QuickCheck, SmallCheck (Haskell)

    • Hypothesis (Python)

    View Slide

  71. Fin.

    Rob Howard

    @damncabbage

    robhoward.id.au

    View Slide