diff --git a/changelog.md b/changelog.md
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,14 @@
+# Version 0.4.4
+
+* Depend on `streaming-commons` instead of `zlib` and `zlib-bindings`,
+  as the latter are deprecated.
+
+* Add `Pipes.Zlib.decompress'` and `Pipes.GZip.decompress'`.
+
+* Bump upper bound dependency on `transformers`.
+
+* Add tests.
+
 # Version 0.4.3
 
 * Fix usage of the `Producer'` type synonym (#14).
diff --git a/pipes-zlib.cabal b/pipes-zlib.cabal
--- a/pipes-zlib.cabal
+++ b/pipes-zlib.cabal
@@ -1,5 +1,5 @@
 name:               pipes-zlib
-version:            0.4.3
+version:            0.4.4
 license:            BSD3
 license-file:       LICENSE
 Copyright:          Copyright (c) Paolo Capriotti 2012,
@@ -24,10 +24,30 @@
     hs-source-dirs:  src
     exposed-modules: Pipes.Zlib
                      Pipes.GZip
-    build-depends:   base             (>= 4.5 && < 5.0)
-                   , transformers     (>= 0.2 && < 0.5)
-                   , pipes            (>= 4.0 && < 4.2)
-                   , bytestring       (>= 0.9.2.1)
-                   , zlib             (>= 0.5 && < 0.7)
-                   , zlib-bindings    (>= 0.1 && < 0.2)
+    build-depends:   base >= 4.5 && < 5.0
+                   , transformers >= 0.2 && < 0.6
+                   , pipes >= 4.0 && < 4.2
+                   , bytestring >= 0.9.2.1
+                   , streaming-commons >= 0.1.15 && < 0.2
     ghc-options: -Wall -O2
+
+test-suite test
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      test
+  main-is:             Main.hs
+  build-depends:       base
+                     , pipes-zlib
+                     , pipes
+                     , bytestring
+                     , HUnit
+                     , QuickCheck
+                     , quickcheck-instances
+                     , tasty
+                     , tasty-quickcheck
+                     , tasty-hunit
+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N
+  default-language:    Haskell2010
+
+source-repository head
+  type:     git
+  location: https://github.com/githubuser/twio
diff --git a/src/Pipes/GZip.hs b/src/Pipes/GZip.hs
--- a/src/Pipes/GZip.hs
+++ b/src/Pipes/GZip.hs
@@ -6,18 +6,19 @@
 module Pipes.GZip (
   -- * Streams
     decompress
+  , decompress'
   , compress
 
   -- * Compression level
-  -- $ccz-re-export
-  , ZC.defaultCompression
-  , ZC.noCompression
-  , ZC.bestSpeed
-  , ZC.bestCompression
-  , ZC.compressionLevel
+  , Pipes.Zlib.CompressionLevel
+  , Pipes.Zlib.defaultCompression
+  , Pipes.Zlib.noCompression
+  , Pipes.Zlib.bestSpeed
+  , Pipes.Zlib.bestCompression
+  , Pipes.Zlib.compressionLevel
   ) where
 
-import qualified Codec.Compression.Zlib    as ZC
+import qualified Data.Streaming.Zlib       as Zlib
 import qualified Data.ByteString           as B
 import           Pipes
 import qualified Pipes.Zlib
@@ -35,11 +36,21 @@
   :: MonadIO m
   => Proxy x' x () B.ByteString m r -- ^ Compressed stream
   -> Proxy x' x () B.ByteString m r -- ^ Decompressed stream
-  -- -> Producer' B.ByteString m r -- ^ Decompressed stream
-decompress = Pipes.Zlib.decompress (ZC.WindowBits 31)
+decompress = Pipes.Zlib.decompress gzWindowBits
 {-# INLINABLE decompress #-}
 
+-- | Decompress bytes flowing from a 'Producer', returning any leftover input
+-- that follows the compressed stream.
+decompress'
+  :: MonadIO m
+  => Producer B.ByteString m r -- ^ Compressed stream
+  -> Producer B.ByteString m (Either (Producer B.ByteString m r) r)
+     -- ^ Decompressed stream, returning either a 'Producer' of the leftover input
+     -- or the return value from the input 'Producer'.
+decompress' = Pipes.Zlib.decompress' gzWindowBits
+{-# INLINABLE decompress' #-}
 
+
 -- | Compress bytes flowing from a 'Producer'.
 --
 -- @
@@ -50,8 +61,11 @@
 -- @
 compress
   :: MonadIO m
-  => ZC.CompressionLevel
+  => Pipes.Zlib.CompressionLevel
   -> Proxy x' x () B.ByteString m r -- ^ Decompressed stream
   -> Proxy x' x () B.ByteString m r -- ^ Compressed stream
-compress clevel = Pipes.Zlib.compress clevel (ZC.WindowBits 31)
+compress clevel = Pipes.Zlib.compress clevel gzWindowBits
 {-# INLINABLE compress #-}
+
+gzWindowBits :: Zlib.WindowBits
+gzWindowBits = Zlib.WindowBits 31
diff --git a/src/Pipes/Zlib.hs b/src/Pipes/Zlib.hs
--- a/src/Pipes/Zlib.hs
+++ b/src/Pipes/Zlib.hs
@@ -1,4 +1,5 @@
 {-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE ScopedTypeVariables #-}
 
 -- | This module exports utilities to compress and decompress @pipes@ streams
 -- using the zlib compression codec.
@@ -9,24 +10,25 @@
 module Pipes.Zlib (
   -- * Streams
     decompress
+  , decompress'
   , compress
 
-  -- * Compression level
-  -- $ccz-re-export
-  , ZC.defaultCompression
-  , ZC.noCompression
-  , ZC.bestSpeed
-  , ZC.bestCompression
-  , ZC.compressionLevel
+  -- * Compression levels
+  , CompressionLevel
+  , defaultCompression
+  , noCompression
+  , bestSpeed
+  , bestCompression
+  , compressionLevel
 
   -- * Window size
   -- $ccz-re-export
-  , ZC.defaultWindowBits
-  , ZC.windowBits
+  , Z.defaultWindowBits
+  , windowBits
   ) where
 
-import qualified Codec.Compression.Zlib    as ZC
-import qualified Codec.Zlib                as Z
+import           Data.Streaming.Zlib       as Z
+import           Control.Exception         (throwIO)
 import           Control.Monad             (unless)
 import qualified Data.ByteString           as B
 import           Pipes
@@ -39,13 +41,13 @@
 --
 -- @
 -- 'decompress' :: 'MonadIO' m
---            => 'ZC.WindowBits'
+--            => 'Z.WindowBits'
 --            => 'Producer' 'B.ByteString' m r
 --            -> 'Producer' 'B.ByteString' m r
 -- @
 decompress
   :: MonadIO m
-  => ZC.WindowBits
+  => Z.WindowBits
   -> Proxy x' x () B.ByteString m r -- ^ Compressed stream
   -> Proxy x' x () B.ByteString m r -- ^ Decompressed stream
 decompress wbits p0 = do
@@ -58,27 +60,52 @@
     return r
 {-# INLINABLE decompress #-}
 
+-- | Decompress bytes flowing from a 'Producer', returning any leftover input
+-- that follows the compressed stream.
+decompress'
+  :: MonadIO m
+  => Z.WindowBits
+  -> Producer B.ByteString m r -- ^ Compressed stream
+  -> Producer B.ByteString m (Either (Producer B.ByteString m r) r)
+     -- ^ Decompressed stream, ending with either leftovers or a result
+decompress' wbits p0 = go p0 =<< liftIO (Z.initInflate wbits)
+  where
+    flush inf = do
+      bs <- liftIO $ Z.flushInflate inf
+      unless (B.null bs) (yield bs)
+    go p inf = do
+      res <- lift (next p)
+      case res of
+         Left r -> return $ Right r
+         Right (bs, p') -> do
+            fromPopper =<< liftIO (Z.feedInflate inf bs)
+            flush inf
+            leftover <- liftIO $ Z.getUnusedInflate inf
+            if B.null leftover
+               then go p' inf
+               else return $ Left (yield leftover >> p')
+{-# INLINABLE decompress' #-}
 
 -- | Compress bytes flowing from a 'Producer'.
 --
 -- See the "Codec.Compression.Zlib" module for details about
--- 'ZC.CompressionLevel' and 'ZC.WindowBits'.
+-- 'Z.CompressionLevel' and 'Z.WindowBits'.
 --
 -- @
 -- 'compress' :: 'MonadIO' m
---          => 'ZC.CompressionLevel'
---          -> 'ZC.WindowBits'
+--          => 'Z.CompressionLevel'
+--          -> 'Z.WindowBits'
 --          -> 'Producer' 'B.ByteString' m r
 --          -> 'Producer' 'B.ByteString' m r
 -- @
 compress
   :: MonadIO m
-  => ZC.CompressionLevel
-  -> ZC.WindowBits
+  => CompressionLevel
+  -> Z.WindowBits
   -> Proxy x' x () B.ByteString m r -- ^ Decompressed stream
   -> Proxy x' x () B.ByteString m r -- ^ Compressed stream
-compress clevel wbits p0 = do
-    def <- liftIO $ Z.initDeflate (fromCompressionLevel clevel) wbits
+compress (CompressionLevel clevel) wbits p0 = do
+    def <- liftIO $ Z.initDeflate clevel wbits
     r <- for p0 $ \bs -> do
        popper <- liftIO (Z.feedDeflate def bs)
        fromPopper popper
@@ -94,26 +121,40 @@
 -- convenience.
 
 --------------------------------------------------------------------------------
+-- Compression Levels
+
+-- | How hard should we try to compress?
+newtype CompressionLevel = CompressionLevel Int
+                         deriving (Show, Read, Eq, Ord)
+
+defaultCompression, noCompression, bestSpeed, bestCompression :: CompressionLevel
+defaultCompression = CompressionLevel (-1)
+noCompression      = CompressionLevel 0
+bestSpeed          = CompressionLevel 1
+bestCompression    = CompressionLevel 9
+
+-- | A specific compression level between 0 and 9.
+compressionLevel :: Int -> CompressionLevel
+compressionLevel n
+  | n >= 0 && n <= 9 = CompressionLevel n
+  | otherwise        = error "CompressionLevel must be in the range 0..9"
+
+windowBits :: Int -> WindowBits
+windowBits = WindowBits
+
+--------------------------------------------------------------------------------
 -- Internal stuff
 
 -- | Produce values from the given 'Z.Popper' until exhausted.
-fromPopper :: MonadIO m => Z.Popper -> Producer' B.ByteString m ()
-fromPopper pop = loop where
+fromPopper :: MonadIO m
+           => Z.Popper
+           -> Producer' B.ByteString m ()
+fromPopper pop = loop
+  where
     loop = do
       mbs <- liftIO pop
       case mbs of
-         Nothing -> return ()
-         Just bs -> yield bs >> loop
+          PRDone     -> return ()
+          PRError e  -> liftIO $ throwIO e
+          PRNext bs  -> yield bs >> loop
 {-# INLINABLE fromPopper #-}
-
--- We need this function until the @zlib@ library hides the
--- 'ZC.CompressionLevel' constructors in future version 0.7.
-fromCompressionLevel :: ZC.CompressionLevel -> Int
-fromCompressionLevel level = case level of
-    ZC.DefaultCompression   -> -1
-    ZC.NoCompression        -> 0
-    ZC.BestSpeed            -> 1
-    ZC.BestCompression      -> 9
-    ZC.CompressionLevel n
-         | n >= 0 && n <= 9 -> fromIntegral n
-    _  -> error "CompressLevel must be in the range 1..9"
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,73 @@
+{-# LANGUAGE OverloadedStrings #-}
+import           Control.Arrow
+import qualified Data.ByteString.Char8 as B8
+import           Data.List
+import           Data.Ord
+import           Test.Tasty
+import           Test.Tasty.QuickCheck as QC
+import           Test.QuickCheck.Instances ()
+import           Test.Tasty.HUnit
+
+import qualified Pipes as P
+import qualified Pipes.Prelude as P
+import qualified Pipes.Zlib as PZ
+import qualified Pipes.GZip as PGZ
+
+main = defaultMain tests
+
+--------------------------------------------------------------------------------
+tests :: TestTree
+tests = testGroup "Tests" [properties, unitTests]
+
+properties :: TestTree
+properties = testGroup "Properties" [qcProps]
+
+qcProps = testGroup "(checked by QuickCheck)"
+ [ QC.testProperty "id == decompress . compress" $ \bs -> QC.ioProperty $ do
+     let pc = PZ.compress PZ.defaultCompression PZ.defaultWindowBits (P.yield bs)
+         pd = PZ.decompress PZ.defaultWindowBits pc
+     bs' <- B8.concat <$> P.toListM pd
+     return (bs QC.=== bs')
+
+ , QC.testProperty "id == decompress' . compress" $ \bs bsl -> QC.ioProperty $ do
+     let pc = PZ.compress PZ.defaultCompression PZ.defaultWindowBits (P.yield bs)
+         pd = PZ.decompress' PZ.defaultWindowBits (pc >> P.yield bsl)
+     (bs', elr) <- first B8.concat <$> P.toListM' pd
+     case elr of
+        Left pl -> do
+           bsl' <- B8.concat <$> P.toListM pl
+           return $ (bs QC.=== bs') QC..&&. (bsl QC.=== bsl')
+        Right () -> do
+           return $ (bs QC.=== bs') QC..&&. (bsl QC.=== B8.empty)
+ ]
+
+unitTests = testGroup "Unit tests"
+  [ testCase "Zlib compression default" $ do
+      let pc = PZ.compress PZ.defaultCompression PZ.defaultWindowBits
+                   (P.yield bsUncompressed)
+      bs <- B8.concat <$> P.toListM pc
+      bs @?= bsCompressedZlibDefault
+  , testCase "Zlib decompression default" $ do
+      let pd = PZ.decompress PZ.defaultWindowBits
+                   (P.yield bsCompressedZlibDefault)
+      bs <- B8.concat <$> P.toListM pd
+      bs @?= bsUncompressed
+  , testCase "GZip compression default" $ do
+      let pd = PGZ.compress PGZ.defaultCompression (P.yield bsUncompressed)
+      bs <- B8.concat <$> P.toListM pd
+      bs @?= bsCompressedGZipDefault
+  , testCase "GZip decompression default" $ do
+      let pd = PGZ.decompress (P.yield bsCompressedGZipDefault)
+      bs <- B8.concat <$> P.toListM pd
+      bs @?= bsUncompressed
+  ]
+
+bsUncompressed :: B8.ByteString
+bsUncompressed = "foo"
+
+bsCompressedZlibDefault :: B8.ByteString
+bsCompressedZlibDefault = "x\156K\203\207\a\NUL\STX\130\SOHE"
+
+bsCompressedGZipDefault :: B8.ByteString
+bsCompressedGZipDefault =
+  "\US\139\b\NUL\NUL\NUL\NUL\NUL\NUL\ETXK\203\207\a\NUL!es\140\ETX\NUL\NUL\NUL"
