Property-Based Testing (Railscamp Sydney, 2015)

Property-Based Testing (Railscamp Sydney, 2015)

E34acb847338523dc088f03f0eedd1eb?s=128

Rob Howard

June 15, 2015
Tweet

Transcript

  1. Property-Based Testing

  2. Kinds of Testing

  3. Example-Based Tests

  4. +,  -­‐

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

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

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

  8. Property-Based Tests

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

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

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

     n   n  -­‐  1  <  n   n  +  1  -­‐  1  =  n      #
  12. Practicalities

  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
  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
  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
  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
  17. success:  100  tests      describes  +  and  -­‐

  18. Something that breaks

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

     x
  20. it  "split/join  is  reversible"  do      property_of  {  

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

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

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

           string      }.check  {  |x|          expect(
            x.split("  ").join("  ")
        ).to  eq(x)      }   end
  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)
  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)
  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)
  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)
  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)
  29. Three Things: Data Generation, Testing with the Data, Data Reduction

  30. Uses

  31. Edge Cases

  32. Treating the code like an adversary

  33. Honest TDD;
 No fudging code to pass an example test.

  34. Kinds of Properties

  35. Reversible

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

  37. n  +  1  -­‐  1  ==  n   ! #

     where  y  !=  "  "
 x.split(y).join(y)  ==  x Reversible
  38. n  +  1  -­‐  1  ==  n   ! #

     where  y  !=  "  "
 x.split(y).join(y)  ==  x   ! decompress(compress(d))  ==  d Reversible
  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
  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. :-)
  41. Repeatable

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

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

  44. Unbreakable Rules

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

  46.    list.sort.count  ==  list.count   !    list.sort.all?  {|x|  

           list.find_index(x)  !=  nil      } Unbreakable Rules
  47. Swapping the Ordering

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

     1} Swapping the Ordering
  49. Prove a Small Part

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

     y        }
 
      #  pairs([1,2,3])        #    =>  [[1,2],  [2,3]]   Prove a Small Part
  51. Hard to Solve, Easy to Check

  52.  solve(solvable_maze)      !=  nil    solve(unsolvable_maze)  ==  nil Hard

    to Solve, Easy to Check
  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.
  54. Consult an Oracle

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

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

    Oracle
  57. SHOW US SOME REAL EXAMPLES

  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
  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
  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
  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
  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
  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
  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
  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
  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]
  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]
  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]
  69. Random vs Exhaustive

  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)
  71. Fin. 
 Rob Howard
 @damncabbage robhoward.id.au