QCon London 2016 An Introduction to Property Based Testing Aaron - - PowerPoint PPT Presentation

qcon london 2016
SMART_READER_LITE
LIVE PREVIEW

QCon London 2016 An Introduction to Property Based Testing Aaron - - PowerPoint PPT Presentation

QCon London 2016 An Introduction to Property Based Testing Aaron Bedra Chief Security Officer, Eligible @abedra Why do we test? To better understand what we are building To help us think deeper about what we are building To ensure


slide-1
SLIDE 1

QCon London 2016

An Introduction to Property Based Testing

Aaron Bedra Chief Security Officer, Eligible @abedra

slide-2
SLIDE 2

Why do we test?

  • To better understand what we are building
  • To help us think deeper about what we are building
  • To ensure the correctness of what we are building
  • To help us explore our design*
  • To explain to others how our code should work
slide-3
SLIDE 3

How do we test?

  • With compilers (type systems, static analysis, etc)
  • Manual testing
  • X-Unit style tests
  • Property/generative based tests
  • Formal modeling
slide-4
SLIDE 4

How do we test?

  • With compilers (type systems, static analysis, etc)
  • Manual testing
  • X-Unit style tests
  • Property/generative based tests
  • Formal modeling
slide-5
SLIDE 5

What is it?

slide-6
SLIDE 6

An abstraction

slide-7
SLIDE 7

Property based testing eliminates the guess work on value and

  • rder of operations testing
slide-8
SLIDE 8

Magic numbers

slide-9
SLIDE 9

Instead of specifying how you specify what

slide-10
SLIDE 10

Testing over time

slide-11
SLIDE 11

When we start our test suite, things are usually easy to understand

slide-12
SLIDE 12

public class Basic { public static Integer calculate(Integer x, Integer y) { return x + y; } }

slide-13
SLIDE 13

public class BasicTest { @Test public void TestCalculate() { assertEquals(Integer.valueOf(5), Basic.calculate(3, 2)); } }

slide-14
SLIDE 14

What other tests might we write for this code?

slide-15
SLIDE 15

Like all programs we start simple

slide-16
SLIDE 16

But over time things get more complicated

slide-17
SLIDE 17

What happens when our simple calculate function grows to include an entire domain?

slide-18
SLIDE 18

Our test suite will undoubtedly grow, but we have options to control the growth

slide-19
SLIDE 19

And also maintain confidence in our tests

slide-20
SLIDE 20
slide-21
SLIDE 21

By changing our mental model just a bit we can cover much more ground

slide-22
SLIDE 22

Let’s revisit our basic example

slide-23
SLIDE 23

public class Basic { public static Integer calculate(Integer x, Integer y) { return x + y; } }

slide-24
SLIDE 24

But instead of a unit test, let’s write a property

slide-25
SLIDE 25

@RunWith(JUnitQuickcheck.class) public class BasicProperties { @Property public void calculateBaseAssumption(Integer x, Integer y) { Integer expected = x + y; assertEquals(expected, Basic.calculate(x, y)); } }

public class BasicTest { @Test public void TestCalculate() { assertEquals(Integer.valueOf(5), Basic.calculate(3, 2)); } }

slide-26
SLIDE 26

@RunWith(JUnitQuickcheck.class) public class BasicProperties { @Property(trials = 1000000) public void calculateBaseAssumption(Integer x, Integer y) { Integer expected = x + y; assertEquals(expected, Basic.calculate(x, y)); } }

slide-27
SLIDE 27

This property isn’t much different than the unit test we had before it

slide-28
SLIDE 28

It’s just one level of abstraction higher

slide-29
SLIDE 29

Let’s add a constraint to

  • ur calculator
slide-30
SLIDE 30

Let’s say that the output cannot be negative

slide-31
SLIDE 31

public class Basic { public static Integer calculate(Integer x, Integer y) { Integer total = x + y; if (total < 0) { return 0; } else { return total; } } } java.lang.AssertionError: Property calculateBaseAssumption falsified for args shrunken to [0, -679447654]

slide-32
SLIDE 32

Shrinking

slide-33
SLIDE 33

@RunWith(JUnitQuickcheck.class) public class BasicProperties { @Property public void calculateBaseAssumption(Integer x, Integer y) { Integer expected = x + y; assertEquals(expected, Basic.calculate(x, y)); } }

public class Basic { public static Integer calculate(Integer x, Integer y) { Integer total = x + y; if (total < 0) { return 0; } else { return total; } } }

slide-34
SLIDE 34

