Writing Pure, Testable, Effectful Programs: A Saga

Hey guys, I wrote a blog post about trying to write a simple pure, testable, effectful program in PureScript and the issues I ran into along the way. I’d appreciate any feedback y’all have. It also might be helpful for anyone new to the effect system in PureScript.

4 Likes

Is there a link to this blog post?

1 Like

whoops, lol, there is now!

1 Like

I think mtl style is probably the simplest option for achieving what you want, and would be a good starting point based on where you are now. You start by defining a type class for the operations you want:

class MonadFileSystem m where
  readFile :: String -> m String
  writeFile :: String -> String -> m Unit

and then write your code using constraints rather than concrete monads/ monad transformer stacks:

getSignedMessage :: forall m. MonadFileSystem m => m String

And finally you define instances for both the type you want to use in production as well as in testing:

instance MonadFileSystem (ExceptT Error Effect) where [...]

instance MonadFileSystem (Writer (Array String)) where
  readFile path = do
    tell ["Read file: " <> path]
    pure "pretend file contents"
  writeFile path _ = do
    tell ["Wrote file: " <> path]

Of course the instance you use for testing will depend on exactly how you want to test. You might want a state monad which can keep track of the ‘files’ you’ve written to so that reading files returns whatever you’ve previously written to them, for example.

3 Likes

Personally, I don’t derive nearly as much value out of automated testing for effectful code than what you have to put in. Especially when mocking the effects themselves, so the effectiveness of the tests relies on the developer already understanding the exact behavior of those effects. I get a lot more mileage out of extracting as much logic as possible out into pure functions and testing that logic with my unit tests. On a good day, the remaining effectful code is so straightforward that any sort of test involving mocks would be strictly tautological. Then I’ll try to cover it in my system tests that actually run the real effects so your tests are covering the real interactions of your system, rather than the perceived interactions of whoever built the mocks.

I’m very, very interested in hearing other opinions on the matter, because I could be way off in the deep end with my perspective. Has anyone derived any value out of mocked out integration tests of effectful code? Like they’ve caught legitimate bugs with it?

4 Likes

Rad. This looks like it will work. I was originally looking into free monads, but it was a little more heavy-weight than I was ready for.

Personally, I don’t understand the fascination with free monads. I think they’re overly complicated and often not that useful; imo you don’t want them in the vast majority of cases.

2 Likes

I tend to agree that testing effectful code can have diminishing returns. Anytime you start mocking things, your tests provide less value. However, there are many times when your logic depends heavily on the result of an effect, and therefore can’t be extracted without a bunch of indirection.

For example, I was working on porting a little utility for tracking file size changes across builds from JavaScript to PureScript. It involves lots of effects: reading a config file, reading file stats, posting GitHub commit status updates, etc.

In the JavaScript version, I used an effect type which has reader functionality baked-in (code here if you’re at all interested), allowing me to inject dependencies. It’s somewhat similar to the ZIO type from Scala. And I’ve definitely caught legitimate bugs through these “integration” tests. Although they are much more complicated to write (see /packages/thresh/src/tests/thresh-test.js from that same repo for an example) I’ve found I don’t need to write too many of them to gain a lot of confidence that the functionality of my code is correct overall.

2 Likes

Have you looked at purescript-run? I’m enjoying it a lot more than mtl style

3 Likes