1

I have written a program for a project that uses Pipes, which I love! I'm struggling to unit test my code however.

I have a series of functions of type Pipe In Out IO () (for example) that I wish to test with HSpec. How can I go about this?

For example, suppose I have this domain:

data Person = Person String Int | Unknown deriving (Show, Eq)
data Classification = Friend | Foe | Undecided deriving Show

and this Pipe:

classify :: Pipe Person (Person, Classification) IO ()
classify = do
    p@(Person name _) <- await
    case name of 
      "Alex" -> yield (p, Friend)
      "Bob" -> yield (p, Foe)
      _ -> yield (p, Undecided)

I would like to write a spec:

main = hspec $ do
  describe "readFileP" $ 
    it "yields all the lines of a file"
      pendingWith "How can I test this Pipe? :("
Alex
  • 8,093
  • 6
  • 49
  • 79
  • Convert your pipe to a producer or an effect and use [toListM](https://hackage.haskell.org/package/pipes-4.2.0/docs/Pipes-Prelude.html#v:toListM-39-) or simply `runEffect` to materialize the values. Obviously you must decide how to create and supply test data to the pipe. – user2407038 Aug 27 '16 at 17:23
  • `runEffect` will just give me an `m r` which in this case is `IO ()`. Not sure how that's supposed to help? – Alex Aug 27 '16 at 19:11
  • Not `runEffect classify` but `runEffect (giveDataToClassify classify)` - like I said, your pipe takes an input and you must decide *what* the input is, simply by combining your pipe in the appropriate appropriate manner with a pipe which creates output without requiring input (I think in `pipes` this is a `Producer`). For example, does `toListM $ mapM_ yield [ Person "Bob" 10, Person "June" 20 ] >-> classify` do what you want? Note the type of `\xs -> toListM $ mapM_ yield xs >-> classify` is `[Person] -> IO [(Person, Classification)]` which seems to me a form compatible with HSpec. – user2407038 Aug 28 '16 at 15:33
  • I understand that I need to feed data to the pipe. My issue is that the `runEffect` call is in the `IO` monad, which can't evaluate to the `Expectation` required by hspec's `it`. – Alex Aug 29 '16 at 14:51
  • Looking at the [type of `it`](https://hackage.haskell.org/package/hspec-2.2.3/docs/Test-Hspec.html), it takes an `Example a => a` argument, and you have an instance for `Example Expectation` (i.e. `Example (IO ())`), whose semantics are throwing a `HUnitFailure` exception denotes failure of the test and throwing a `Result` exception (seems to?) denote success of the test. Which (almost) fits the bill. I think it strange that there is no `Example a => Example (IO a)` instance, or even `Example (IO Result)` - it seems these could be useful to you. Perhaps you should try writing them yourself? – user2407038 Aug 29 '16 at 16:00
  • I'll give it a go. Much appreciated. – Alex Aug 29 '16 at 17:27

2 Answers2

1

You could use the functions of the temporary package to create temporary files with the expected data, and then test that the data is read correctly by the pipe.

Incidentally, your Pipe is using readFile that performs lazy I/O. Lazy I/O and streaming libraries like pipes don't mix very well, in fact the latter exist mainly as an alternative to the former!

Perhaps you should instead use functions which perform strict I/O, like openFile and getLine.

One annoyance with strict I/O is that it forces you to consider resource allocation more carefully. How to ensure that each file handle is closed at the end, or in case of error? One possible way to achieve this is to work in the ResourceT IO monad, instead of directly in IO.

danidiaz
  • 26,936
  • 4
  • 45
  • 95
  • My code isn't doing anything of the sort with IO like that (actually DB calls etc). The concept of IO in this question is merely for demonstration purposes and not the intent of the question at all. I want to know if & how I can test my Pipe code with HSpec. – Alex Aug 27 '16 at 19:50
1

The trick is to use toListM from Pipes ListT monad transformer.

import Pipes
import qualified Pipes.Prelude as P
import Test.Hspec

data Person = Person String Int | Unknown deriving (Show, Eq)
data Classification = Friend | Foe | Undecided deriving (Show, Eq)

classify :: Pipe Person (Person, Classification) IO ()
classify = do
  p@(Person name _) <- await
  case name of 
    "Alex" -> yield (p, Friend)
    "Bob" -> yield (p, Foe)
    _ -> yield (p, Undecided)

The test, using the ListT transformer to convert the pipe to a ListT and asserting using HSpec:

main = hspec $ do
  describe "classify" $ do
    it "correctly finds friends" $ do
      [(p, cl)] <- P.toListM $ each [Person "Alex" 31] >-> classify
      p `shouldBe` (Person "Alex" 31)
      cl `shouldBe` Friend

Note, you don't have to use each, this could be a simple producer with a call to yield.

Alex
  • 8,093
  • 6
  • 49
  • 79