Giter Site home page Giter Site logo

monad-mock's Introduction

monad-mock Build Status

monad-mock is a Haskell package that provides a monad transformer to help create “mocks” of mtl-style typeclasses, intended for use in unit tests. A mock can be executed by providing a sequence of expected monadic calls and their results, and the mock will verify that the computation conforms to the expectation.

For example, imagine a MonadFileSystem typeclass, which describes a class of monads that may perform filesystem operations:

class Monad m => MonadFileSystem m where
  readFile :: FilePath -> m String
  writeFile :: FilePath -> String -> m ()

Using MockT, it’s possible to test computations that use MonadFileSystem in a completely pure way:

copyFile :: MonadFileSystem m => FilePath -> FilePath -> m ()
copyFile a b = do
  x <- readFile a
  writeFile b x

makeMock "FileSystemAction" [ts| MonadFileSystem |]

spec = describe "copyFile" $
  it "reads a file and writes its contents to another file" $
    evaluate $ copyFile "foo.txt" "bar.txt"
      & runMock [ ReadFile "foo.txt" :-> "contents"
                , WriteFile "bar.txt" "contents" :-> () ]

For more information, see the documentation on Hackage.

monad-mock's People

Contributors

lexi-lambda avatar

Stargazers

 avatar Chris Smith avatar George Takumi Crary avatar GAURAV avatar Henry Blanchette avatar Jappie Klooster avatar Jonathan Queiroz avatar Santi Lertsumran avatar Dave Parfitt avatar Kamil Adam avatar  avatar Jonghyouk Yun avatar Orestis Ousoultzoglou avatar Mathias Verraes avatar Yoshi avatar Brooklyn Zelenka avatar tana-gh avatar  avatar 大関 金城 秀喜 カシオ avatar  avatar  avatar Allan Lukwago avatar Maciej Smolinski avatar Johnny Reina avatar xmonader avatar canavar avatar Marco Z avatar Valentin Reis avatar chessai avatar Stephen Diehl avatar Leon Coto avatar Daniel P. Brice avatar Carlos Pavanetti avatar Timothy Emiola avatar Daniel Kahlenberg avatar bouzuya avatar Daishi Nakajima avatar Matt Audesse avatar harapan avatar Thomas Honeyman avatar Matheus Albuquerque avatar Andrejs Agejevs avatar Eliza Zhang avatar Sascha Grunert avatar Kristian Sällberg avatar Dan Rosén avatar Rajiv Bose avatar Hiroshi Hatake avatar Ecky Putrady avatar Serhii Khoma avatar Pablo Parga avatar Cheshire Cat avatar YAMAMOTO Yuji avatar Julien Goux avatar Philip avatar Greg Hale avatar Lin He avatar Marco Sampellegrini avatar  avatar Diogo Biazus avatar Val Packett avatar Tim Kersey avatar Dmitrii Kovanikov avatar Jack Firth avatar Paul Young avatar Brandon Byskov avatar  avatar Vaibhav Sagar avatar Elliot Cameron avatar Tyler Sommer avatar Eric Bailey avatar

Watchers

 avatar Rafał Kotusiewicz avatar  avatar  avatar James Cloos avatar Stu Penrose avatar Daniel P. Brice avatar Kamil Adam avatar tana-gh avatar  avatar  avatar

monad-mock's Issues

Problem installing with stack

In the dependencies for myproject-0.1.0.0:
    monad-mock needed, but the stack configuration has no specified version  (latest matching version is 0.2.0.0)
needed since myproject is a build target.

Some different approaches to resolving this:

  * Consider trying 'stack solver', which uses the cabal-install solver to attempt to find some working build configuration. This can be
    convenient when dealing with many complicated constraint errors, but results may be unpredictable.

  * Recommended action: try adding the following to your extra-deps in /home/srghma/projects/myproject/stack.yaml:

More flexible matching, ordering, cardinality, and responses

