Skip to main content

Haskell Feature Flag Demo (Part 4)

This entry of the Haskell Feature Flag Demo series presents tests for the demo using simple types as well as the demo using tagged feature flag configuration.

Spec

The Spec module defines the main function for the test, using the tasty test framework. It is straightforward except that it sets the TASTY_NUM_THREADS environment variable to ensure that the tests are executed sequentially. This is essential because the tested code uses global storage. Tests use different feature flag configuration, and tests running in parallel would conflict.

main :: IO ()
main = do
    setEnv "TASTY_NUM_THREADS" "1"  -- run only one test at a time!
    defaultMain $ testGroup "test"
      [ Demo1.IO.Test.tests
      , Demo2.IO.Test.tests
      ]

Demo1.IO.Test

This module implements tests for the foo and bar pure functions as well as the run function of the demo using simple types.

Function foo is tested using unit tests, passing different feature flag configuration for each test.

testFoo :: TestTree
testFoo = testGroup "foo"
    [ testCase "-ffSomeBug" $ 13 @=? Demo1.IO.foo False
    , testCase "+ffSomeBug" $ 42 @=? Demo1.IO.foo True
    ]

Function bar is tested using property testing. The property asserts that the refactored implementation is equivalent to the original implementation.

testBar :: TestTree
testBar = testProperty "bar" prop_equiv
  where
    prop_equiv :: Int -> Bool
    prop_equiv n = Demo1.IO.bar False n == Demo1.IO.bar True n

Function run is tested using unit tests, passing different combinations of feature flag configurations for each test.

testRun :: TestTree
testRun = testGroup "run"
    [ testCase "-ffSomeBug -ffSomeBusinessLogic" $
        (26 @=?) =<< Demo1.IO.run (ffMap False False)
    , testCase "-ffSomeBug +ffSomeBusinessLogic" $
        (26 @=?) =<< Demo1.IO.run (ffMap False True)
    , testCase "+ffSomeBug -ffSomeBusinessLogic" $
        (84 @=?) =<< Demo1.IO.run (ffMap True False)
    , testCase "+ffSomeBug +ffSomeBusinessLogic" $
        (84 @=?) =<< Demo1.IO.run (ffMap True True)
    ]
  where
    ffMap :: Bool -> Bool -> FF.FeatureFlagMap
    ffMap ffSomeBug ffSomeBusinessLogic = Map.fromList
      [ (FF.Fix_202407_SomeBug_42, ffSomeBug)
      , (FF.Ref_202407_SomeBusinessLogic, ffSomeBusinessLogic)
      ]

Demo2.IO.Test

This module implements tests for the foo and bar pure functions as well as the run function of the demo using tagged feature flag configuration.

Function foo is tested using unit tests, passing different feature flag configuration for each test. Helper function ffSomeBug constructs a feature flag configuration of the correct type. The default case is tested as well.

testFoo :: TestTree
testFoo = testGroup "foo"
    [ testCase "-ffSomeBug" $ 13 @=? Demo2.IO.foo (ffSomeBug (Just False))
    , testCase "+ffSomeBug" $ 42 @=? Demo2.IO.foo (ffSomeBug (Just True))
    , testCase "!ffSomeBug" $ 42 @=? Demo2.IO.foo (ffSomeBug Nothing)
    ]
  where
    ffSomeBug :: Maybe Bool -> FF.FeatureFlagConfig FF.Fix_202407_SomeBug_42
    ffSomeBug = FF.FeatureFlagConfig

Function bar is tested using property testing. The property asserts that the refactored implementation is equivalent to the original implementation. Note that the ffSomeBusinessLogic helper function hard-codes the Just constructor because the case of no configuration is irrelevant to this property.

testBar :: TestTree
testBar = testProperty "bar" prop_equiv
  where
    prop_equiv :: Int -> Bool
    prop_equiv n =
      Demo2.IO.bar (ffSomeBusinessLogic False) n
        == Demo2.IO.bar (ffSomeBusinessLogic True) n

    ffSomeBusinessLogic
      :: Bool
      -> FF.FeatureFlagConfig FF.Ref_202407_SomeBusinessLogic
    ffSomeBusinessLogic = FF.FeatureFlagConfig . Just

Function run is tested using unit tests, passing different combinations of feature flag configurations for each test.

testRun :: TestTree
testRun = testGroup "run"
    [ testCase "-ffSomeBug -ffSomeBusinessLogic" $
        (26 @=?) =<< Demo2.IO.run (ffMap False False)
    , testCase "-ffSomeBug +ffSomeBusinessLogic" $
        (26 @=?) =<< Demo2.IO.run (ffMap False True)
    , testCase "+ffSomeBug -ffSomeBusinessLogic" $
        (84 @=?) =<< Demo2.IO.run (ffMap True False)
    , testCase "+ffSomeBug +ffSomeBusinessLogic" $
        (84 @=?) =<< Demo2.IO.run (ffMap True True)
    ]
  where
    ffMap :: Bool -> Bool -> FF.FeatureFlagMap
    ffMap ffSomeBug ffSomeBusinessLogic = DMap.fromList
      [ FF.Fix_202407_SomeBug_42 ==> ffSomeBug
      , FF.Ref_202407_SomeBusinessLogic ==> ffSomeBusinessLogic
      ]

Code

The full source is available on GitHub.

These tests uses GHC 9.6.5 and can be run using the following command.

$ cabal test

Alternatively, if you use Stack, run the demo using the following command.

$ stack test