December 29, 2013
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
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 ]