Currently, monad-mock requires an exact list of the actions that will be performed by the test case. I think it's fairly well understood that, in practice, this leads to over-assertion and brittle tests. By contrast, well-developed mock frameworks in mainstream programming languages have converged around a design that lets a programmer:

  1. Match arguments using predicates, rather than exact values.
  2. Specify whether mocked actions must happen in a guaranteed order, in any order, some number of times, etc.
  3. Perform arbitrary actions when responding to a mocked call, including looking at parameters, setting up additional expectations (e.g., if you open a file handle, you must close it). This isn't so important if the exact sequence of actions is listed, but it's more important when expectations are more flexible.

The effect of this is to move closer to a middle ground between mocks and fakes, where a user can write tests that pass for a larger range of correct implementations, while asserting more precisely which properties they care about.

I've been experimenting today with extending the techniques in monad-mock to provide some of these benefits. An example that I think is at least a bit compelling is https://code.world/haskell#P1ruEyf0CvftpB5zOiN2kUw

copyFile :: MonadFS m => FilePath -> FilePath -> m ()
copyFile a b = do
  writeFile "progress.txt" "Starting..."
  txt <- readFile a
  cfg <- readFile ".config"
  writeFile "progress.txt" "... read the source file"
  writeFile b $
    if cfg == "transform: upper" then map toUpper txt else txt
  writeFile "progress.txt" "Finished."

main :: IO ()
main = hspec $ do
  describe "copyFile" $ do
    let isConfigFile = startsWith "." `andAlso` hasSubstring "config"

        setupConfig tform =
          mock $
            whenever $
              ReadFile_ isConfigFile :=> \_ -> return ("transform: " ++ tform)

        ignoreProgress =
          mock $
            whenever $
              WriteFile_ (isEqual "progress.txt") isAny :=> \_ -> return ()

        ignoreCopy = do
          mock $ whenever $ ReadFile "foo.txt" |-> ""
          mock $
            whenever $
              WriteFile_ (isEqual "bar.txt") isAny :=> \_ -> return ()

    it "copies the file correctly" $
      example $
        runMockT $ do
          setupConfig "none"
          ignoreProgress

          mock $ expect $ ReadFile "foo.txt" |-> "lorem ipsum"
          mock $ expect $ WriteFile "bar.txt" "lorem ipsum" |-> ()

          copyFile "foo.txt" "bar.txt"

    it "performs the transform" $
      example $
        runMockT $ do
          setupConfig "upper"
          ignoreProgress

          mock $ expect $ ReadFile "foo.txt" |-> "lorem ipsum"
          mock $ expect $ WriteFile "bar.txt" "LOREM IPSUM" |-> ()

          copyFile "foo.txt" "bar.txt"

    it "doesn't read the config file more than once" $
      example $
        runMockT $ do
          ignoreProgress
          ignoreCopy

          mock $
            expectN (atMost 1) $
              ReadFile_ isConfigFile :=> \_ -> return "transform: none"

          copyFile "foo.txt" "bar.txt"

    it "reports progress in progress.txt" $
      example $
        runMockT $ do
          setupConfig "none"
          mock $
            inSequence
              [ expect $ WriteFile "progress.txt" "Starting..." |-> (),
                expect $ ReadFile "foo.txt" |-> "",
                expect $ WriteFile "progress.txt" "... read the source file" |-> (),
                expect $ WriteFile_ (isEqual "bar.txt") isAny :=> \_ -> return (),
                expect $ WriteFile "progress.txt" "Finished." |-> ()
              ]

          copyFile "foo.txt" "bar.txt"

I don't know how to keep this backward compatible, but except for the generated instances, it's close. One simply must replace :-> with |->, and move the list of function calls from an argument of runMockT to mock calls in the body of the test. To retain the behavior of the old version, you should use inSequence in the mock calls, but honestly it's probably better not to, unless there's a reason to assert things about the sequence, so you should probably use allOf or multiple mock calls instead.

There were some places I changed a bit more than necessary, perhaps: such as removing the redundant argument to runMockT, and changing the Action class to Mockable and parameterizing over the constraint. But honestly, I think it's so much nicer and more consistent this way.

This is just a prototype. But I wanted to get some early feedback about whether this is a direction you're interested in going with monad-mock, or if this seems better suited to a new package.

