Notes (HPFP 14/31): Testing
14 Testing
Intermission: Short Exercise
14.7 Chapter Exercises
Validating numbers into words
Using QuickCheck
Failure
Irrational numbers like e.g. the sqaure root of 2 cannot be represented with infinite precision in a finite amount of memory. So an expression like (sqrt 2) is not actually equal to the square root of 2, but rather is an accurate approximation to some precision. So even though square is the inverse of square root, because sqrt cannot be infinitely accurate the square of a square root will have some error. E.g.
sqrt 2 = 1.4142135, (sqrt 2) ^ 2 = 1.9999999
Idempotence
Make a Gen random generator for the datatype
Hangman testing
Skipping this one, I think that this testing chapter is probably better understood after you understand what monads are. The reader here only barely has the tools to build something for which testing is important.
So I went back and did this exercise and my initial instinct has been confirmed. I found it a pretty interesting exercise to build a Gen Puzzle
arbitrary puzzle generator, but actually applyting that generator to a reasonable property test for the hangman game is way too complicated for a beginner given the material covered so far. I figured it out, but it requires using the monadicIO
function from the Test.QuickCheck.Monadic
library, which is super interesting in how it works, but yeah, definitely something I only understood on my second pass through this book.
In any case, check out my new and improved hangman. I was able to refactor out a lot of complexity, using a little monadic goodness. I also simplified the Puzzle
type by making it only contain two strings, one for the word and one for guesses:
data Puzzle = Puzzle { word :: String, guessed :: String } deriving Show
This simplifies the code enormously, because instead of dragging around a stateful filledIn :: [Maybe Char]
, we can instead just compute:
discovered :: Puzzle -> [Maybe Char]
= (\x -> if elem x (guessed p) then Just x else Nothing) <$> word p discovered p
This is a little slower, sure, to recompute discovered
every time we want to check the characters that have already been solved rather than already having it in our Puzzle
type. But honestly, the scope of the game is so small that it doesn’t matter. And if for some reason we need Puzzle
to work efficiently with million-character words, there are better ways to cache that computation, and bigger inefficiencies, like the use of String
, that we would have to address first.
Validating ciphers
This uses a little newtype
trick to make sure that the arbitrary strings we generate are all composed of lowercase characters. The reason being that the vignere
and caesar
functions have a built-in filter that ensures they only work on lowercase characters. Arguably not a great design decision on my part when I wrote vignere
and caesar
, but I wanted to test the functions I actually wrote in the previous chapters rather than rework them.
14.9 Follow-up resources
- Pedro Vasconcelos; An introduction to QuickCheck testing;
- Koen Claessen and John Hughes; (2000) QuickCheck: A Lightweight Tool for Random Testing of Haskell Programs
- Pedro Vasconcelos;Verifying a Simple Compiler Using Property-based Random Testing;