# Testing with Hspec and QuickCheck — Haskell

Source: https://www.geekswithgeeks.com/en/haskell/w-testing

> Write example-based tests and property-based tests.

## Types plus tests

Types rule out many bugs, but not wrong logic, so Haskell projects still test thoroughly. **Hspec** provides readable example-based tests (`describe`, `it`, `shouldBe`, `shouldSatisfy`, `shouldThrow`) and **tasty** is an alternative test runner that combines providers. **QuickCheck**, which originated in Haskell, pioneered **property-based testing**: you state properties that should hold for all inputs, such as "reversing a list twice gives the original", and QuickCheck generates hundreds of random inputs and **shrinks** any failure to a minimal counterexample. Custom types get generators by implementing **`Arbitrary`**, and modifiers like `NonNegative`, `Positive` and `NonEmptyList` constrain inputs. **Hedgehog** is a newer alternative with integrated shrinking. Pure functions make testing simple: no mocks, no setup, just inputs and outputs. **Doctests** check examples in Haddock comments. Configure test suites in the `.cabal` file (`test-suite spec`) and run them with `cabal test`. Combined with `-Wall`, a type checker and **HLint**, this catches most mistakes before code runs in production.

## Property-based testing

Random inputs test a property; failures shrink to the smallest counterexample.

![A dice icon feeding many small input cards into a function box with a checkmark, and one failing card being shrunk step by step to a tiny card.](assets/figures/haskell/section-7-map.svg) — Figure 7.1 — Generate, check and shrink.

## Hspec examples and QuickCheck properties

Testing a pricing function and a round-trip property.

```haskell
-- test/Spec.hs
import Test.Hspec
import Test.QuickCheck

withGstPaise :: Int -> Int
withGstPaise p = p + (p * 18 + 50) `div` 100

discount :: Int -> Int -> Int
discount percent p = p * (100 - percent) `div` 100

encodeCsv :: [String] -> String
encodeCsv = foldr1 (\a b -> a ++ "," ++ b)

decodeCsv :: String -> [String]
decodeCsv s = case break (== ',') s of
  (field, [])       -> [field]
  (field, _ : rest) -> field : decodeCsv rest

main :: IO ()
main = hspec $ do
  describe "withGstPaise" $ do
    it "adds 18% GST" $
      withGstPaise 10000 `shouldBe` 11800
    it "never decreases a price" $ property $
      \(NonNegative p) -> withGstPaise p >= p

  describe "discount" $
    it "never increases a price" $ property $
      \(NonNegative p) -> forAll (choose (0, 100)) $ \pct -> discount pct p <= p

  describe "CSV round trip" $
    it "decode . encode is identity for comma-free fields" $ property $
      forAll (listOf1 (listOf (elements ['a' .. 'z']))) $ \fields ->
        decodeCsv (encodeCsv fields) == fields
```

## Properties find edge cases you would not write

QuickCheck quickly tries empty strings, zero, negative numbers and huge values. If the CSV generator above allowed commas, it would immediately report a minimal failing example, revealing that the format needs escaping.

**Quiz:** What does QuickCheck do when it finds a failing input?

- [x] Shrinks it to a smaller counterexample that still fails
- [ ] Stops without information
- [ ] Ignores it
- [ ] Retries until it passes

*Answer:* Shrinks it to a smaller counterexample that still fails. Shrinking makes failures much easier to understand.
