diff --git a/iteratee.cabal b/iteratee.cabal
--- a/iteratee.cabal
+++ b/iteratee.cabal
@@ -1,5 +1,5 @@
 Name:		iteratee
-Version:        0.2.1
+Version:        0.2.3
 Cabal-Version:  >= 1.2
 Description:	The IterateeGM monad provides strict, safe, and functional
                 I/O.  In addition to pure Iteratee processors, file IO and 
@@ -53,4 +53,18 @@
                         Data.Iteratee.WrappedByteString
  other-modules:         Data.Iteratee.IO.Fd
                         Data.Iteratee.IO.Handle
+
+Executable testIteratee
+  Hs-Source-Dirs:  src, tests
+  Main-Is:         testIteratee.hs
+  Other-Modules:   QCUtils
+  if flag(buildTests)
+    Build-Depends: QuickCheck >= 2 && < 3, test-framework >= 0.2 && < 0.3, test-framework-quickcheck2 >= 0.2 && < 0.3
+  Else
+    Executable:    False
+    Buildable:     False
+  If flag(splitBase)
+    Build-Depends: base >= 3 && < 5
+  Else
+    Build-Depends: base >= 1 && < 2
 
diff --git a/src/Data/Iteratee/Base.hs b/src/Data/Iteratee/Base.hs
--- a/src/Data/Iteratee/Base.hs
+++ b/src/Data/Iteratee/Base.hs
@@ -34,12 +34,17 @@
   heads,
   peek,
   skipToEof,
-  seek,
-  -- ** Advanced iteratee combinators
+  length,
+  -- ** Nested iteratee combinators
   take,
   takeR,
   mapStream,
   convStream,
+  filter,
+  -- ** Folds
+  foldl,
+  foldl',
+  foldl1,
   -- * Enumerators
   enumEof,
   enumErr,
@@ -47,15 +52,17 @@
   enumPure1Chunk,
   enumPureNChunk,
   -- * Misc.
+  seek,
   FileOffset
 )
 where
 
-import Prelude hiding (head, drop, dropWhile, take, break)
+import Prelude hiding (head, drop, dropWhile, take, break, foldl, foldl1, length, filter)
 import qualified Prelude as P
 
 import qualified Data.Iteratee.Base.StreamChunk as SC
 import qualified Data.ListLike as LL
+import qualified Data.ListLike.FoldableLL as FLL
 import Data.Iteratee.IO.Base
 import Control.Monad
 import Control.Applicative
@@ -136,6 +143,7 @@
 
 -- Useful combinators for implementing iteratees and enumerators
 
+-- | Lift an IterGV result into an 'IterateeG'
 liftI :: (Monad m, SC.StreamChunk s el) =>
          IterGV s el m a -> IterateeG s el m a
 liftI (Cont k Nothing)     = k
@@ -146,12 +154,15 @@
   check str (Chunk str') = return $ Done a (Chunk $ str `mappend` str')
   check _str e@(EOF _)   = return $ Done a e
 
+-- | Run an 'IterateeG' and get the result.  An 'EOF' is sent to the
+-- iteratee as it is run.
 run :: (Monad m, SC.StreamChunk s el) => IterateeG s el m a -> m a
 run iter = runIter iter (EOF Nothing) >>= \res ->
   case res of
     Done x _ -> return x
     Cont _ e -> error $ "control message: " ++ show e
 
+-- | Check if a stream has finished ('EOF').
 isFinished :: (SC.StreamChunk s el, Monad m) =>
               IterateeG s el m (Maybe ErrMsg)
 isFinished = IterateeG check
@@ -159,8 +170,8 @@
   check s@(EOF e) = return $ Done (Just $ fromMaybe (Err "EOF") e) s
   check s         = return $ Done Nothing s
 
--- |If the iteratee (IterGV) has finished, return its value.  If it has not
--- finished then apply it to the given EnumeratorGM.
+-- |If the iteratee ('IterGV') has finished, return its value.  If it has not
+-- finished then apply it to the given 'EnumeratorGM'.
 -- If in error, throw the error.
 checkIfDone :: (SC.StreamChunk s el, Monad m) =>
                (IterateeG s el m a -> m (IterateeG s el m a)) ->
