Alex
Alex

Reputation: 8303

Haskell Pipes and testing with HSpec

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? :("

Upvotes: 2

Views: 348

Answers (2)

Alex
Alex

Reputation: 8303

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.

Upvotes: 2

danidiaz
danidiaz

Reputation: 27771

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.

Upvotes: 2

Related Questions