This is an old revision of the document!
Table of Contents
Testing conventions
Setup with stack
package.yaml
- package.yaml:
... tests: Test4-test: main: Spec.hs source-dirs: test ghc-options: - -threaded - -rtsopts - -with-rtsopts=-N dependencies: - Test4 - tasty - tasty-quickcheck - tasty-hunit
Test preparation
Folder: 'project/test'
Root: Spec.hs
In folder test is a haskell file as root for all tests name spec.hs.
spec.hs:import qualified Test.Tasty as T import qualified SpecModuleA -- test module for module ModuleA import qualified SpecModuleB -- test module for module ModuleB import qualified SpecModuleC -- test module for module ModuleC main :: IO () main = T.defaultMain tests tests :: T.TestTree tests = T.testGroup "Tests" [ SpecModuleA.testGroup, SpecModuleB.testGroup, SpecModuleC.testGroup ]
Test module: Spec<Module>.hs
Beginning
- example
spec<Module>.hs:module SpecCode ( testGroup ) where import qualified Test.Tasty as T import qualified Test.Tasty.QuickCheck as QC import qualified Test.Tasty.HUnit as HU import qualified ModuleA as A -- ...
Test tree by test group
- example
spec<Module>.hs:-- ... testGroup :: T.TestTree testGroup = T.testGroup "module A" [ tgClassA, tgUnitf1, tgUnitf2, tgUnitf3 ] -- ... tgClassA :: T.TestTree tgClassA = T.testGroup "class A" [ -- laws: tgLawA, tgLawB, tgLawC, -- unit tests: tgUnitf3, tgUnitlcdConvert ] -- ...
HUnit test
- example
spec<Module>.hs:-- ... tgLawA :: T.TestTree tgLawA = T.testGroup "Law: A" [ -- ..., HU.testCase "#1" (A.f4 3 HU.@?= (A.f5 3 * A.f5 3)), HU.testCase "#2" (A.f4 4 HU.@?= (A.f5 4 * A.f5 4)) -- ... ] -- ...
QuickCheck test
- example
spec<Module>.hs:-- ... tgLawA :: T.TestTree tgLawA = T.testGroup "Law: A" [ -- ..., QC.testProperty "#1" (\nj -> A.f3 nj QC.==> (A.f4 nj == (A.f5 nj * A.f5 nj)) ), QC.testProperty "#1" (\nj -> not A.f3 nj QC.==> (A.f4 nj == -1 ) -- ... ] -- ...
Test types in more detail
QuickCheck
Control of test samples
QuickCheck tests with random samples, and the frequency distribution of the ramdom samples can be controlled by the class Arbitrary.
- Example: Let suppose there is a type that is to huge to test all possible combinations.
- type declaration of example:
data TooHugeToTestAll = ToHuge { ra :: Int, rb :: Int, rc :: Int, rd :: Int } deriving (Show)
- And you want to have more tests in a certain range of values plus some in the vast possibilities.
- Then you create an orphan instance of Arbitrary.
- class instance of example:
instance QC.Arbitrary TooHugeToTestAll where arbitrary = ToHuge <$> f <*> f <*> f <*> f where f = QC.frequency [ (1, QC.choose (niMin, -1)), -- -1 or less (5, QC.choose (0, niMax)), -- 0 or more (3, QC.choose (niMaxPlus1, 1000)), -- up to 1000 (1, QC.choose (1001, maxBound :: Int)) -- up to maxBound ] niMax :: Int niMax = 3 niMaxPlus1 :: Int niMaxPlus1 = niMax + 1 niMin :: Int niMin = - niMax
Control of test samples for known types
Background: The following instances already exist with a standard distribution for sample data.
- Arbitrary Bool
- Arbitrary Char
- Arbitrary Double
- Arbitrary Float
- Arbitrary Int
- Arbitrary Int8
- Arbitrary Int16
- Arbitrary Int32
- Arbitrary Int64
- Arbitrary Integer
- Arbitrary Ordering
- Arbitrary Word
- Arbitrary Word8
- Arbitrary Word16
- Arbitrary Word32
- Arbitrary Word64
- Arbitrary ()
- and much more … see all of them here Test.Tasty - class Arbitrary
So how to control them differently.
Solution: By declaration of a new type (by 'newtype').
- example:
newtype Char' = Char' Char deriving Show fromChar' :: Char' -> Char fromChar' (Char' ch) = ch instance QC.Arbitrary Char' where arbitrary = do genChar <- QC.frequency [ (1, QC.elements ['A'..'Z']), (1, QC.elements ['a'..'z']), (1, QC.elements ['0'..'9']), (5, QC.chooseAny ) ] return (Char' genChar) -- ... QC.testProperty "Char" (\ch' -> (fromChar' (ch' :: Char') == '\0') QC.==> (fXYZ (fromChar' ch')) ),
- example, with a more generalised approach:
{-# LANGUAGE FlexibleInstances #-} -- ... newtype T' a = T' a deriving Show fromT' :: T' a -> a fromT' (T' x) = x instance QC.Arbitrary (T' Char) where arbitrary = do genChar <- QC.frequency [ (1, QC.elements ['A'..'Z']), (1, QC.elements ['a'..'z']), (1, QC.elements ['0'..'9']), (5, QC.chooseAny ) ] return (T' genChar) -- ... QC.testProperty "Char #1" (\cd' -> (fromT' (cd' :: (TH.T' Char)) == '\0') QC.==> (fXYZ (fromT' cd') == '\0') ),
Validation
The following symbols are possible for each validation criteria:
- ❓
- not yet clear
- ❌
- not yet
- N/A
- not applicable
- ✅
- criteria is met
- ✅ with comment
- criteria is met with comment and mitigation
- example:
NOTE: correct functioning depends on "Cd.cdFromChar :: Char -> CharUtf8" test mitigation: ✅ supplemental test data (approx. 100 characters) are use in unit tests
Functions and laws
Each function has its own test group designated as tgUnit<functionName>.
And each law has its own test group designated as tgLaw<LawName>.
Tests are validated by checking the following criteria:
- completeness:
- the complete set of possible test values/data is used in unit tests,
- or values for all possible equivalence classes are used
- or ramdom sample values/data from complete set of possible values/data is used in case of high-volume cases (QuickCheck)
- independence:
- test method is independent from library functions that are under test,
- or dependent on tested functions
- boundaries:
- corner cases and boundaries are tested by unit tests (HUnit)
- conform doc.:
- all tests are conform to documentation, e.g. source code comments by haddock)
After checking the abovementioned criteria, the result is documented as source code comment:
{- f1 * validated: ✅ * completeness: ✅ * independence: ✅ * boundaries : ✅ * conform doc.: ✅ -} tgUnitf1 :: T.TestTree tgUnitf1 = T.testGroup "f1" [ -- ... ]
Classes
Each function has its own test group designated as tgClass<ClassName>.
Tests are validated by checking the following criteria:
- documented:
- laws:
- laws and its criteria are described and are verifyable
- characteristics:
- the general characteristics of the class are described
- prefix
- the prefix of the class is given
- short description
- a short description is given
- completeness:
- the class is tested to fulfill all laws
- the class is tested for all types and type combinations, respectively
- and the class is tested for all types and type combinations
- stubbed:
- if any, the default functions of the class are also tested by stub instance(s), as well
After checking the abovementioned criteria, the result is documented as source code comment:
{- class A * validated: ✅ * documented: ✅ * laws : ✅ * characteristics : ✅ * prefix : ✅ * short description: ✅ * completeness: ✅ * all laws documented : ✅ * all type combinations : ✅ * stubbed for default functions: ✅ -} tgClassA :: T.TestTree tgClassA = T.testGroup "class A" [ -- laws: tgLawA, tgLawB, tgLawC, -- unit tests: tgUnitf3, tgUnitlcdConvert ]
Modules
Each module exports a test group designated as testGroup.
Tests are validated by checking the following criteria:
- documented:
- there is completely and correct filled header like:
Description : provides a class for ... Copyright : (c) <Author>, <Year> License : <Licese> Maintainer : <E-Mail-Address> Stability : experimental Portability : POSIX
- It follows a general description of the purpose of the module.
- completeness:
- all classes are in the root test group
- all global functions have tests in the root test group
- all classes are validated
- all global functions are validated
- example:
{- module A * validated: ✅ * documented: ✅ * completeness: ✅ -} testGroup :: T.TestTree testGroup = T.testGroup "module A" [ tgClassA, tgUnitf1, tgUnitf2, tgUnitf3 ]