Problem with typeclass having function returning ExceptT

I have this kind of type class:

data AddUserError = AddUserErrorDuplicateName
type Name = Text
type UserId = Text

class UserRepo where
  addUser :: Name -> ExceptT AddUserError UserId

when I use TH:

makeAction "UserRepoAction" [ts| UserRepo |]

I got this error:

Data constructor ‘AddUser’ returns type ‘ExceptT AddUserError UserRepoAction UserId’
        instead of an instance of its parent type ‘UserRepoAction r_a1gzR’

I'm wondering how would you usually model effect that might return error in type class and use monad-mock to test it.

thanks!

GHC 7.10 support

I am using this library in language-ninja, which attempts to support all versions of GHC going back to the version used in the latest Ubuntu LTS release (right now, that's GHC 7.10.3). Unfortunately, monad-mock currently contains some Template Haskell stuff that seems difficult to make functional across many versions of GHC. It seems to me that it would be a good idea to have a separate monad-mock-th, to avoid this issue.

If you'd rather just fix monad-mock for 7.10, here's a diff representing the progress I made on that front:

diff --git a/.stylish-haskell.yaml b/.stylish-haskell.yaml
new file mode 100644
index 0000000..5e36f0c
--- /dev/null
+++ b/.stylish-haskell.yaml
@@ -0,0 +1 @@
+steps: {}
diff --git a/library/Control/Monad/Mock/TH.hs b/library/Control/Monad/Mock/TH.hs
index 20fc9e6..8180528 100644
--- a/library/Control/Monad/Mock/TH.hs
+++ b/library/Control/Monad/Mock/TH.hs
@@ -1,5 +1,9 @@
 {-# LANGUAGE CPP #-}
+#if __GLASGOW_HASKELL__ >= 801
 {-# LANGUAGE TemplateHaskellQuotes #-}
+#else
+{-# LANGUAGE TemplateHaskell #-}
+#endif
 
 {-|
 This module provides Template Haskell functions for automatically generating
diff --git a/package.yaml b/package.yaml
index 2ea3c77..663cb49 100644
--- a/package.yaml
+++ b/package.yaml
@@ -40,7 +40,7 @@ default-extensions:
 
 library:
   dependencies:
-  - base >= 4.9.0.0 && < 5
+  - base >= 4.6.0.0 && < 5
   - constraints >= 0.3.1
   - exceptions >= 0.6
   - haskell-src-exts
@@ -48,7 +48,8 @@ library:
   - th-orphans
   - monad-control >= 1.0.0.0 && < 2
   - mtl
-  - template-haskell >= 2.11.0.0 && < 2.13
+  - template-haskell >= 2.10.0.0 && < 2.13
+  - transformers
   - transformers-base
   source-dirs: library

It currently fails due to changes in the Infix* and Parens* constructors between template-haskell-2.10.0.0 and template-haskell-2.11.0.0.

Provide a stateless version of MockT

As described here the class of monads for which StM m a ~ a holds enjoy better compositionality properties than the ones that do not.

This property does not currently hold for MockT, as StM (MockT f) a ~ (a, [WithResult f]). This is a consequence of building MockT f on top of StateT [WithResult f] m, and it limits the applicability of MockT in monad stacks which require a stateless MonadTransControl instance. This is not an academic concern - it happens to be quite common these days.

To address this,MockT can be defined on top of ReaderT (IORef [WithResult f]) m, which possesses a MonadIO instance as long given MonadIO m. Or more generally using MutVar from the primitive package.

Since this incurs in additional constraints for the base monad, it's not a backwards compatible change. An alternative is to add a Control.Monad.Mock.Stateless module with a one-for-one replacement for MockT.

Class Members with Type Variable Cause Compile Error in makeAction

It seems only fixed types are supported by makeAction. If I change the type of readFile in the test case slightly, to say:

class MonadError e m => MonadFileSystem e m | m -> e where
  readFile :: Show a => a -> m String
  writeFile :: FilePath -> String -> m ()

Then the code produced by makeAction will not compile:

    • Could not deduce: a1 ~ a
      from the context: (r ~ [Char], Show a)
        bound by a pattern with constructor:
                   ReadFile :: forall a_XrHB.
                               Show a_XrHB =>
                               a_XrHB -> FileSystemAction String,
                 in an equation for ‘==’
        at /Users/jeremy/repos/haskell/monad-mock/test-suite/Control/Monad/MockSpec.hs:20:1-60
      ‘a1’ is a rigid type variable bound by
        a pattern with constructor:
          ReadFile :: forall a_XrHB.
                      Show a_XrHB =>
                      a_XrHB -> FileSystemAction String,
        in an equation for ‘==’
        at /Users/jeremy/repos/haskell/monad-mock/test-suite/Control/Monad/MockSpec.hs:20:1
      ‘a’ is a rigid type variable bound by
        a pattern with constructor:
          ReadFile :: forall a_XrHB.
                      Show a_XrHB =>
                      a_XrHB -> FileSystemAction String,
        in an equation for ‘==’
        at /Users/jeremy/repos/haskell/monad-mock/test-suite/Control/Monad/MockSpec.hs:20:1
    • In the second argument of ‘(==)’, namely ‘b1’
      In the expression: ((a1 == b1))
      In an equation for ‘==’:
          (==) (ReadFile a1) (ReadFile b1) = ((a1 == b1))
      When typechecking the code for ‘==’
        in a derived instance for ‘Eq (FileSystemAction r)’:
        To see the code I am typechecking, use -ddump-deriv
    • Relevant bindings include
        b1 :: a1
          (bound at /Users/jeremy/repos/haskell/monad-mock/test-suite/Control/Monad/MockSpec.hs:20:1)
        a1 :: a
          (bound at /Users/jeremy/repos/haskell/monad-mock/test-suite/Control/Monad/MockSpec.hs:20:1)

/Users/jeremy/repos/haskell/monad-mock/test-suite/Control/Monad/MockSpec.hs:20:1: warning: [-Wdeferred-type-errors]
    • Could not deduce: a2 ~ a1
      from the context: (a ~ [Char], Show a1)
        bound by a pattern with constructor:
                   ReadFile :: forall a_XrHB.
                               Show a_XrHB =>
                               a_XrHB -> FileSystemAction String,
                 in an equation for ‘eqAction’
        at /Users/jeremy/repos/haskell/monad-mock/test-suite/Control/Monad/MockSpec.hs:20:1-60
      or from: (b ~ [Char], Show a2)
        bound by a pattern with constructor:
                   ReadFile :: forall a_XrHB.
                               Show a_XrHB =>
                               a_XrHB -> FileSystemAction String,
                 in an equation for ‘eqAction’
        at /Users/jeremy/repos/haskell/monad-mock/test-suite/Control/Monad/MockSpec.hs:20:1-60
      ‘a2’ is a rigid type variable bound by
        a pattern with constructor:
          ReadFile :: forall a_XrHB.
                      Show a_XrHB =>
                      a_XrHB -> FileSystemAction String,
        in an equation for ‘eqAction’
        at /Users/jeremy/repos/haskell/monad-mock/test-suite/Control/Monad/MockSpec.hs:20:1
      ‘a1’ is a rigid type variable bound by
        a pattern with constructor:
          ReadFile :: forall a_XrHB.
                      Show a_XrHB =>
                      a_XrHB -> FileSystemAction String,
        in an equation for ‘eqAction’
        at /Users/jeremy/repos/haskell/monad-mock/test-suite/Control/Monad/MockSpec.hs:20:1
    • In the second argument of ‘(==)’, namely ‘y_arVc’
      In the first argument of ‘(&&)’, namely ‘(==) x_arVb y_arVc’
      In the expression: (&&) ((==) x_arVb y_arVc) True
    • Relevant bindings include
        y_arVc :: a2
          (bound at /Users/jeremy/repos/haskell/monad-mock/test-suite/Control/Monad/MockSpec.hs:20:1)
        x_arVb :: a1
          (bound at /Users/jeremy/repos/haskell/monad-mock/test-suite/Control/Monad/MockSpec.hs:20:1)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.