Now we can be more specific with our property

slide-35
SLIDE 35

@RunWith(JUnitQuickcheck.class) public class BasicProperties { @Property public void calculateBaseAssumption(Integer x, Integer y) { assumeThat(x, greaterThan(0)); assumeThat(y, greaterThan(0)); assertThat(Basic.calculate(x, y), is(greaterThan(0))); } } java.lang.AssertionError: Property calculateBaseAssumption falsified for args shrunken to [647853159, 1499681379]

slide-36
SLIDE 36

We could keep going from here but let’s dive into some of the concepts

slide-37
SLIDE 37

Refactoring

slide-38
SLIDE 38

This is one of my favorite use cases for invoking property based testing

slide-39
SLIDE 39

Legacy code becomes the model

slide-40
SLIDE 40

It’s incredibly powerful

slide-41
SLIDE 41

It ensures you have exact feature parity

slide-42
SLIDE 42

Even for unintended features!

slide-43
SLIDE 43

Generators

slide-44
SLIDE 44

You can use them for all kinds of things

slide-45
SLIDE 45

Scenario

slide-46
SLIDE 46

Every route in your web application

slide-47
SLIDE 47

You could define generators based on your routes

slide-48
SLIDE 48

And create valid and invalid inputs for every endpoint

slide-49
SLIDE 49

You could run the generators on every test

slide-50
SLIDE 50

Or save the output of the generation for faster execution

slide-51
SLIDE 51

Saved execution of generators can even bring you to simulation testing

slide-52
SLIDE 52

There are tons of property based testing libraries available

slide-53
SLIDE 53

But this is a talk in a functional language track

slide-54
SLIDE 54

So let’s have some fun

slide-55
SLIDE 55

Let’s pretend we have some legacy code

slide-56
SLIDE 56

Written in C

slide-57
SLIDE 57

And we want to test it to make sure it actually works

slide-58
SLIDE 58

But there are no quickcheck libraries available*

slide-59
SLIDE 59

Warning! The crypto you are about to see should not be attempted at work

slide-60
SLIDE 60

Caesar’s Cipher

slide-61
SLIDE 61

Let’s start with our implementation

slide-62
SLIDE 62

#include <stdlib.h> #include <string.h> #include <ctype.h> char *caesar(int shift, char *input) { char *output = malloc(strlen(input)); memset(output, '\0', strlen(input)); for (int x = 0; x < strlen(input); x++) { if (isalpha(input[x])) { int c = toupper(input[x]); c = (((c - 65) + shift) % 26) + 65;

  • utput[x] = c;

} else {

  • utput[x] = input[x];

} } return output; }

slide-63
SLIDE 63

Next we create a new implementation to test against

slide-64
SLIDE 64

caesar :: Int -> String -> String caesar k = map f where f c | inRange ('A', 'Z') c = chr $ ord 'A' + (ord c - ord 'A' + k) `mod` 26 | otherwise = c

slide-65
SLIDE 65

We now have two functions that “should” do the same thing

slide-66
SLIDE 66

But they aren’t in the same language

slide-67
SLIDE 67

Thankfully Haskell has good FFI support

slide-68
SLIDE 68

foreign import ccall "caesar.h caesar" c_caesar :: CInt -> CString -> CString native_caesar :: Int -> String -> IO String native_caesar shift input = withCString input $ \c_str -> peekCString(c_caesar (fromIntegral shift) c_str)

slide-69
SLIDE 69

$ stack exec ghci caesar.hs caesar.so GHCi, version 7.10.3: http://www.haskell.org/ghc/ :? for help [1 of 1] Compiling Main ( caesar.hs, interpreted ) Ok, modules loaded: Main. *Main> caesar 2 "ATTACKATDAWN" "CVVCEMCVFCYP" *Main> native_caesar 2 "ATTACKATDAWN" "CVVCEMCVFCYP"

slide-70
SLIDE 70

We can now execute our C code from inside of Haskell

slide-71
SLIDE 71

We can use Haskell’s quickcheck library to verify our C code

slide-72
SLIDE 72

First we need to write a property

slide-73
SLIDE 73

unsafeEq :: IO String -> String -> Bool unsafeEq x y = unsafePerformIO(x) == y genSafeChar :: Gen Char genSafeChar = elements ['A' .. 'Z'] genSafeString :: Gen String genSafeString = listOf genSafeChar newtype SafeString = SafeString { unwrapSafeString :: String } deriving Show instance Arbitrary SafeString where arbitrary = SafeString <$> genSafeString equivalenceProperty = forAll genSafeString $ \str -> unsafeEq (native_caesar 2 str) (caesar 2 str)

