02-Test-Syntax.purs

module Test.QuickCheckSyntax where

import Prelude

import Effect (Effect)
import Effect.Console (log)

-- needed to run tests
import Effect.Exception (catchException, message)

-- new imports
-- these will be explained below
import Test.QuickCheck (
      quickCheck, quickCheck'
    , (<?>)
    , (==?), (===), (/=?), (/==)
    , (<?), (<=?), (>?), (>=?)

    -- needed to reduce noise in syntax
    , class Testable
    )

-- wraps the test output with the name of the test
runTest :: String -> Effect Unit -> Effect Unit
runTest testName test = do
  log $ "=== Running test:  " <> testName
  test
  log $ "=== Test finished: " <> testName <> "\n\n"

-- runs our tests
main :: Effect Unit
main = do
  runTest "Pass - exhaustive"       passingTest_exhaustive
  runTest "Pass - normal threshold" passingTest_normal_threshold
  runTest "Pass - custom threshold" passingTest_custom_threshold

  runTest "Fail - no error message"       error_message_None
  runTest "Fail - standard error message" error_message_Standard
  runTest "Fail - custom error message"   error_message_Custom

{-
QuickCheck cannot always infer which type we're using for our
randomly-generated data.

  -- What is the type of `a` ? Answer: any instance of Eq
  quickCheck (\a -> a == a)

To fix this, we could use one of two things:                        -}
-- 1. (argName :: Type) syntax
noisySyntax1 :: Effect Unit
noisySyntax1 = quickCheck (\(i :: Int) -> true)

noisySyntax2 :: Effect Unit
noisySyntax2 = quickCheck (\i -> (\_ -> true) (i :: Int))

-- 2. Use a function that specifies its type
intOnlyFunction :: Int -> Boolean
intOnlyFunction _ = true

functionThatSpecifiesType :: Effect Unit
functionThatSpecifiesType = quickCheck (\i -> intOnlyFunction i)
{-
Each adds "noise" to the syntax.

So, the below functions specify what the type is. When you read
these functions in actual tests, remove the "_type" suffix:
  - quickCheck_int        =>  quickCheck
  - quickCheck'_int       =>  quickCheck'

The functions appear at the bottom of the file.
-}

-- test that "b && true" reduces to "b"
-- This test will not stop after 2 tries, even though that's all it needs.
passingTest_exhaustive :: Effect Unit
passingTest_exhaustive = quickCheck_boolean (\b -> (b && true) == b)

-- Test that "+" works regardless of argument order (i.e. commutative)
passingTest_normal_threshold :: Effect Unit
passingTest_normal_threshold = quickCheck_int       (\i -> i + 1 == 1 + i)

-- increase confidence by increasing the number of tests
passingTest_custom_threshold :: Effect Unit
passingTest_custom_threshold = quickCheck'_int 1000 (\i -> i + 1 == 1 + i)

-- A test that fails will crash our program, preventing us from
-- seeing the output of other failing tests. We'll fix this by
-- preceding such tests with `printErrorMessage`. The function appears
-- at the bottom of the file.

error_message_None :: Effect Unit
error_message_None =
  printErrorMessage $ quickCheck_int (\i -> i + 1 == i)

{-
Quickcheck uses "[operator]?" syntax to add a standard error message

Normal without message | ==  | /=  | <  | <=  | >  | >=
Normal with    message | ==? | /=? | <? | <=? | >? | >=?
Alternative            | === | /== |
-}
error_message_Standard :: Effect Unit
error_message_Standard = do
  printErrorMessage $ quickCheck_int (\i -> i + 1 ==? i)
  log "" -- add a blank line in-between tests
  printErrorMessage $ quickCheck_int (\i -> i + 1 === i)
  log ""
  printErrorMessage $ quickCheck_int (\i -> i     /=? i)
  log ""
  printErrorMessage $ quickCheck_int (\i -> i     /== i)
  log ""
  printErrorMessage $ quickCheck_int (\i -> i + 1 <=? i)
  log ""
  printErrorMessage $ quickCheck_int (\i -> i + 1  <? i)
  log ""
  printErrorMessage $ quickCheck_int (\i -> i - 1  >? i)
  log ""
  printErrorMessage $ quickCheck_int (\i -> i - 1 >=? i)

error_message_Custom :: Effect Unit
error_message_Custom =
  printErrorMessage $ quickCheck_int
    (\i -> i + 1 == i <?> show i <> " did not equal " <> (show $ i + 1))

-- Helper functions

printErrorMessage :: Effect Unit -> Effect Unit
printErrorMessage test = catchException (\error -> log $ message error) test

data Result_
  = Success_
  | Failure_ String -- error message
-- "a" is
--    either a Boolean, which is converted into a Result
--    or     a Result itself.

quickCheck_boolean :: forall a. Testable a => (Boolean -> a) -> Effect Unit
quickCheck_boolean test = quickCheck test

quickCheck_int ::  forall a. Testable a => (Int -> a) -> Effect Unit
quickCheck_int test = quickCheck test

-- variant that allows one to specify the number of tests
quickCheck'_int :: forall a. Testable a => Int -> (Int -> a) -> Effect Unit
quickCheck'_int numOfTests test = quickCheck' numOfTests test