Testing validates that software meets functional and nonfunctional requirements. Its purpose is not simply to search for bugs: it fixes contracts and expectations in executable form, documents behavior, prevents regressions, and reduces project risk. Testability is therefore an architectural concern, not a task postponed until implementation is complete. section 15.0 section 15.3
Write tests early enough to act as design validators. A test can expose a missing interface, an unclear communication type, or components that cannot be assembled before their real implementations exist. Shape the system around the required form, use dummy content at first, and replace fakes only when real behavior is needed. This is a relaxed use of test-driven development: tests should guide design without becoming a rigid procedure. section 4.1.4
Choose a testing approach by the risk and behavior to validate:
- Use black-box testing through public interfaces when internal implementation details should not matter. section 15.1
- Use grey-box or white-box testing when knowledge of implementation is needed to cover subtle cases. section 15.1
- Use functional testing for isolated functions, modules, subsystems, or requirements without calling real external services. section 15.1
- Use integration testing to validate functional requirements across integrated application parts. section 15.1
A test case should be small, independent, and responsible for one action. Arrange the data and environment, act by performing the behavior under test, then assert the result. Shared mutable state can make cases dependent on execution order; parallel execution then creates race conditions and unreliable results. Pure, deterministic functions are especially convenient to test, though determinism does not make every real-world failure observable in a test environment. section 15.1.1 section 15.1
Testing a pure function
A square-function specification groups cases for positive, negative, and zero inputs, then asserts each expected result. The zero case demonstrates why a test set should include an edge case rather than only ordinary inputs. section 15.1.1
Listing 15.1 Simple tests with hspec
square :: Integer -> Integer #1
square x = x * x
spec :: Spec
spec = do
describe "square algorithm tests" $ do #2
it "squaring positive integer test" $ do
square 2 `shouldBe` 4
square 5 `shouldBe` 25
it "squaring negative integer test" $ do
square (-2) `shouldBe` 4
square (-5) `shouldBe` 25
it "squaring zero test" $
square 0 `shouldBe` 0
Inspect a compact hspec specification that organizes examples under a description and checks ordinary and edge-case outputs. section 15.1.1
Good architecture makes meaningful testing possible. Applications that directly depend on many external services and side effects are difficult to control in tests. Decoupling interfaces from implementations, keeping coupling low, and using mocks where necessary improve coverage for IO-bound applications. Property-based testing fits CPU-bound, pure code better than IO-bound code; no single testing style covers every requirement or risk. section 15.2 section 15.3




















































