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 +25/−0
- README.md +205/−0
- Setup.hs +2/−0
- mockazo.cabal +71/−0
- src/Data/Component/Mock.hs +112/−0
- src/Data/Component/Mock/TH.hs +31/−0
- src/Data/Component/Mock/TH/Common.hs +97/−0
- src/Data/Component/Mock/TH/Gadt.hs +104/−0
- src/Data/Component/Mock/TH/Instance.hs +99/−0
- src/Data/Component/Mock/TH/NewFunction.hs +57/−0
- test/Mock/HigherKinded.hs +22/−0
- test/Mock/SingleKinded.hs +22/−0
- test/Spec.hs +31/−0
+ 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_++[](https://circleci.com/gh/theam/mockazo)+[](http://makeapullrequest.com)+[](http://isitmaintained.com/project/theam/mockazo "Average time to resolve an issue")+[](https://github.com/theam/mockazo/blob/master/LICENSE)+[](https://hackage.haskell.org/package/mockazo)+[](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