@@ -252,6 +263,9 @@
 
 -- |Check if an iteratee produces an error.
 -- Returns 'Right a' if it completes without errors, otherwise 'Left ErrMsg'
+-- checkErr is useful for iteratees that may not terminate, such as 'head'
+-- with an empty stream.  In particular, it enables them to be used with
+-- 'convStream'.
 checkErr :: (Monad m, SC.StreamChunk s el) =>
               IterateeG s el m a ->
               IterateeG s el m (Either ErrMsg a)
@@ -290,7 +304,7 @@
 
 -- |The identity iterator.  Doesn't do anything.
 identity :: (Monad m) => IterateeG s el m ()
-identity = IterateeG (return . Done ())
+identity = return ()
 
 
 -- |Attempt to read the next element of the stream and return it
@@ -368,6 +382,18 @@
   step (Chunk str)       = return $ Done () (Chunk (LL.drop n str))
   step stream            = return $ Done () stream
 
+
+-- |Return the total length of the stream
+length :: (Num a, LL.ListLike (s el) el, Monad m) => IterateeG s el m a
+length = length' 0
+  where
+  length' = IterateeG . step
+  step i (Chunk xs) = return $ Cont
+                               (length' $! i + fromIntegral (LL.length xs))
+                               Nothing
+  step i stream     = return $ Done i stream
+
+
 -- ---------------------------------------------------
 -- The converters show a different way of composing two iteratees:
 -- `vertical' rather than `horizontal'
@@ -464,6 +490,55 @@
   docase (Cont _ (Just e)) = return $ throwErr e
 
 {-# SPECIALIZE convStream :: IterateeG s el IO (Maybe (s' el')) -> EnumeratorN s el s' el' IO a #-}
+
+
+-- |Creates an enumerator with only elements from the stream that
+-- satisfy the predicate function.
+filter :: (LL.ListLike (s el) el, Monad m) =>
+          (el -> Bool) ->
+          EnumeratorN s el s el m a
+filter p = convStream f'
+  where
+  f' = IterateeG step
+  step (Chunk xs) | LL.null xs = return $ Cont f' Nothing
+  step (Chunk xs) = return $ Done (Just $ LL.filter p xs) mempty
+  step stream     = return $ Done Nothing stream
+
+-- ------------------------------------------------------------------------
+-- Folds
+
+-- | Left-associative fold.
+foldl :: (LL.ListLike (s el) el, FLL.FoldableLL (s el) el, Monad m) =>
+         (a -> el -> a) ->
+         a ->
+         IterateeG s el m a
+foldl f i = IterateeG step
+  where
+  step (Chunk xs) | LL.null xs = return $ Cont (foldl f i) Nothing
+  step (Chunk xs) = return $ Cont (foldl f (FLL.foldl f i xs)) Nothing
+  step stream     = return $ Done i stream
+
+-- | Left-associative fold that is strict in the accumulator.
+foldl' :: (LL.ListLike (s el) el, FLL.FoldableLL (s el) el, Monad m) =>
+          (a -> el -> a) ->
+          a ->
+          IterateeG s el m a
+foldl' f i = IterateeG step
+  where
+  step (Chunk xs) | LL.null xs = return $ Cont (foldl' f i) Nothing
+  step (Chunk xs) = return $ Cont (foldl' f $! FLL.foldl' f i xs) Nothing
+  step stream     = return $ Done i stream
+
+-- | Variant of foldl with no base case.  Requires at least one element
+--   in the stream.
+foldl1 :: (LL.ListLike (s el) el, FLL.FoldableLL (s el) el, Monad m) =>
+          (el -> el -> el) ->
+          IterateeG s el m el
+foldl1 f = IterateeG step
+  where
+  step (Chunk xs) | LL.null xs = return $ Cont (foldl1 f) Nothing
+  step (Chunk xs) = return $ Cont (foldl f (FLL.foldl1 f xs)) Nothing
+  step stream     = return $ Cont (foldl1 f) (Just (setEOF stream))
 
 -- ------------------------------------------------------------------------
 -- Enumerators
diff --git a/src/Data/Iteratee/Codecs/Wave.hs b/src/Data/Iteratee/Codecs/Wave.hs
--- a/src/Data/Iteratee/Codecs/Wave.hs
+++ b/src/Data/Iteratee/Codecs/Wave.hs
@@ -27,6 +27,7 @@
 )
 where
 
+import Prelude as P
 import Data.Iteratee.Base
 import qualified Data.Iteratee.Base as Iter
 import Data.Iteratee.Binary
@@ -80,7 +81,7 @@
 waveChunk str
   | str == "fmt " = Just WAVE_FMT
   | str == "data" = Just WAVE_DATA
-  | length str == 4 = Just $ WAVE_OTHER str
+  | P.length str == 4 = Just $ WAVE_OTHER str
   | otherwise = Nothing
 
 -- |Convert a WAVE_CHUNK to the representative string
@@ -154,7 +155,7 @@
 
 loadDict :: [(Int, WAVE_CHUNK, Int)] ->
                IterateeG L Word8 IO (Maybe WAVEDict)
-loadDict = foldl read_entry (return (Just IM.empty))
+loadDict = P.foldl read_entry (return (Just IM.empty))
   where
   read_entry dictM (offset, typ, count) = dictM >>=
     maybe (return Nothing) (\dict -> do
diff --git a/tests/QCUtils.hs b/tests/QCUtils.hs
new file mode 100644
--- /dev/null
+++ b/tests/QCUtils.hs
@@ -0,0 +1,42 @@
+{-# LANGUAGE FlexibleInstances, FlexibleContexts #-}
+
+module QCUtils where
+
+import Test.QuickCheck
+import Test.QuickCheck.Arbitrary
+import Test.QuickCheck.Gen
+
+import Data.Iteratee
+import qualified Data.Iteratee as I
+import Data.Iteratee.Base.StreamChunk (StreamChunk)
+import Control.Monad.Identity
+
+-- Show instance
+instance (Show a, StreamChunk s el) => Show (IterateeG s el Identity a) where
+  show = (++) "<<Iteratee>> " . show . runIdentity . run
+
+-- Arbitrary instances
+
+instance Arbitrary ErrMsg where
+  arbitrary = do
+    err <- arbitrary
+    n <- arbitrary :: Gen Int
+    elements [Err err, Seek (fromIntegral n)]
+
+instance Arbitrary (c el) => Arbitrary (StreamG c el) where
+  arbitrary = do
+    err <- arbitrary
+    xs <- arbitrary
+    elements [EOF err, Chunk xs]
+
+instance (Num a, Ord a, Arbitrary a, Monad m) => Arbitrary (IterateeG [] a m [a]) where
+  arbitrary = do
+    n <- suchThat arbitrary (>0)
+    ns <- arbitrary
+    elements [
+              I.drop n >> stream2list
+              ,I.break (< 5)
+              ,I.heads ns >> stream2list
+              ,I.peek >> stream2list
+              ]
+
diff --git a/tests/testIteratee.hs b/tests/testIteratee.hs
new file mode 100644
--- /dev/null
+++ b/tests/testIteratee.hs
@@ -0,0 +1,297 @@
+{-# OPTIONS_GHC -O #-}
+{-# LANGUAGE NoMonomorphismRestriction #-}
+
+import QCUtils
+
+import Test.Framework (defaultMain, testGroup)
+import Test.Framework.Providers.QuickCheck2 (testProperty)
+
+import Test.QuickCheck
+
+import Data.Iteratee hiding (head, break)
+import qualified Data.Iteratee.Char as IC
+import qualified Data.Iteratee as Iter
+import qualified Data.Iteratee.Base.StreamChunk as SC
+import Control.Monad.Identity
+import Data.Monoid
+import qualified Data.ListLike as LL
+
+import Text.Printf (printf)
+import System.Environment (getArgs)
+
+instance Show (a -> b) where
+  show _ = "<<function>>"
+
+-- ---------------------------------------------
+-- StreamG instances
+
+type ST = StreamG [] Int
+
+prop_eq str = str == str
+  where types = (str :: ST)
+
+prop_mempty = mempty == (Chunk [] :: StreamG [] Int)
+
+prop_mappend str1 str2 | isChunk str1 && isChunk str2 =
+  str1 `mappend` str2 == Chunk (chunkData str1 ++ chunkData str2)
+prop_mappend str1 str2 = isEOF $ str1 `mappend` str2
+  where types = (str1 :: ST, str2 :: ST)
+
+prop_functor str@(EOF _) f = isEOF $ fmap f str
+prop_functor str@(Chunk xs) f = fmap f str == Chunk (fmap f xs)
+  where types = (str :: ST, f :: Int -> Integer)
+
+prop_mappend2 str = str `mappend` mempty == mempty `mappend` str
+  where types = (str :: ST)
+
+
+isChunk (Chunk _) = True
+isChunk (EOF _)   = False
+
+chunkData (Chunk xs) = xs
+
+isEOF (EOF _)   = True
+isEOF (Chunk _) = False
+
+-- ---------------------------------------------
+-- Iteratee instances
+
+runner0 = runIdentity . Iter.run
+runner1 = runIdentity . Iter.run . runIdentity
+
+prop_iterFmap xs f a = runner1 (enumPure1Chunk xs (fmap f $ return a))
+                     == runner1 (enumPure1Chunk xs (return $ f a))
+  where types = (xs :: [Int], f :: Int -> Int, a :: Int)
+
+prop_iterFmap2 xs f i = runner1 (enumPure1Chunk xs (fmap f i))
+                      == f (runner1 (enumPure1Chunk xs i))
+  where types = (xs :: [Int], i :: I, f :: [Int] -> [Int])
+
+prop_iterMonad1 xs a f = runner1 (enumPureNChunk xs 1 (return a >>= f))
+                       == runner1 (enumPure1Chunk xs (f a))
+  where types = (xs :: [Int], a :: Int, f :: Int -> I)
+
+prop_iterMonad2 m xs = runner1 (enumPureNChunk xs 1 (m >>= return))
+                     == runner1 (enumPure1Chunk xs m)
+  where types = (xs :: [Int], m :: I)
+
+prop_iterMonad3 m f g xs = runner1 (enumPureNChunk xs 1 ((m >>= f) >>= g))
+                         == runner1 (enumPure1Chunk xs (m >>= (\x -> f x >>= g)))
+  where types = (xs :: [Int], m :: I, f :: [Int] -> I, g :: [Int] -> I)
+
+-- ---------------------------------------------
+-- List <-> Stream
+
+prop_list xs = runner1 (enumPure1Chunk xs stream2list) == xs
+  where types = xs :: [Int]
+
+prop_clist xs n = n > 0 ==> runner1 (enumPureNChunk xs n stream2list) == xs
+  where types = xs :: [Int]
+
+prop_break f xs = runner1 (enumPure1Chunk xs (Iter.break f)) == fst (break f xs)
+  where types = xs :: [Int]
+
+prop_break2 f xs = runner1 (enumPure1Chunk xs (Iter.break f >> stream2list)) == snd (break f xs)
+  where types = xs :: [Int]
+
+prop_head xs = length xs > 0 ==> runner1 (enumPure1Chunk xs Iter.head) == head xs
+  where types = xs :: [Int]
+
+prop_head2 xs = length xs > 0 ==> runner1 (enumPure1Chunk xs (Iter.head >> stream2list)) == tail xs
+  where types = xs :: [Int]
+
+prop_heads xs = runner1 (enumPure1Chunk xs $ heads xs) == length xs
+  where types = xs :: [Int]
+
+prop_heads2 xs = runner1 (enumPure1Chunk xs $ heads [] >>= \c ->
+                          stream2list >>= \s -> return (c,s))
+                 == (0, xs)
+  where types = xs :: [Int]
+
+prop_peek xs = runner1 (enumPure1Chunk xs peek) == sHead xs
+  where
+  types = xs :: [Int]
+  sHead [] = Nothing
+  sHead (x:_) = Just x
+
+prop_peek2 xs = runner1 (enumPure1Chunk xs (peek >> stream2list)) == xs
+  where types = xs :: [Int]
+
+prop_skip xs = runner1 (enumPure1Chunk xs (skipToEof >> stream2list)) == []
+  where types = xs :: [Int]
+
+-- ---------------------------------------------
+-- Simple enumerator tests
+
+type I = IterateeG [] Int Identity [Int]
+
+prop_enumChunks n xs i = n > 0  ==>
+  runner1 (enumPure1Chunk xs i) == runner1 (enumPureNChunk xs n i)
+  where types = (n :: Int, xs :: [Int], i :: I)
+
+prop_app1 xs ys i = runner1 (enumPure1Chunk ys (joinIM $ enumPure1Chunk xs i))
+                    == runner1 (enumPure1Chunk (xs ++ ys) i)
+  where types = (xs :: [Int], ys :: [Int], i :: I)
+
+prop_app2 xs ys = runner1 ((enumPure1Chunk xs >. enumPure1Chunk ys) stream2list)
+                  == runner1 (enumPure1Chunk (xs ++ ys) stream2list)
+  where types = (xs :: [Int], ys :: [Int])
+
+prop_app3 xs ys i = runner1 ((enumPure1Chunk xs >. enumPure1Chunk ys) i)
+                    == runner1 (enumPure1Chunk (xs ++ ys) i)
+  where types = (xs :: [Int], ys :: [Int], i :: I)
+
+prop_eof xs ys i = runner1 (enumPure1Chunk ys $ runIdentity $
+                           (enumPure1Chunk xs >. enumEof) i)
+                 == runner1 (enumPure1Chunk xs i)
+  where types = (xs :: [Int], ys :: [Int], i :: I)
+
+prop_isFinished = runner1 (enumEof (isFinished :: IterateeG [] Int Identity (Maybe ErrMsg))) == Just (Err "EOF")
+
+prop_isFinished2 = runner1 (enumErr "Error" (isFinished :: IterateeG [] Int Identity (Maybe ErrMsg))) == Just (Err "Error")
+
+prop_null xs i = runner1 (enumPure1Chunk xs =<< enumPure1Chunk [] i)
+                 == runner1 (enumPure1Chunk xs i)
+  where types = (xs :: [Int], i :: I)
+
+prop_nullH xs = length xs > 0 ==>
+                runner1 (enumPure1Chunk xs =<< enumPure1Chunk [] Iter.head)
+                == runner1 (enumPure1Chunk xs Iter.head)
+  where types = (xs :: [Int])
+-- ---------------------------------------------
+-- Nested Iteratees
+
+-- take, mapStream, convStream, and takeR
+
+runner2 = runIdentity . run . runner1
+
+prop_mapStream xs i = runner2 (enumPure1Chunk xs $ mapStream id i)
+                      == runner1 (enumPure1Chunk xs i)
+  where types = (i :: I, xs :: [Int])
+
+prop_mapStream2 xs n i = n > 0 ==>
+                         runner2 (enumPureNChunk xs n $ mapStream id i)
+                         == runner1 (enumPure1Chunk xs i)
+  where types = (i :: I, xs :: [Int])
+
+prop_mapjoin xs i =
+  runIdentity (run (joinI . runIdentity $ enumPure1Chunk xs $ mapStream id i))
+  == runner1 (enumPure1Chunk xs i)
+  where types = (i :: I, xs :: [Int])
+
+
+convId :: (SC.StreamChunk s el, Monad m) => IterateeG s el m (Maybe (s el))
+convId = IterateeG (\str -> case str of
+  s@(Chunk xs) | LL.null xs -> return $ Cont convId Nothing
+  s@(Chunk xs) -> return $ Done (Just xs) (Chunk mempty)
+  s@(EOF e)   -> return $ Done Nothing (EOF e)
+  )
+
+prop_convId xs = runner1 (enumPure1Chunk xs convId) == Just xs
+  where types = xs :: [Int]
+
+prop_convstream xs i = length xs > 0 ==>
+                       runner2 (enumPure1Chunk xs $ convStream convId i)
+                       == runner1 (enumPure1Chunk xs i)
+  where types = (xs :: [Int], i :: I)
+
+prop_convstream2 xs = length xs > 0 ==>
+                      runner2 (enumPure1Chunk xs $ convStream convId Iter.head)
+                      == runner1 (enumPure1Chunk xs Iter.head)
+  where types = (xs :: [Int])
+
+prop_convstream3 xs = length xs > 0 ==>
+                      runner2 (enumPure1Chunk xs $ convStream convId stream2list)
+                      == runner1 (enumPure1Chunk xs stream2list)
+  where types = (xs :: [Int])
+
+prop_take xs n = n >= 0 ==>
+                 runner2 (enumPure1Chunk xs $ Iter.take n stream2list)
+                 == runner1 (enumPure1Chunk (Prelude.take n xs) stream2list)
+  where types = (xs :: [Int])
+
+prop_take2 xs n = n > 0 ==>
+                  runner2 (enumPure1Chunk xs $ Iter.take n peek)
+                  == runner1 (enumPure1Chunk (Prelude.take n xs) peek)
+  where types = (xs :: [Int])
+
+prop_takeR xs n = n >= 0 ==>
+                  runner2 (enumPure1Chunk xs $ Iter.take n stream2list)
+                  == runner2 (enumPure1Chunk xs $ takeR n stream2list)
+  where types = (xs :: [Int])
+
+-- ---------------------------------------------
+-- Data.Iteratee.Char
+
+{-
+-- this isn't true, since lines "\r" returns ["\r"], and IC.line should
+-- return Right "".  Not sure what a real test would be...
+prop_line xs = length xs > 0 ==>
+               fromEither (runner1 (enumPure1Chunk xs $ IC.line))
+               == head (lines xs)
+  where
+  types = xs :: [Char]
+  fromEither (Left l)  = l
+  fromEither (Right l) = l
+-}
+
+-- ---------------------------------------------
+tests = [
+  testGroup "Elementary" [
+    testProperty "list" prop_list
+    ,testProperty "chunkList" prop_clist]
+  ,testGroup "StreamG tests" [
+    testProperty "mempty" prop_mempty
+    ,testProperty "mappend" prop_mappend
+    ,testProperty "mappend associates" prop_mappend2
+    ,testProperty "functor" prop_functor
+    ,testProperty "eq" prop_eq
+  ]
+  ,testGroup "Simple Iteratees" [
+    testProperty "break" prop_break
+    ,testProperty "break remaineder" prop_break2
+    ,testProperty "head" prop_head
+    ,testProperty "head remainder" prop_head2
+    ,testProperty "heads" prop_heads
+    ,testProperty "null heads" prop_heads2
+    ,testProperty "peek" prop_peek
+    ,testProperty "peek2" prop_peek2
+    ,testProperty "skipToEof" prop_skip
+    ,testProperty "iteratee Functor 1" prop_iterFmap
+    ,testProperty "iteratee Functor 2" prop_iterFmap2
+    ,testProperty "iteratee Monad LI" prop_iterMonad1
+    ,testProperty "iteratee Monad RI" prop_iterMonad2
+    ,testProperty "iteratee Monad Assc" prop_iterMonad3
+    ]
+  ,testGroup "Simple Enumerators/Combinators" [
+    testProperty "enumPureNChunk" prop_enumChunks
+    ,testProperty "enum append 1" prop_app1
+    ,testProperty "enum sequencing" prop_app2
+    ,testProperty "enum sequencing 2" prop_app3
+    ,testProperty "enumEof" prop_eof
+    ,testProperty "isFinished" prop_isFinished
+    ,testProperty "isFinished error" prop_isFinished2
+    ,testProperty "null data idempotence" prop_null
+    ,testProperty "null data head idempotence" prop_nullH
+    ]
+  ,testGroup "Nested iteratees" [
+    testProperty "mapStream identity" prop_mapStream
+    ,testProperty "mapStream identity 2" prop_mapStream2
+    ,testProperty "mapStream identity joinI" prop_mapjoin
+    ,testProperty "take" prop_take
+    ,testProperty "take (finished iteratee)" prop_take2
+    ,testProperty "takeR" prop_takeR
+    ,testProperty "convStream EOF" prop_convstream2
+    ,testProperty "convStream identity" prop_convstream
+    ,testProperty "convStream identity 2" prop_convstream3
+    ]
+  ,testGroup "Data.Iteratee.Char" [
+    --testProperty "line" prop_line
+    ]
+  ]
+
+------------------------------------------------------------------------
+-- The entry point
+
+main = defaultMain tests
+
