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

Testing in Haskell & QuickCheck

Testing in Haskell & QuickCheck

George Pollard

May 19, 2015
Tweet

More Decks by George Pollard

Other Decks in Programming

Transcript

  1. Testing in Haskell – good news! Not that different –

    but there are useful tools. To write testable code, it’s much like any other language: • Aim for small functions • Minimize dependencies/coupling • Avoid IO/side-effects • etc
  2. Testing in Haskell – more good news Not that different

    – but there are useful tools. In order of preference: 1. Use the type system to enforce correctness 2. Use libraries to generate tests for us (QuickCheck) 3. Write tests ourselves
  3. Use the type system to enforce correctness We want to:

    minimize things that need to be tested, make it harder to write incorrect code. Two basic things: • Encode invariants in types • Prefer more general types
  4. Encode invariants in types lookupPerson :: String → IO (Maybe

    Person) type Name = String lookupPerson :: Name → IO (Maybe Person)
  5. Newtypes & smart constructors module Types (Name, toName) where --

    does not export constructor. “Name(..)” would -- A Name is a non-empty String. newtype Name = Name String deriving (Show, Eq, Ord) -- “Smart constructor” toName :: String → Maybe Name toName "" = Nothing toName n = Just (Name n) lookupPerson :: Name → IO (Maybe Person)
  6. Newtypes are cheap! public class Name : IEquatable<Name>, IComparable<Name> {

    private readonly string _value; private Name(string value) { _value = value; } public static Name Create(string value) { if (string.IsNullOrEmpty(value)) { return null; } return new Name(value); } public override bool Equals(object obj) { return Equals(obj as Name); } public bool Equals(Name other) { return other != null && _value.Equals(other._value); } public int CompareTo(Name other) { return _value.CompareTo(other._value); } public override int GetHashCode() { return _value.GetHashCode(); } public override string ToString() { return _value; } }
  7. Write generic/parametric code idBool :: Bool → Bool idBool x

    = x idBool x = not x idBool x = True idBool x = False
  8. Write generic/parametric code idInt8 :: Int8 → Int8 32317006071311007300714876688669951960444102669715484032 13034542752465513886789089319720141152291346368871796092

    18980194941195591504909210950881523864482831206308773673 00996091750197750389652106796057638384067568276792218642 61975616183809433847617047058164585203630504288757589154 10658086075523991239303855219143333896683424206849747865 64569494856176035326322058077805659331026192708460314150 25859286417711672594360371846185735759835115230164590440 36976132332872312271256847108202097251571017269313234696 78542580656697935045997268352998638215525166389437335543 60213543322960464531847860495214819355585361105959623065 6
  9. Write generic/parametric code map :: (a → b) → [a]

    → [b] > map abs numbers map :: (a → a) → [a] → [a]
  10. Avoid partial functions head :: [a] → a > head

    [] -- bang! fromJust :: Maybe a → a > fromJust (toName "") -- bang!
  11. Prefer pattern matching -- head :: [a] → a --

    instead case items of (item:rest) → {- do something -} [] → {- handle other case -}
  12. Prefer pattern matching -- fromMaybe :: Maybe a → a

    -- instead case something of Just x → {- do something -} Nothing → {- handle other case -}
  13. Using the type system to help enforce correctness Summary: •

    Enforce invariants with types • Opt for the more-general version of a function
  14. Using QuickCheck “QuickCheck: A Lightweight Tool for Random Testing of

    Haskell Programs” Implementations exist for: C, C++, Chicken Scheme, Clojure, Common Lisp, D, Elm, Erlang, F#, Factor, Io, Java, JavaScript, Node.js, Objective-C, OCaml, Perl, Prolog, Python, R, Ruby, Scala, Scheme, Smalltalk and Standard ML
  15. Using QuickCheck -- Define a property: reverseTwiceDoesNothing :: [Int] →

    Bool reverseTwiceDoesNothing list = reverse (reverse list) == list -- Test the property: > quickCheck reverseReverseDoesNothing +++ OK, passed 100 tests.
  16. Using QuickCheck -- toName :: String → Maybe Name toNameBehavesCorrectly

    s = let name = toName s in (isNothing name) == (s == "") -- Test the property: > quickCheck toNameBehavesCorrectly +++ OK, passed 100 tests.
  17. Using QuickCheck -- getId :: Name → Id getIdIsDistinct ::

    Name → Name → Bool getIdIsDistinct n1 n2 = (n1 == n2) == (getId n1 == getId n2) -- Test the property: > quickCheck getIdIsDistinct No instance for (Arbitrary Name) arising from a use of `quickCheck'
  18. Writing an Arbitrary instance class Arbitrary a where arbitrary ::

    Gen a choose :: Random a => (a, a) → Gen a -- Generates a random element in the given inclusive range. elements :: [a] → Gen a -- Generates one of the given values. frequency :: [(Int, Gen a)] → Gen a -- Chooses one of the given generators, with a weighted random distribution.
  19. Using generator combinators (“choose”) data Height = Height Int deriving

    (Show, Eq) class Arbitrary Height where -- arbitrary :: Gen Height arbitrary = do heightCms ← choose (100, 250) return (Height heightCms)
  20. Using generator combinators (“elements”) data Color = R | G

    | B deriving (Show, Eq) class Arbitrary Color where arbitrary = do color ← elements [R, G, B] return color
  21. Using generator combinators (“frequency”) data Color = R | G

    | B deriving (Show, Eq) class Arbitrary Color where arbitrary = do color ← frequency [(1, return R), (1, return G), (2, return B)] return color
  22. Using other Arbitrary instances data Point = Point Int Int

    deriving (Show, Eq) class Arbitrary Point where arbitrary = do x ← arbitrary -- this is Gen Int y ← arbitrary return (Point x y)
  23. Writing an Arbitrary instance for Name instance Abitrary Name where

    arbitrary = do s ← listOf1 arbitrary case toName s of Nothing → error "can’t happen" Just name → return name
  24. Using QuickCheck -- getId :: Name → Id getIdIsDistinct ::

    Name → Name → Bool getIdIsDistinct n1 n2 = (n1 == n2) == (getId n1 == getId n2) -- Test the property: > quickCheck getIdIsDistinct +++ OK, passed 100 tests.
  25. Making IO testable findLines :: String → FilePath → IO

    [String] findLines word filePath = do text ← readFile filePath return $ filter containsWord $ lines text where containsWord line = word `isInfixOf` line
  26. Making IO testable findLines :: String → FilePath → IO

    [String] findLines word filePath = do text ← readFile filePath return $ filter containsWord $ lines text where containsWord line = word `isInfixOf` line
  27. Making IO testable -- readFile :: FilePath → IO String

    -- writeFile :: FilePath → String → IO () class Monad m ⇒ RWMonad m where readText :: FilePath → m String writeText :: FilePath → String → m () instance RWMonad IO where readText = readFile writeText = writeFile
  28. Making IO testable findLines :: (RWMonad m) ⇒ String →

    FilePath → m [String] findLines word filePath = do text ← readText filePath return $ filter containsWord $ lines text where containsWord line = word `isInfixOf` line
  29. Making IO testable type InMemory = State (Map.Map FilePath String)

    instance RWMonad InMemory where readText fileName = do files ← get case Map.lookup fileName files of Nothing → error ("File not found: " ++ fileName) Just content → return content writeText fileName content = do files ← get put (Map.insert fileName content files) runInMemory :: InMemory r → r runInMemory f = evalState f Map.empty
  30. Try it yourself Using cabal to install quickcheck: Make a

    directory to work in. Inside the directory: “cabal sandbox init” Then: “cabal install quickcheck” Now you can make your .hs file, then load it with: “cabal repl Test.hs”