slide-74
SLIDE 74

unsafeEq :: IO String -> String -> Bool unsafeEq x y = unsafePerformIO(x) == y genSafeChar :: Gen Char genSafeChar = elements ['A' .. 'Z'] genSafeString :: Gen String genSafeString = listOf genSafeChar newtype SafeString = SafeString { unwrapSafeString :: String } deriving Show instance Arbitrary SafeString where arbitrary = SafeString <$> genSafeString equivalenceProperty = forAll genSafeString $ \str -> unsafeEq (native_caesar 2 str) (caesar 2 str)

slide-75
SLIDE 75

unsafeEq :: IO String -> String -> Bool unsafeEq x y = unsafePerformIO(x) == y genSafeChar :: Gen Char genSafeChar = elements ['A' .. 'Z'] genSafeString :: Gen String genSafeString = listOf genSafeChar newtype SafeString = SafeString { unwrapSafeString :: String } deriving Show instance Arbitrary SafeString where arbitrary = SafeString <$> genSafeString equivalenceProperty = forAll genSafeString $ \str -> unsafeEq (native_caesar 2 str) (caesar 2 str)

slide-76
SLIDE 76

unsafeEq :: IO String -> String -> Bool unsafeEq x y = unsafePerformIO(x) == y genSafeChar :: Gen Char genSafeChar = elements ['A' .. 'Z'] genSafeString :: Gen String genSafeString = listOf genSafeChar newtype SafeString = SafeString { unwrapSafeString :: String } deriving Show instance Arbitrary SafeString where arbitrary = SafeString <$> genSafeString equivalenceProperty = forAll genSafeString $ \str -> unsafeEq (native_caesar 2 str) (caesar 2 str)

slide-77
SLIDE 77

unsafeEq :: IO String -> String -> Bool unsafeEq x y = unsafePerformIO(x) == y genSafeChar :: Gen Char genSafeChar = elements ['A' .. 'Z'] genSafeString :: Gen String genSafeString = listOf genSafeChar newtype SafeString = SafeString { unwrapSafeString :: String } deriving Show instance Arbitrary SafeString where arbitrary = SafeString <$> genSafeString equivalenceProperty = forAll genSafeString $ \str -> unsafeEq (native_caesar 2 str) (caesar 2 str)

slide-78
SLIDE 78

unsafeEq :: IO String -> String -> Bool unsafeEq x y = unsafePerformIO(x) == y genSafeChar :: Gen Char genSafeChar = elements ['A' .. 'Z'] genSafeString :: Gen String genSafeString = listOf genSafeChar newtype SafeString = SafeString { unwrapSafeString :: String } deriving Show instance Arbitrary SafeString where arbitrary = SafeString <$> genSafeString equivalenceProperty = forAll genSafeString $ \str -> unsafeEq (native_caesar 2 str) (caesar 2 str)

slide-79
SLIDE 79

*Main> quickCheck equivalenceProperty *** Failed! Falsifiable (after 20 tests): "QYMSMCWTIXNDFDMLSL" *Main> caesar 2 "QYMSMCWTIXNDFDMLSL" "SAOUOEYVKZPFHFONUN" *Main> native_caesar 2 "QYMSMCWTIXNDFDMLSL" “SAOUOEYVKZPFHFONUN/Users/abedra/x“

slide-80
SLIDE 80

#include <stdlib.h> #include <string.h> #include <ctype.h> char *caesar(int shift, char *input) { char *output = malloc(strlen(input)); memset(output, '\0', strlen(input)); for (int x = 0; x < strlen(input); x++) { if (isalpha(input[x])) { int c = toupper(input[x]); c = (((c - 65) + shift) % 26) + 65;

  • utput[x] = c;

} else {

  • utput[x] = input[x];

} } return output; }

slide-81
SLIDE 81

We’ve found a memory handling issue in our C code!

slide-82
SLIDE 82

In reality there are more issues with this code, but our issue was quickly exposed

slide-83
SLIDE 83

And easily reproduced

slide-84
SLIDE 84

Wrapping up

slide-85
SLIDE 85

Not all testing is created equal

slide-86
SLIDE 86

You should use as many different testing techniques as you need

slide-87
SLIDE 87

Remember to think about the limits of your tools

slide-88
SLIDE 88

And use tools that help you achieve your results more effectively

slide-89
SLIDE 89

And more efficiently

slide-90
SLIDE 90

Questions?