mockcat-1.0.0.0: src/Test/MockCat/Mock.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
{-# OPTIONS_GHC -Wno-name-shadowing #-}
{-# OPTIONS_GHC -Wno-redundant-constraints #-}
{- | Utilities for constructing verifiable stub functions.
This module provides the core functions for creating mocks and stubs.
= Key Functions
* 'mock': Create a verifiable mock function (records calls).
* 'stub': Create a pure stub function (no recording).
* 'mockM': Create a monadic mock function (allows explicit side effects).
-}
module Test.MockCat.Mock
( MockBuilder
, buildMock
, mock
, mockM
, createNamedMockFnWithParams
, stub
, shouldBeCalled
, times
, atLeast
, atMost
, greaterThan
, lessThan
, once
, never
, inOrder
, inPartialOrder
, inOrderWith
, inPartialOrderWith
, calledWith
, anything
, withArgs
, onCase
, cases
, casesIO
, label
, Label
) where
import Control.Monad.IO.Class (MonadIO(..))
import Control.Monad.State (get, put)
import Data.Kind (Type)
import Data.Proxy (Proxy(..))
import Data.Typeable (Typeable)
import Prelude hiding (lookup)
import Test.MockCat.Internal.Builder
import qualified Test.MockCat.Internal.MockRegistry as MockRegistry ( register )
import Test.MockCat.Internal.Types
import Test.MockCat.Param
import Test.MockCat.Verify
import Test.MockCat.Cons (Head(..), (:>)(..))
-- | Type family to convert raw values to mock parameters.
-- Raw values (like "foo") are converted to Head :> Param a,
-- while existing Param chains remain unchanged.
type family ToMockParams p where
ToMockParams (Param a :> rest) = Param a :> rest -- Already a Param chain, keep as is
ToMockParams (Param a) = Param a -- Single Param, keep as is
ToMockParams (Cases a b) = Cases a b -- Cases, keep as is
ToMockParams (IO a) = IO a -- IO, keep as is
ToMockParams (Head :> a) = Head :> a -- Already has Head, keep as is
ToMockParams a = Head :> Param a -- Raw value, wrap with Head :> Param
-- | Type class for converting values to mock parameters.
class CreateMock p where
toParams :: p -> ToMockParams p
-- Instance for Param chains (most specific - should match first)
instance {-# OVERLAPPING #-} CreateMock (Param a :> rest) where
toParams = id
-- Instance for single Param
instance {-# OVERLAPPING #-} CreateMock (Param a) where
toParams = id
-- Instance for Cases
instance {-# OVERLAPPING #-} CreateMock (Cases a b) where
toParams = id
-- Instance for IO
instance {-# OVERLAPPING #-} CreateMock (IO a) where
toParams = id
-- Instance for Head :> Param r (constant values)
instance {-# OVERLAPPING #-} CreateMock (Head :> Param r) where
toParams = id
-- Instance for raw values (fallback)
-- This handles raw values by wrapping them with Head :> Param
-- We need to ensure this doesn't match Param chains, Cases, IO, or Head types
instance {-# OVERLAPPABLE #-}
( ToMockParams b ~ (Head :> Param b)
, Normalize b ~ Param b
, Typeable b
, ToParamArg b
) => CreateMock b where
toParams value = Head :> toParamArg value
-- | Label type for naming mock functions.
newtype Label = Label MockName
-- | Label function for naming mock functions.
-- Use it with 'mock' to provide a name for the mock function.
--
-- @
-- f <- mock (label "mockName") $ "a" ~> "b"
-- @
label :: MockName -> Label
label = Label
-- | Type class for creating mock functions with optional name.
class CreateMockFn a where
mockImpl :: a
-- | Type class for creating stub functions with optional name.
class CreateStubFn a where
stubImpl :: a
-- | Create a mock function with verification hooks attached (unnamed version).
-- The returned function mimics a pure function (via 'unsafePerformIO') but records its calls for later verification.
--
-- > f <- mock $ "a" ~> "b"
-- > f "a" `shouldBe` "b"
-- > f `shouldBeCalled` "a"
instance
( MonadIO m
, CreateMock p
, MockBuilder (ToMockParams p) fn verifyParams
, Typeable verifyParams
, Typeable fn
) =>
CreateMockFn (p -> m fn)
where
mockImpl p = do
let params = toParams p
BuiltMock { builtMockFn = fn, builtMockRecorder = recorder } <- buildMock Nothing params
liftIO $ MockRegistry.register Nothing recorder fn
-- | Create a named mock function.
-- The name is used in error messages to help you identify which mock failed.
--
-- > f <- mock (label "MyAPI") $ "a" ~> "b"
instance {-# OVERLAPPING #-}
( MonadIO m
, CreateMock p
, MockBuilder (ToMockParams p) fn verifyParams
, Typeable verifyParams
, Typeable fn
) =>
CreateMockFn (Label -> p -> m fn)
where
mockImpl (Label name) p = do
let params = toParams p
BuiltMock { builtMockFn = fn, builtMockRecorder = recorder } <- buildMock (Just name) params
liftIO $ MockRegistry.register (Just name) recorder fn
-- | Create a mock function with verification hooks attached.
--
-- This function can be used in two ways:
--
-- 1. Without a name:
-- @
-- f <- mock $ "a" ~> "b"
-- @
--
-- 2. With a name (using 'label'):
-- @
-- f <- mock (label "mockName") $ "a" ~> "b"
-- @
--
-- The function creates a verifiable stub that records calls
-- and can be verified via the unified 'shouldBeCalled' API.
-- The function internally uses 'unsafePerformIO' to make the returned function
-- appear pure, but it requires 'MonadIO' for creation.
mock :: CreateMockFn a => a
mock = mockImpl
-- | Type class for creating monadic mock functions without unsafePerformIO.
class CreateMockFnM a where
mockMImpl :: a
instance
( MonadIO m
, CreateMock p
, MockIOBuilder (ToMockParams p) fn verifyParams
, LiftFunTo fn fnM m
, Typeable verifyParams
, Typeable fnM
) =>
CreateMockFnM (p -> m fnM)
where
mockMImpl p = do
let params = toParams p
BuiltMock { builtMockFn = fnIO, builtMockRecorder = verifier } <- buildIO Nothing params
let lifted = liftFunTo (Proxy :: Proxy m) fnIO
liftIO $ MockRegistry.register Nothing verifier lifted
instance {-# OVERLAPPING #-}
( MonadIO m
, CreateMock p
, MockIOBuilder (ToMockParams p) fn verifyParams
, LiftFunTo fn fnM m
, Typeable verifyParams
, Typeable fnM
) =>
CreateMockFnM (Label -> p -> m fnM)
where
mockMImpl (Label name) p = do
let params = toParams p
BuiltMock { builtMockFn = fnIO, builtMockRecorder = verifier } <- buildIO (Just name) params
let lifted = liftFunTo (Proxy :: Proxy m) fnIO
liftIO $ MockRegistry.register (Just name) verifier lifted
mockM :: CreateMockFnM a => a
mockM = mockMImpl
-- | Internal function for TH code that already has MockBuilder constraint.
-- This avoids CreateNamedMock instance resolution issues in generated code.
createNamedMockFnWithParams ::
( MonadIO m
, MockBuilder params fn verifyParams
, Typeable verifyParams
, Typeable fn
) =>
MockName ->
params ->
m fn
createNamedMockFnWithParams name params = do
BuiltMock { builtMockFn = fn, builtMockRecorder = recorder } <- buildMock (Just name) params
liftIO $ MockRegistry.register (Just name) recorder fn
-- | Create a pure stub function without verification hooks.
-- Useful when you only need to return values and don't care about verification.
-- This is completely pure and safe.
--
-- > let f = stub $ "a" ~> "b"
instance StubBuilder params fn => CreateStubFn (params -> fn) where
stubImpl = buildStub Nothing
-- | Create a named pure stub function without verification hooks (named version).
--
-- The provided name is used in failure messages.
-- This function creates a simple stub that returns values based on the provided
-- parameters, but does not support verification. Use 'mock' if you need
-- verification capabilities.
--
-- @
-- let f = stub (label "stubName") $ "a" ~> "b"
-- @
instance {-# OVERLAPPING #-} StubBuilder params fn => CreateStubFn (Label -> params -> fn) where
stubImpl (Label name) = buildStub (Just name)
{- | Create a pure stub function without verification hooks.
This function can be used in two ways:
1. Without a name:
@
let f = stub $ "a" ~> "b"
@
2. With a name (using 'label'):
@
let f = stub (label "stubName") $ "a" ~> "b"
@
This function creates a simple stub that returns values based on the provided
parameters, but does not support verification. Use 'mock' if you need
verification capabilities.
-}
stub :: CreateStubFn a => a
stub = stubImpl
{- | Register a stub case within a 'Cases' builder. -}
onCase :: a -> Cases a ()
onCase a = Cases $ do
st <- get
put (st ++ [a])
{- | Define stub cases from a list of patterns. -}
cases :: [a] -> Cases a ()
cases a = Cases $ put a
{- | IO variant of 'cases'. -}
casesIO :: [a] -> Cases (IO a) ()
casesIO = Cases . (put . map pure)
class LiftFunTo funIO funM (m :: Type -> Type) | funIO m -> funM where
liftFunTo :: Proxy m -> funIO -> funM
instance MonadIO m => LiftFunTo (IO r) (m r) m where
liftFunTo _ = liftIO
instance LiftFunTo restIO restM m => LiftFunTo (a -> restIO) (a -> restM) m where
liftFunTo proxy f a = liftFunTo proxy (f a)