packages feed

mockazo (empty) → 0.1.0

raw patch · 13 files changed

+878/−0 lines, 13 filesdep +basedep +constraintsdep +hspecsetup-changed

Dependencies added: base, constraints, hspec, mockazo, multistate, relude, template-haskell

Files

+ LICENSE.md view
@@ -0,0 +1,25 @@+The MIT License (MIT)+=====================++Copyright © `2019` `The Agile Monkeys`++Permission is hereby granted, free of charge, to any person+obtaining a copy of this software and associated documentation+files (the “Software”), to deal in the Software without+restriction, including without limitation the rights to use,+copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the+Software is furnished to do so, subject to the following+conditions:++The above copyright notice and this permission notice shall be+included in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR+OTHER DEALINGS IN THE SOFTWARE.
+ README.md view
@@ -0,0 +1,205 @@+# Mockazo 👃+_Mock your records of functions with ease_++[![CircleCI](https://circleci.com/gh/theam/mockazo.svg?style=svg&circle-token=e80d08cb0d9855a774709311335f4e29ca40f5de)](https://circleci.com/gh/theam/mockazo)+[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)+[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/theam/mockazo.svg)](http://isitmaintained.com/project/theam/mockazo "Average time to resolve an issue")+[![GitHub license](https://img.shields.io/github/license/theam/mockazo.svg)](https://github.com/theam/mockazo/blob/master/LICENSE)+[![Hackage version](https://img.shields.io/hackage/v/mockazo.svg)](https://hackage.haskell.org/package/mockazo)+[![Open Source Love png1](https://badges.frapsoft.com/os/v1/open-source.png?v=103)](https://github.com/ellerbrock/open-source-badges/)++One approach to structure a Haskell is using records of functions, sometimes called+handles, services, or as we like to call them, components.++Mockazo provides a way of mocking components with ease and to verify that they executed+the proper operations with the proper results.++# Adding it to your project++Add the `mockazo` dependency to your `package.yaml` **or** your `cabal` file.++If you use Stack, you also need to add `multistate-0.8.0.2` to your `extra-deps`+section in the `stack.yaml` file.++In your tests, `import Data.Component.Mock`, and you are ready to roll!++# Some restrictions++For Mockazo to work properly, we need that you do a little tweaking on your component+definitions:++## Parametrize the return context++It is common practice to make component methods return values in the `IO` context.+This might looks straightforward, but when mocking comes into place, it is much easier+to work in other contexts.++Imagine that we have a simple logging component:++```haskell+data Component = Component+  { logInfo  :: Text -> IO ()+  , logWarn  :: Text -> IO ()+  , logError :: Text -> IO ()+  }+```++For Mockazo to work properly, we parametrize the context of execution:++```haskell+data Component context = Component+  { logInfo  :: Text -> context ()+  , logWarn  :: Text -> context ()+  , logError :: Text -> context ()+  }+```++This not only makes testing easier, but also makes your code much more robust,+because when defining a function that uses this component, we are unable to execute+any other kind of code that runs in another execution contexts (like a colleague+calling `launchMissiles :: IO ()`).++## All methods must return something in a context++Generally, we use Components to model pieces of our application that perform side effects,+so adding a field that contains some static piece of data doesn't make much sense.++If you really need to do this, we recommend you that you create a companion `Configuration`+type, with all of these values, and leave the component for the side effect operations only.++If you **really** need the value inside of the component, wrap it in `context`.++Mockazo, expects all the methods to be effectful. So it will choke on a return value that+is not wrapped in the context.++So, instead of doing:++```haskell+data Component context = Component+  { foo :: Text+  }+```++Do this:++```haskell+data Component context = Component+  { foo :: context Text+  }+```++# Creating your first mock++Let's suppose that we want to mock the logging component from the first example:++```haskell+data Component context = Component+  { logInfo  :: Text -> context ()+  , logWarn  :: Text -> context ()+  , logError :: Text -> context ()+  }+```++Create a separate module for the mock (we recommend you to do it in `test/Mock`, and the+name of the module should match the name of the component module).++After that, we create the mock (following the advice, we make it in `test/Mock/Logging.hs`):++```haskell+module Mock.Logging where+import Logging   -- We import the component *UNQUALIFIED*+makeMock ''Component+```++That's it! (Yes, really)++If for some reason, you want to add the export list to the mock module (your compiler is+complaining), you can fix it like this:++```haskell+module Mock.Logging (Action(..), Component(..), mock) where+import Logging+makeMock ''Component+```++# Testing a function that calls our component++Suppose that somewhere we have a function `importantOperation`+that looks like this:++```haskell+importantOperation :: Monad context => Logging.Component context -> context ()+importantOperation Logging.Component{..} = do+  logInfo "info"+  logWarn "warn"+  logError "error"+```++We want to assure that these operations are run in order and with the+appropriate arguments. We can write a test for it by using Mockazo's little DSL.++In our tests file:++```haskell+import qualified Mock.Logging as Logging++let loggingMock = Logging.mock++-- ... somewhere in our test framework++runMock+  $ withActions+    [ Logging.LogInfo  "info"  :-> ()+    , Logging.LogWarn  "warn"  :-> ()+    , Logging.LogError "error" :-> ()+    ]+  $ importantOperation loggingMock+```++We tell the test to run a function with a mock using `runMock`.++After that, we specify the actions that we expect to be run, and what they return,+using the `:->` operator, inside of a `withActions` block.++Finally, we run the function that we want to test, by passing the mocked component to it.++# Functions that depend on multiple components++Suppose that `importantOperation` depended on two, three, or whatever more components.++The great stuff about Mockazo, is that you can chain as many `withActions` blocks as+you want, passing the expected operations for each one of the mocks.++Suppose that apart from the `Logging` component, we had another called `UserFetch`.+We could also mock it in the same way we did with `Logging`, and add the expected+operations too:++```haskell+import qualified Mock.Logging as Logging+import qualified Mock.UserFetch as UserFetch++let loggingMock   = Logging.mock+let userFetchMock = UserFetch.mock++-- ... somewhere our test framework++runMock+  $ withActions+    [ Logging.LogInfo  "info"  :-> ()+    , Logging.LogWarn  "warn"  :-> ()+    , Logging.LogError "error" :-> ()+    ]+  $ withActions+    [ UserFetch.Connect "localhost"  :-> ()+    , UserFetch.Fetch :-> User { name = "mike", password = "absolutely-encrypted" }+    ]+  $ importantOperation loggingMock userFetchMock+```++# Acknowledgements++Mockazo is heavily inspired by [`monad-mock`](https://github.com/cjdev/monad-mock/). It wouldn't have been possible to create this package without it's existence.++To all of the authors and contributors of `monad-mock`:++### **Thank you!**
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ mockazo.cabal view
@@ -0,0 +1,71 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.31.1.+--+-- see: https://github.com/sol/hpack+--+-- hash: 835e8ef5d2e80d1f671e85ac3cee6fc223d5a0055d244bf702a12ea687d50cd7++name:           mockazo+version:        0.1.0+synopsis:       Mock records of functions easily+description:    Please see the README on GitHub at <https://github.com/theam/mockazo#readme>+category:       Testing+homepage:       https://github.com/theam/mockazo#readme+bug-reports:    https://github.com/theam/mockazo/issues+author:         The Agile Monkeys+maintainer:     hackers@theagilemonkeys.com+copyright:      2019 The Agile Monkeys+license:        MIT+license-file:   LICENSE.md+build-type:     Simple+extra-source-files:+    README.md+    LICENSE.md++source-repository head+  type: git+  location: https://github.com/theam/mockazo++library+  exposed-modules:+      Data.Component.Mock+  other-modules:+      Data.Component.Mock.TH+      Data.Component.Mock.TH.Common+      Data.Component.Mock.TH.Gadt+      Data.Component.Mock.TH.Instance+      Data.Component.Mock.TH.NewFunction+      Paths_mockazo+  hs-source-dirs:+      src+  default-extensions: ConstraintKinds DataKinds DefaultSignatures FlexibleContexts GADTs KindSignatures NoImplicitPrelude OverloadedStrings RankNTypes RecordWildCards StandaloneDeriving ScopedTypeVariables TemplateHaskell TupleSections TypeOperators+  ghc-options: -Wall -fno-warn-orphans -fno-warn-name-shadowing -optP-Wno-nonportable-include-path -Wincomplete-uni-patterns -Wincomplete-record-updates -Wcompat -Widentities -Wredundant-constraints -Wmissing-export-lists -Wpartial-fields -fhide-source-paths -freverse-errors+  build-depends:+      base >=4.7 && <5+    , constraints >=0.10+    , multistate >=0.8+    , relude >=0.4+    , template-haskell >=2.14+  default-language: Haskell2010++test-suite mockazo-test+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      Mock.HigherKinded+      Mock.SingleKinded+      Paths_mockazo+  hs-source-dirs:+      test+  default-extensions: ConstraintKinds DataKinds DefaultSignatures FlexibleContexts GADTs KindSignatures NoImplicitPrelude OverloadedStrings RankNTypes RecordWildCards StandaloneDeriving ScopedTypeVariables TemplateHaskell TupleSections TypeOperators+  ghc-options: -Wall -fno-warn-orphans -fno-warn-name-shadowing -optP-Wno-nonportable-include-path -Wincomplete-uni-patterns -Wincomplete-record-updates -Wcompat -Widentities -Wredundant-constraints -Wmissing-export-lists -Wpartial-fields -fhide-source-paths -freverse-errors -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      base >=4.7 && <5+    , constraints >=0.10+    , hspec+    , mockazo+    , multistate >=0.8+    , relude >=0.4+    , template-haskell >=2.14+  default-language: Haskell2010
+ src/Data/Component/Mock.hs view
@@ -0,0 +1,112 @@+module Data.Component.Mock+  ( (:~:)(..)+  , WithResult(..)+  , IsAction(..)+  , mockAction+  , InContextOf+  , Executes+  , runMock+  , withActions++  , module Data.Component.Mock.TH+  ) where++import Relude+import Data.Type.Equality ((:~:)(..))+import Data.Constraint ((:-), (\\))+import Data.Constraint.Forall (ForallF, instF)+import Control.Monad.Trans.MultiState hiding (MultiState)+import Data.HList.ContainsType++import Data.Component.Mock.TH++{- | Context that gets injected to the components+to store the values that are being executed.++These actions will be compared during the tests+with the ones that you expect, failing in case+of a mismatch.+-}+type InContextOf s =+  MultiStateT s IO++{-| Constraint to specify that some actions+contain the type of actions to be executed.+-}+type Executes action actions =+  ContainsType [WithResult action] actions++{-| Runs the mock context and all the checks+with it+-}+runMock :: InContextOf '[] a -> IO ()+runMock = runMultiStateTNil_++{-| Specify the expected actions for a given component+to expect to be executed+-}+withActions+  :: IsAction action+  => [WithResult action]+  -> InContextOf ([WithResult action] : otherActions) a+  -> InContextOf otherActions (a, [WithResult action])+withActions actions execution = do+  (result, actionsRest) <- withMultiState actions execution+  case actionsRest of+    [] ->+      pure (result, actionsRest)++    remainingActions ->+      error+        $ "Execution ended, but those actions were expected to be run:\n"+        <> unlines (fmap (\(action :-> _) -> "  • '" <> showAction action <> "'") remainingActions)++{-| Operator to specify that an action returns some result+-}+data WithResult action where+  (:->) :: action result -> result -> WithResult action++{-| Class that all actions must implement in order to work+with the rest of the library.++You only need to implement 'eqAction'+-}+class IsAction (action :: Type -> Type) where+  eqAction :: action a -> action b -> Maybe (a :~: b)+  showAction :: action a -> Text++  default showAction :: ForallF Show action => action a -> Text+  showAction =+    toText . showAction'+   where+    showAction' :: forall g a. ForallF Show g => g a -> String+    showAction' x = show x \\ (instF :: ForallF Show g :- Show (g a))++{-| Utility function to be used in the creation of the mock+components, so the methods store action values instead of+executing anything else+-}+mockAction+  :: ContainsType ([WithResult action]) actions+  => IsAction action+  => Text+  -> action result+  -> InContextOf actions result+mockAction functionName action = do+  nextAction <- mGet+  case nextAction of+    [] ->+      error+        $ "Expected end of program, but called '" <> functionName <> "'\n"+        <> "  given action: '" <> showAction action <> "'\n"++    (action' :-> result) : actions+     | Just Refl <- action `eqAction` action' -> do+        mSet actions+        pure result++     | otherwise ->+        error+          $ "Incorrect call to '" <> functionName <> "'\n"+          <> "  called: '" <> showAction action <> "'\n"+          <> "  expected a call to: '" <> showAction action' <> "'\n"
+ src/Data/Component/Mock/TH.hs view
@@ -0,0 +1,31 @@+module Data.Component.Mock.TH+  (makeMock) where++import Relude++import qualified Language.Haskell.TH as Meta++import qualified Data.Component.Mock.TH.Gadt as Gadt+import qualified Data.Component.Mock.TH.Instance as Instance+import qualified Data.Component.Mock.TH.NewFunction as NewFunction++{-| Generates all the required things to work with a mock:+* A data type representing the component methods (Actions)+* The proper instantiation of 'IsAction' for this data type+* A 'mock' function to be used instead of 'new'.++The only requirement is that all the methods of the component+must return something wrapped in a context.+-}+makeMock :: Meta.Name -> Meta.DecsQ+makeMock componentName = do+  info <- Meta.reify componentName+  case info of+    Meta.TyConI (Meta.DataD _ recordName _ _ [Meta.RecC _ fields] _) -> do+      action <- Gadt.make fields+      instance' <- Instance.make fields+      newFunction <- NewFunction.make recordName fields+      pure $ action <> [instance'] <> newFunction++    _ ->+      fail "makeMock only accepts simple records"
+ src/Data/Component/Mock/TH/Common.hs view
@@ -0,0 +1,97 @@+module Data.Component.Mock.TH.Common+  ( actionName+  , titleizeName+  , functionTypeToList+  , makePrimeVars+  , makeVars+  , Field(..)+  , toField+  , toVarExp+  ) where++import Relude+import Data.Char++import Language.Haskell.TH as Meta+import Language.Haskell.TH.Syntax as Meta++-- | The default name for the action+actionName :: Meta.Name+actionName = Meta.mkName "Action"++-- | Converts a name to the equivalent titleized+--+-- >> titleizeName $ mkName "action"+-- Name "Action"+titleizeName :: Meta.Name -> Meta.Name+titleizeName name =+  Meta.nameBase name+  & capitalizeFirst+  & Meta.mkName+ where+  capitalizeFirst (c : cs) = toUpper c : cs+  capitalizeFirst other = other++-- | Converts a function type into a list of types, skipping the arrows+functionTypeToList :: Meta.Type -> [Meta.Type]+functionTypeToList type' =+  case type' of+    Meta.AppT (Meta.AppT Meta.ArrowT t1) t2 ->+      t1: functionTypeToList t2++    Meta.SigT t _ ->+      functionTypeToList t++    Meta.ForallT _ _ t ->+      functionTypeToList t++    t ->+      [t]++-- | Creates a list of n elements from+-- the sequence from a' to z'+makePrimeVars :: Int -> [Meta.Pat]+makePrimeVars n =+  ['a'..'z']+  & fmap one+  & fmap (<> "'")+  & take n+  & makeVarsWith++-- | Creates a list of n elements from+-- the sequence from a to z+makeVars :: Int -> [Meta.Pat]+makeVars n =+  ['a'..'z']+  & fmap one+  & take n+  & makeVarsWith++data Field = Field+  { name :: Meta.Name+  , argumentsLength :: Int+  }++toField :: Meta.VarBangType -> Field+toField (fieldName, _, fieldType) = do+  let functionTypes = functionTypeToList fieldType+  Field+    { name = fieldName+    , argumentsLength = length functionTypes - 1+    }++toVarExp :: Meta.Pat -> Meta.Exp+toVarExp (Meta.VarP varName) =+  Meta.VarE varName+toVarExp other =+  error+    $ "Mockazo: Error when running 'toVarExp' for value '" <> show other <> "'\n"+    <> "Please file an issue for this at https://github.com/theam/mockazo/issues"++makeVarsWith :: [String] -> [Meta.Pat]+makeVarsWith varNames =+  varNames+  & fmap makeVar+ where+  makeVar varName =+    Meta.VarP (Meta.mkName varName)
+ src/Data/Component/Mock/TH/Gadt.hs view
@@ -0,0 +1,104 @@+module Data.Component.Mock.TH.Gadt+  ( make+  ) where++import Relude+import qualified Relude.Unsafe as Unsafe++import qualified Language.Haskell.TH as Meta+import qualified Language.Haskell.TH.Syntax as Meta++import Data.Component.Mock.TH.Common++{-| Generates a GADT definition for actions for+a record of functions.++For example, for a record like++@+data Component context = Component+  { info    :: Text -> context ()+  , success :: Text -> context ()+  , fail    :: Text -> context ()+  , debug   :: Text -> context ()+  , start   :: Text -> context ()+  , ask     :: Text -> Text -> context Text+  }+@++it will generate the actions:++@+data Action a where+  Info    :: Text -> Action ()+  Success :: Text -> Action ()+  Fail    :: Text -> Action ()+  Debug   :: Text -> Action ()+  Start   :: Text -> Action ()+  Ask     :: Text -> Text -> Action Text+@+-}+make :: [Meta.VarBangType] -> Meta.DecsQ+make fields = do+  constructors <- traverse constructorFromField fields+  let gadtDefinition = defaultDefinition constructors+  pure (gadtDefinition : derivedInstances)++-- | Generates a GADT constructor based on a record field+constructorFromField :: Meta.VarBangType -> Meta.Q Meta.Con+constructorFromField (fieldName, _, type') =+  case functionTypeToList type' of+    result : [] -> do+      resultType <- substituteReturnContext result+      let name = titleizeName fieldName+      pure $ Meta.GadtC [name] [] resultType+    othersAndResult -> do+      let resultType = Unsafe.last othersAndResult+      let inputType = Unsafe.init othersAndResult+      resultType <- substituteReturnContext resultType+      let name = titleizeName fieldName+      pure $ Meta.GadtC [name] (toList $ fmap (noBang,) inputType) resultType++derivedInstances :: [Meta.Dec]+derivedInstances =+  fmap deriveInstanceFor ["Eq", "Show"]+ where+  deriveInstanceFor classStr =+    Meta.StandaloneDerivD+      Nothing+      []+      (Meta.AppT+        (Meta.ConT $ Meta.mkName classStr)+        (Meta.AppT+          (Meta.ConT actionName)+          (Meta.VarT $ Meta.mkName "r")))++-- | Substitutes the context type variable by Action+substituteReturnContext :: Meta.Type -> Meta.Q Meta.Type+substituteReturnContext resultType =+  case resultType of+    Meta.AppT (Meta.VarT _) res ->+      pure $ Meta.AppT (Meta.ConT actionName) res++    other -> do+      fail+        $ "Data.Component.Mock only works with records that return values in a context, but got:\n"+        <> "  - " <> Meta.pprint other++-- | Omit strictness and unpacking+noBang :: Meta.Bang+noBang = Meta.Bang Meta.NoSourceUnpackedness Meta.NoSourceStrictness++-- | Generate a GADT definition based on a list of constructors+defaultDefinition :: [Meta.Con] -> Meta.Dec+defaultDefinition constructors =+  Meta.DataD context actionName typeVars kind constructors derivingClauses+ where+  context =+    []+  typeVars =+    [Meta.PlainTV $ Meta.mkName "a"]+  kind =+    Nothing+  derivingClauses =+    []
+ src/Data/Component/Mock/TH/Instance.hs view
@@ -0,0 +1,99 @@+module Data.Component.Mock.TH.Instance+  ( make+  ) where++import Relude++import qualified Language.Haskell.TH as Meta+import qualified Language.Haskell.TH.Syntax as Meta++import Data.Component.Mock.TH.Common++{-| Generates an instance for IsAction based on a record.++For a record like++@+data Component context = Component+  { rootDir          :: ProjectName -> context (Path Rel Dir)+  , getProjectConfig :: ProjectName -> context ProjectConfig+  , initProject      :: Path Rel Dir -> ProjectConfig -> context ()+  }+@++It will generate an instance like+@+instance IsAction Action where+  eqAction (RootDir a) (RootDir b) =+    if a == b then Just Refl else Nothing++  eqAction (GetProjectConfig a) (GetProjectConfig b) =+    if a == b then Just Refl else Nothing++  eqAction (InitProject a a') (InitProject b b') =+    if a == b && a' == b' then Just Refl else Nothing++  eqAction _ _ =+    Nothing+@+-}+make :: [Meta.VarBangType] -> Meta.DecQ+make rawFields = do+  let fields = fmap toField rawFields+  pure $ Meta.InstanceD+    Nothing+    []+    (Meta.AppT (Meta.ConT $ Meta.mkName "IsAction") (Meta.ConT actionName))+    (functionDeclarations fields)++functionDeclarations :: [Field] -> [Meta.Dec]+functionDeclarations fields =+  [ Meta.FunD+      (Meta.mkName "eqAction")+      (fmap makeClause fields <> [wildcardClause])+    ]++makeClause :: Field -> Meta.Clause+makeClause Field{..} =+  Meta.Clause+    [ Meta.ConP (titleizeName name) (makeVars argumentsLength)+    , Meta.ConP (titleizeName name) (makePrimeVars argumentsLength)+    ]+    (Meta.NormalB+      (Meta.CondE+        (makeCondition argumentsLength)+        (Meta.AppE+          (Meta.ConE $ Meta.mkName "Just")+          (Meta.ConE $ Meta.mkName "Refl"))+        (Meta.ConE $ Meta.mkName "Nothing")))+    []++wildcardClause :: Meta.Clause+wildcardClause =+  Meta.Clause+    [ Meta.WildP+    , Meta.WildP+    ]+    (Meta.NormalB+      (Meta.ConE $ Meta.mkName "Nothing"))+    []++makeCondition :: Int -> Meta.Exp+makeCondition varNumber = do+  let vars = fmap toVarExp $ makeVars varNumber+  let primeVars = fmap toVarExp $ makePrimeVars varNumber+  let comparedVars = zipWith compareVars vars primeVars+  case comparedVars of+    [condition] ->+      condition+    (condition : moreConditions) ->+      foldl' joinConditions condition moreConditions+    other ->+      error+      $ "Mockazo: Error when running 'makeCondition' for value '" <> show other <> "'\n"+      <> "Please file an issue for this at https://github.com/theam/mockazo/issues"+ where+  compareVars v v' =+    Meta.UInfixE v (Meta.VarE (Meta.mkName "==")) v'+  joinConditions c c' =+    Meta.UInfixE c (Meta.VarE (Meta.mkName "&&")) c'
+ src/Data/Component/Mock/TH/NewFunction.hs view
@@ -0,0 +1,57 @@+module Data.Component.Mock.TH.NewFunction+  (make) where++import Relude++import qualified Language.Haskell.TH as Meta+import qualified Language.Haskell.TH.Syntax as Meta++import Data.Component.Mock.TH.Common++make :: Meta.Name -> [Meta.VarBangType] -> Meta.DecsQ+make recordName fields = pure $+  [ Meta.SigD+      mockName+      (Meta.ForallT+        []+        [ Meta.AppT+            (Meta.AppT+              (Meta.ConT $ Meta.mkName "Executes")+              (Meta.ConT $ Meta.mkName "Action"))+            (Meta.VarT $ Meta.mkName "actions")+        ]+        (Meta.AppT+          (Meta.ConT recordName)+          (Meta.AppT+            (Meta.ConT $ Meta.mkName "InContextOf")+            (Meta.VarT $ Meta.mkName "actions"))))+  , Meta.ValD+     (Meta.VarP mockName)+     (Meta.NormalB+       (Meta.RecConE+         (eraseNameInformation recordName)+         (fmap (mockField . toField) fields)+         )) []+  ]++mockField :: Field -> (Meta.Name, Meta.Exp)+mockField Field{..} = do+  let varExps = fmap toVarExp $ makeVars argumentsLength+  let actionConstructor = Meta.ConE (titleizeName name)+  let appliedConstructor = foldl' Meta.AppE actionConstructor varExps+  ( name+    , Meta.LamE+      (makeVars argumentsLength)+      (Meta.AppE+        (Meta.AppE+          (Meta.VarE $ Meta.mkName "mockAction")+          (Meta.LitE $ Meta.StringL $ Meta.nameBase name))+        appliedConstructor)+    )++mockName :: Meta.Name+mockName = Meta.mkName "mock"++eraseNameInformation :: Meta.Name -> Meta.Name+eraseNameInformation =+  Meta.mkName . Meta.nameBase
+ test/Mock/HigherKinded.hs view
@@ -0,0 +1,22 @@+module Mock.HigherKinded+  ( Action(..)+  , Component(..)+  , mock+  , test+  ) where++import Relude+import Data.Component.Mock++data Component context = Component+  { foo :: Maybe Int -> Maybe Int -> context ()+  , bar :: Maybe Text -> context (Maybe Int)+  }++makeMock ''Component++test :: Monad context => Component context -> context ()+test Component{..} = do+  x <- bar (Just "x")+  y <- bar (Just "y")+  foo x y
+ test/Mock/SingleKinded.hs view
@@ -0,0 +1,22 @@+module Mock.SingleKinded+  ( Action(..)+  , Component(..)+  , mock+  , test+  ) where++import Relude+import Data.Component.Mock++data Component context = Component+  { foo :: Int -> Int -> context ()+  , bar :: Text -> context Int+  }++makeMock ''Component++test :: Monad context => Component context -> context ()+test Component{..} = do+  x <- bar "x"+  y <- bar "y"+  foo x y
+ test/Spec.hs view
@@ -0,0 +1,31 @@+import Relude++import Test.Hspec+import Data.Component.Mock++import qualified Mock.SingleKinded as SingleKinded+import qualified Mock.HigherKinded as HigherKinded++main :: IO ()+main = hspec $ do+  describe "mockazo" $ do+    describe "mocking components" $ do+      it "handles single kinded types" $ do+        let mock = SingleKinded.mock+        runMock+          $ withActions+            [ SingleKinded.Bar "x" :-> 3+            , SingleKinded.Bar "y" :-> 4+            , SingleKinded.Foo 3 4 :-> ()+            ]+          $ SingleKinded.test mock++      it "handles (fully applied) higher kinded types" $ do+        let mock = HigherKinded.mock+        runMock+          $ withActions+            [ HigherKinded.Bar (Just "x") :-> Just 2+            , HigherKinded.Bar (Just "y") :-> Just 3+            , HigherKinded.Foo (Just 2) (Just 3) :-> ()+            ]+          $ HigherKinded.test mock