पाठ 19 / 25
Testing with Hspec and QuickCheck
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.
Hspec examples and QuickCheck properties
Testing a pricing function and a round-trip property.
-- 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) == fieldsProperties 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.
त्वरित जाँच: What does QuickCheck do when it finds a failing input?
- 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.