Before you begin
Why this matters
A function can pass every check you wrote and still be wrong for an input you did not think of. Write down what the function should do before you check it. Then look for the input that proves it wrong, and you find the bug before someone else does.
What this helps you reason about
- Write a check whose expected value comes from what the function should do, not from what it does.
- Choose the inputs where code is most often wrong: empty, one, equal, first and last, zero, and negative.
- Tell a check that could fail from one that always passes.
A team app shows each team’s worst game of a season. The function behind it has three tests, and all three pass. This lesson asks what those passing tests prove, and how to find the input that proves the function wrong.
Already know this? The short version
- Write down what the function should return, and for which inputs, before you choose inputs; every expected value comes from that sentence, worked out by hand, never from running the code.
- Passing checks prove that sentence only for their inputs; one input that gives a wrong result disproves it, so try inputs the code was not designed around.
- Try the edges first: empty, one element, equal values, first and last place, zero, negative, and no negatives at all.
- A check with no comparison, or whose expected value comes from the code it checks, cannot fail.
- Control.Exception.assert throws when its condition is False and its result is demanded, but GHC removes it under -O and -fignore-asserts; return Maybe or Either for failures that come from outside.
Words in this lesson (7)
- test
- Code that calls a function with a chosen input, compares the result with an expected value worked out in advance, and reports pass or fail.
- counterexample
- One input for which a belief about a function is false. A single counterexample is enough to show that the belief is wrong.
- regression test
- A check, kept with the code, that fails while a bug is present and passes once it is fixed, so the bug cannot come back unnoticed.
- boundary
- An input at the edge of what a function handles, such as an empty list, a single element, equal values, the first or last element, zero, or a negative number.
- assertion
- A check written into the program that a condition is true at that point. assert condition value, from Control.Exception, gives value when the condition is True and throws an exception when it is False; GHC removes it under -O or -fignore-asserts.
- precondition
- Something that must be true when a function is called, such as “the list is not empty”. The code that calls the function is responsible for it.
- pragma
- An instruction to the compiler written between {-# and #-}. {-# OPTIONS_GHC -fignore-asserts #-} at the top of a file passes that flag to GHC for the file.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / The belief
What do you believe the function does?
A test checks a belief about a function, so write the belief down first. The belief is one sentence that says what the function returns, and for which inputs. Then write the tests. A test is a call with a chosen input, plus the value that the belief says the call should return, called the expected value. Work out each expected value from the belief, by hand, before you run the code. The test then compares two things made separately: what the code returns and what the belief says.
For each game, the app stores a score: the team’s points minus the other team’s points. So -1 means the team lost by one point, 0 means a draw, and 2 means the team won by two points. The belief: lowest returns the smallest score in a list that has at least one score. The tests use a small function, check name got want, where got is the value lowest returned and want is the expected value. check returns the line to print: pass when got equals want, and FAIL with both values when they differ. putStrLn prints that line. The full code of check is in the experiment of the next section.
foldr min 0 scores combines the scores with min, starting from 0: for [1, -1, 2] it computes min 1 (min (-1) (min 2 0)). A negative number as an argument needs parentheses, as in (-1).
-- lowest should return the smallest score of a list
-- that has at least one score.
lowest :: [Int] -> Int
lowest scores = foldr min 0 scores
-- Each line prints pass.
putStrLn (check "mixed" (lowest [1, -1, 2]) (-1))
putStrLn (check "a draw" (lowest [0, 3]) 0)
putStrLn (check "one game" (lowest [-2]) (-2)) Write down your belief and each expected value before you run the code.
Coming from Python: functools.reduce(min, scores, 0) has the same trap as foldr min 0: the starting value 0 takes part in every min, so it is the answer for any list of wins.
Precisely: in a real project
Haskell projects write example tests with Hspec and properties with QuickCheck, and run both with cabal test. Below, it names one example, and shouldBe compares the result with the expected value. property asks QuickCheck to try many generated lists; ==> skips the lists that do not meet the condition on its left, here the empty ones. $ applies the function on its left to everything on its right, and \scores -> … is a function of scores. On foldr min 0, the property is false for every list of positive scores, so QuickCheck soon finds one.
This lesson uses a plain check function instead, because the stepper runs one file with no test tool.
import Test.Hspec
import Test.QuickCheck
main :: IO ()
main = hspec $ describe "lowest" $ do
it "finds the worst game when every game was won" $
lowest [2, 1] `shouldBe` 1
it "agrees with minimum on lists that are not empty" $
property $ \scores ->
not (null scores) ==> lowest scores == minimum scores 02 / Counterexamples
Which input would prove the belief wrong?
Passing tests show that the belief holds for their inputs. They say nothing about other inputs. To learn more, look for a counterexample: one input for which the belief is false. One counterexample is enough to show that a belief is wrong, however many tests pass.
Look first at inputs the code was not designed around. Every list in the tests has a game the team lost or drew. A strong team can win every game.
Here lowest [2, 1] returns 0, a draw the team never played, so [2, 1] is a counterexample. foldr min 0 [2, 1] is min 2 (min 1 0), which is 0. Keep the counterexample as a test. After the repair it passes, and it stays as a regression test, the kind of check that Minimal reproductions ends with.
-- The belief gives 1.
putStrLn (check "all wins" (lowest [2, 1]) 1)
-- FAIL all wins: got 0, want 1 Passing tests show that the belief holds for their inputs; one counterexample shows that it is false.
Coming from Python: Hypothesis in Python and QuickCheck in Haskell do the same job: generate many inputs, look for a counterexample, and shrink it before reporting it.
Precisely: letting a tool find counterexamples
Property-based testing states a belief as a property, such as “the result is one of the scores and no score is lower”. A tool then generates hundreds of inputs and looks for a counterexample. When it finds one, it shrinks the input, as in Minimal reproductions, before reporting it. For Haskell, QuickCheck is such a tool, and it came first: many libraries in other languages copy it. The property in the first section’s note finds this counterexample without a hand-picked input.
Experiment
Add a team that won every game
foldr min 0 scores starts from 0 and takes the min of each score with the result so far: for [2, 1] it computes min 2 (min 1 0), which is 0, a draw the team never played. The 0 is not a score from the list. Each of the three tests had a score at or below 0, where starting at 0 does no harm.
Why it matters Passing tests show that the belief holds for their inputs. To find a bug, try an input the code was not designed around, and keep the counterexample as a test after the repair.
All three tests print pass. Predict whether lowest is right for every list. Then add putStrLn (check "all wins" (lowest [2, 1]) 1) as the last line of main and run again.
lowest :: [Int] -> Int
lowest scores = foldr min 0 scores
check :: String -> Int -> Int -> String
check name got want
| got == want = "pass " ++ name
| otherwise =
"FAIL " ++ name ++ ": got " ++ show got ++ ", want " ++ show want
main :: IO ()
main = do
putStrLn (check "mixed" (lowest [1, -1, 2]) (-1))
putStrLn (check "a draw" (lowest [0, 3]) 0)
putStrLn (check "one game" (lowest [-2]) (-2))
What to look for
GHCi shows one location per stop, not a stack of calls, and a value shows as _ (unevaluated) until something needs it. It stops in lowest once for each test, where scores is visible, and in check. foldr min 0 scores is one expression, so there is no low to watch: the output is the evidence. After the edit, GHCi stops in lowest with scores = [2,1], then in check with want = 1 and got = 0. The output grows piece by piece, FAIL , then FAIL all wins, and so on: putStrLn writes each part as soon as it is computed. The last line is FAIL all wins: got 0, want 1.
Expected output
pass mixed
pass a draw
pass one game03 / Boundaries
Where do counterexamples usually hide?
Counterexamples are most often found at boundaries: inputs at the edge of what the code handles. Try these first: an empty input; a single element; equal values; the answer in the first place and in the last; zero; negative numbers, and no negative numbers at all. A loop that starts or stops one place too early, or a starting value such as 0, is wrong at a boundary and right everywhere else.
For each boundary, work out the expected value from the belief before you run it. The belief may say nothing about an input. This belief says nothing about an empty list. That is a finding too: decide what should happen, and add the decision to the belief.
The repair uses foldr1 min, which combines the scores with min and takes its starting value from the list itself. minimum from the Prelude does the same job and is the usual way to write it. Both need at least one score, and both stop the program on an empty list. The table in this section runs the repaired lowest on one input for each boundary.
lowest :: [Int] -> Int
lowest scores = foldr1 min scores Test the competing explanations
Predict first. These are recorded outcomes of the example, not tests running in your browser.
01One game: [3]
The belief gives 3.
Reveal the observation
Observed 3: pass.
What this rules in or out With one score, foldr1 min returns that score and never calls min.
02Equal scores: [1, 1]
The belief gives 1.
Reveal the observation
Observed 1: pass.
What this rules in or out A tie for the lowest score gives that score once.
03Lowest score last: [2, 0, -1]
The belief gives -1.
Reveal the observation
Observed -1: pass.
What this rules in or out foldr1 min combines every score, the last one included.
04Every game won: [2, 1]
The belief gives 1.
Reveal the observation
Observed 1: pass.
What this rules in or out The start value now comes from the list, not from 0.
05No games: an empty list
The belief covers lists with at least one score, so it predicts nothing.
Reveal the observation
Observed The program stops: Prelude.foldr1: empty list.
What this rules in or out Decide whether an empty list is a mistake in the calling code or an input to handle. The assertions section takes this up.
Try the edges first: empty, one, equal, first and last, zero, and negative.
Precisely: a list that cannot be empty
The type NonEmpty Int, from Data.List.NonEmpty, holds lists with at least one element. A lowest of type NonEmpty Int -> Int moves the precondition into the type: the compiler rejects a call with a list that might be empty, and the caller must decide what an empty season means.
04 / Tests that check nothing
Can a test pass without checking anything?
A test that cannot fail still prints pass. It tells you nothing. Three kinds are common.
The first kind has no comparison. It calls the function and prints the result. Nothing in it can fail.
The second kind gets its expected value from the code under test. It calls the same function again, or it repeats the same formula. So it compares the code with itself.
The third kind has an expected value copied from the output of one run. It checks that the output does not change. If that output was wrong, the test keeps the bug.
The rule from the first section prevents all three: the expected value comes from the belief, worked out without the code. To check a new test, break the code on purpose. For example, make the function return 0 at once, and run the test again. If the test still passes, it does not test that code.
The test below gets want from lowest itself. The experiment runs it on the counterexample from the earlier section.
testAllWins :: String
testAllWins = verdict ++ ": got " ++ show got ++ ", want " ++ show want
where
wins = [2, 1]
want = lowest wins
got = lowest wins
-- got == want, whatever lowest returns
verdict = if got == want then "pass" else "FAIL" A test must be able to fail: its expected value comes from the belief, not from the code.
Precisely: breaking the code on purpose
Mutation testing tools automate the check in this section. They make many small changes to the code, such as < to <=, run the tests after each, and report every change that no test noticed. For Haskell, these tools are rare; a common alternative is a QuickCheck property that compares lowest with minimum on many generated lists.
Experiment
Take the expected value from the function itself
want and got are both results of lowest for the same list. Whatever lowest returns, the two are equal, so the comparison is always true. This test passes with the wrong lowest, and it would pass with a correct one. It cannot tell them apart.
Why it matters Work out the expected value from the belief, by hand. Then a wrong function gives a different value, and the test can fail.
You know that lowest [2, 1] is wrong. Predict what this test prints. Then replace want = lowest wins with want = 1 and run again.
lowest :: [Int] -> Int
lowest scores = foldr min 0 scores
testAllWins :: String
testAllWins = verdict ++ ": got " ++ show got ++ ", want " ++ show want
where
wins = [2, 1]
want = lowest wins
got = lowest wins
verdict = if got == want then "pass" else "FAIL"
main :: IO ()
main = putStrLn testAllWins
What to look for
GHCi stops in testAllWins, then in verdict, got, lowest, wins, want, and lowest again: verdict needs got == want, and == needs got first. GHCi does not show the values of got and want here; the output pass: got 0, want 0 shows them. After the edit, GHCi stops in want without calling lowest for it, and the output is FAIL: got 0, want 1.
Expected output
pass: got 0, want 005 / Assertions
When should the program stop itself?
A test calls the code from outside, with inputs you chose. An assertion sits inside the code. It states that a condition is true at that point, every time the program reaches it. When the condition is false, the code itself is wrong, and the program should stop with a message.
Use assertions for programmer mistakes. A common one is a broken precondition: something that must be true when a function is called, such as “the list has at least one score”. The code that calls the function is responsible for it. Do not use assertions for problems that come from outside the program, such as a missing file, a score typed as text, or an empty list sent by another program. Those can happen when every line of the code is right. Handle them as errors and tell the user.
In Haskell, assert condition value, from Control.Exception, gives value when the condition is True, and throws an exception that stops the program when it is False. assert follows demand: the condition is checked only when something demands the result of assert. If nothing uses the result of lowest [], its condition is never checked. GHC removes every assert when it optimizes with -O, which most builds use, and when it is given the flag -fignore-asserts. For input from outside the program, return a Maybe or an Either and let the caller decide.
import Control.Exception (assert)
lowest :: [Int] -> Int
lowest scores = assert (not (null scores)) (foldr1 min scores) Assert what only a programmer mistake can break, and remember that -O removes the check. Return Maybe or Either for everything else.
Coming from Rust: Rust’s assert! stays in optimized builds; Haskell’s assert is removed under -O, so it behaves like Rust’s debug_assert!.
Precisely: what GHC does with assert
assert is an ordinary function in Control.Exception, but GHC rewrites calls to it while compiling. With assertions on, the call also records where it was made, which is why the message names Main.hs and a line. With -O or -fignore-asserts, the call becomes its second argument. GHCi, which the stepper uses, does not optimize, so asserts stay on unless a flag removes them.
Experiment
Switch the assert back on
{-# OPTIONS_GHC -fignore-asserts #-} is a pragma: a line that passes a flag to GHC for this file. With that flag, GHC replaces assert condition value with value. Without it, print demands the result of assert, so the condition is evaluated first, and False throws an exception that stops the program.
Why it matters GHC also drops every assert when it optimizes with -O, and most builds do. An assert checks the builds you test with, not the program you ship.
The first line tells GHC to ignore asserts. Predict whether assert stops the program for game 3, whose length is 0. Then delete that first line and run again.
{-# OPTIONS_GHC -fignore-asserts #-}
import Control.Exception (assert)
minutesLeft :: Int -> Int -> Int
minutesLeft played total =
assert (total > 0) (total - played)
-- each game's length in minutes; game 3's was left at 0 by mistake
totals :: [Int]
totals = [40, 60, 0]
main :: IO ()
main = print (minutesLeft 30 (totals !! 2))
What to look for
GHCi stops in main, then in minutesLeft with played = 30 and total = _ (unevaluated), and the output is -30. After the edit, GHCi stops in minutesLeft, now at line 5, and the run ends with *** Exception: Assertion failed, followed by assert, called at Main.hs:5:3. Nothing is printed.
Expected output
-3006 / Try it
Can you find the counterexample?
Predict the output, then look for a counterexample before you reveal the answer. A team’s score sheet shows the top scorer of a game, by position in the team list. The belief is the comment at the top of the program. zip points [0 ..] pairs each score with its position, maximum picks the largest pair, and snd takes the position out of the pair.
-- topScorer should return the position of the highest score;
-- on a tie, the earlier player's position.
topScorer :: [Int] -> Int
topScorer points = snd (maximum (zip points [0 ..]))
check :: String -> Int -> Int -> String
check name got want
| got == want = "pass " ++ name
| otherwise =
"FAIL " ++ name ++ ": got " ++ show got ++ ", want " ++ show want
main :: IO ()
main = do
putStrLn (check "second" (topScorer [8, 12, 5]) 1)
putStrLn (check "first" (topScorer [20, 3]) 0)
putStrLn (check "alone" (topScorer [4]) 0)
What does this print, which input is a counterexample to the belief, and which boundary does it sit on?
Reveal the reasoning
It prints pass second, pass first, and pass alone. The belief is still false: topScorer [9, 9] returns 1 where the belief gives 0. The counterexample sits on the equal-values boundary: pairs are compared by score first and then by position, so of (9, 0) and (9, 1) maximum picks (9, 1), the later player. A tie below the top, such as [3, 3, 8], would not fail, because 8 wins alone. One repair counts the players before the first one with the top score: length (takeWhile (/= maximum points) points), where (/= m) tests “not equal to m”. The tie then stays as a regression test.
One more boundary: topScorer [] stops the program, because maximum needs at least one pair. The belief needs a decision for an empty team too.
Practice tickets
- TEST-101 · Long games never finish on the score screen → Free
- TEST-102 · A relay team with one swimmer entered twice was accepted → Sign in · Pro
- TEST-103 · A screen that shows the wrong lift wait passes the checker → Sign in · Pro