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:
- Match arguments using predicates, rather than exact values.
- Specify whether mocked actions must happen in a guaranteed order, in any order, some number of times, etc.
- 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.