Roman Cheplyaka

Resources in Tasty (update)

December 29, 2013

In a recent article I described how resources were introduced to the Tasty test framework, as well as some alternative approaches. This article describes the new API, introduced in Tasty 0.7.

To recap, there was a function, withResource, that handled creation/acquisition and disposal of resources, but if you needed to access the resource directly in the tests, you had to store the resource in an IORef (or similar) as part of the initialization routine.

At the time it seemed acceptable, but later I discovered that when the number of resources was bigger than one or two, or even not known in advance (when tests are generated rather than just written down), this was inconvenient enough to start looking for a different solution.

One of the major problems with tests receiving the resource value directly, as in

withResource
  :: IO a
  -> (a -> IO ())
  -> (a -> TestTree)
  -> TestTree

… was that the resource could be used not only in the tests themselves, but to construct the tests, which is bad/wrong for a number of reasons. For instance, we don’t want to create the resources when we’re not running tests, but we still want to know which tests we have.

The solution I found is to pass not the value of the resource, but an IO action yielding the resource.

withResource
  :: IO a
  -> (a -> IO ())
  -> (IO a -> TestTree)
  -> TestTree

Even though it’s an IO action, it doesn’t acquire the resource, because such a resource wouldn’t be shared across multiple tests, which is the semantics we’re after. Instead, it returns the resource which has been acquired (think: reads from an IORef or MVar). But thanks to it being an IO action, it can only be used inside a test, and not to construct or alter tests based on the resource value.

Here’s a modified example from the last article which works with this new API:

import Test.Tasty
import Test.Tasty.HUnit

-- assumed defintions
data Foo
acquire :: IO Foo
release :: Foo -> IO ()
testWithFoo :: Foo -> Assertion
(acquire, release, testWithFoo) = undefined

main = do
  defaultMain $
    withResource acquire release tests

tests :: IO Foo -> TestTree
tests getResource =
  testGroup "Tests"
    [ testCase "x" $ getResource >>= testWithFoo
    ]