diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,42 @@
+herringbone
+===========
+
+herringbone is a Haskell library for compiling and serving web assets.  It
+aims to make it dead simple to create a 'Network.Wai.Middleware' or
+'Network.Wai.Application' which deals with all of your static assets, including
+preprocessing for languages like Fay, CoffeeScript, Sass, and LESS.
+
+It takes most of its inspiration from the Ruby library, [Sprockets], hence the
+name.
+
+Status
+------
+
+Alpha.
+
+How to use it
+-------------
+
+```haskell
+import Web.Herringbone
+
+fay, sass :: PP
+
+hb = Herringbone
+hb = herringbone
+    ( addSourceDir "assets"
+    . setDestDir   "compiled_assets"
+    . addPreprocessors [fay, sass]
+    )
+
+-- You can now access assets programmatically
+asset <- findAsset hb (fromJust . makeLogicalPath $ ["application.js"])
+
+-- Or serve them with a Wai application
+app = toApplication hb
+```
+
+For more information, go and look at the documentation on [Hackage]!
+
+[Sprockets]: https://github.com/sstephenson/sprockets
+[Hackage]: http://hackage.haskell.org/package/herringbone
diff --git a/herringbone.cabal b/herringbone.cabal
--- a/herringbone.cabal
+++ b/herringbone.cabal
@@ -1,5 +1,5 @@
 name:                herringbone
-version:             0.0.6
+version:             0.0.7
 synopsis:            A library for compiling and serving static web assets.
 description:         A library for compiling and serving static web assets. For more information, please see <https://github.com/hdgarrood/herringbone>.
 license:             MIT
@@ -9,6 +9,9 @@
 category:            Web
 build-type:          Simple
 cabal-version:       >=1.10
+extra-source-files:
+    README.md
+    test/*.hs
 
 source-repository head
   type:     git
@@ -83,8 +86,8 @@
   default-language:     Haskell2010
 
 executable herringbone-test-server
-  hs-source-dirs:   test
-  build-depends:    base, herringbone, text, warp
-  main-is:          TestServer.hs
-  default-language: Haskell2010
-  default-extensions: OverloadedStrings
+  hs-source-dirs:       test
+  build-depends:        base, herringbone, text, warp
+  main-is:              TestServer.hs
+  default-language:     Haskell2010
+  default-extensions:   OverloadedStrings
diff --git a/test/AssertionsHelper.hs b/test/AssertionsHelper.hs
new file mode 100644
--- /dev/null
+++ b/test/AssertionsHelper.hs
@@ -0,0 +1,63 @@
+module AssertionsHelper where
+
+import Control.Applicative
+import Control.Monad
+import Data.Text (Text)
+import Data.List (sort)
+import Test.HUnit hiding (path)
+import Prelude hiding (FilePath)
+import Filesystem.Path.CurrentOS (FilePath, (</>))
+import Prelude hiding (FilePath)
+import qualified Filesystem as F
+
+import LazinessHelper
+import TestHerringbone
+import Web.Herringbone
+
+testWithInputs :: String -> (a -> Assertion) -> [a] -> Test
+testWithInputs groupName f =
+    TestLabel groupName . TestList . map mkTest . zipWith assignName ([1,2..] :: [Int])
+    where
+        mkTest (name, input) = TestLabel name (TestCase (f input))
+        assignName n input   = ("input #" ++ show n, input)
+
+resultsDir :: FilePath
+resultsDir = "test/resources/results"
+
+testWithExpectedResult :: Text -> Assertion
+testWithExpectedResult logicalPathText = do
+    let logicalPath = lp logicalPathText
+    let filePath = toFilePath logicalPath
+    result <- findAsset testHB logicalPath
+    either (fail . show)
+           (assertResultMatches filePath . assetFilePath)
+           result
+
+assertResultMatches :: FilePath -> FilePath -> Assertion
+assertResultMatches resultName filePath = do
+    let resultPath = resultsDir </> resultName
+    assertFileContentsMatch resultPath filePath
+
+-- Extra assertions
+assertFileExists :: FilePath -> Assertion
+assertFileExists path = do
+    exists <- F.isFile path
+    assertBool ("Expected a file to exist: " ++ es path) exists
+
+assertFileContentsMatch :: FilePath -> FilePath -> Assertion
+assertFileContentsMatch pathA pathB = do
+    matches <- (==) <$> F.readFile pathA <*> F.readFile pathB
+    assertBool ("expected file contents to match\n" ++
+                "this file:       " ++ es pathA ++ "\n" ++
+                "versus this one: " ++ es pathB ++ "\n") matches
+
+assertEqual' :: (Eq a, Show a) => a -> a -> Assertion
+assertEqual' = assertEqual ""
+
+assertSameElems :: (Eq a, Show a, Ord a) => [a] -> [a] -> Assertion
+assertSameElems xs ys = assertEqual' (sort xs) (sort ys)
+
+assertIsRight :: Show a => Either a b -> Assertion
+assertIsRight (Right _) = return ()
+assertIsRight (Left x)  = assertFailure $
+                            "Expected a Right value; got: Left " ++ show x
diff --git a/test/FindAssetSpec.hs b/test/FindAssetSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/FindAssetSpec.hs
@@ -0,0 +1,57 @@
+module FindAssetSpec where
+
+import Filesystem.Path.CurrentOS ((</>))
+import Prelude hiding (FilePath)
+import qualified Filesystem as F
+import Test.Hspec
+import Test.HUnit hiding (path)
+
+import Web.Herringbone
+import SpecHelper
+
+spec :: Spec
+spec = do
+    let destDir = hbDestDir testHB
+
+    context "without preprocessors" $ do
+        let logPath = lp "buildNoPreprocessors.js"
+
+        it "should copy a source file to the destination directory" $ do
+            asset' <- findAsset testHB logPath
+
+            assertIsRight asset'
+            let Right asset = asset'
+            assertFileExists (assetFilePath asset)
+            assertFileContentsMatch (assetSourcePath asset)
+                                    (assetFilePath asset)
+
+        it "should get the modification time of the source file" $ do
+            Right asset <- findAsset testHB logPath
+            sourceMTime <- F.getModified (assetSourcePath asset)
+
+            assertEqual' sourceMTime (assetModifiedTime asset)
+
+        it "should not compile unless necessary" $ do
+            Right asset  <- findAsset testHB logPath
+            mTime        <- F.getModified (assetFilePath asset)
+            Right asset' <- findAsset testHB logPath
+            mTime'       <- F.getModified (assetFilePath asset')
+
+            assertEqual' mTime mTime'
+
+    context "with preprocessors" $ do
+        it "should run a single preprocessor" $ do
+            testWithExpectedResult "onePreprocessor.js"
+
+        it "should run preprocessors in the correct order" $ do
+            testWithExpectedResult "threePreprocessors.js"
+
+    context "when there's a compile error" $ do
+        it "should report the error" $ do
+            Left result <- findAsset testHB (lp "compileError.css")
+            assertEqual' (AssetCompileError "Oh snap!") result
+
+        it "should not create the output file" $ do
+            _ <- findAsset testHB (lp "compileError.css")
+            exists <- F.isFile (destDir </> "compileError.css")
+            assert (not exists)
diff --git a/test/LazinessHelper.hs b/test/LazinessHelper.hs
new file mode 100644
--- /dev/null
+++ b/test/LazinessHelper.hs
@@ -0,0 +1,14 @@
+module LazinessHelper where
+
+import Web.Herringbone
+import Data.Text (Text)
+import qualified Data.Text as T
+import Prelude hiding (FilePath)
+import Filesystem.Path.CurrentOS (FilePath)
+import qualified Filesystem.Path.CurrentOS as F
+
+es :: FilePath -> String
+es = F.encodeString
+
+lp :: Text -> LogicalPath
+lp = unsafeMakeLogicalPath . T.splitOn "/"
diff --git a/test/LocateAssetsSpec.hs b/test/LocateAssetsSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/LocateAssetsSpec.hs
@@ -0,0 +1,86 @@
+module LocateAssetsSpec where
+
+import Test.Hspec
+import Test.Hspec.HUnit
+import Test.HUnit hiding (path)
+import Data.Text (Text)
+import Filesystem.Path.CurrentOS (FilePath, (</>))
+import Prelude hiding (FilePath)
+
+import Web.Herringbone.LocateAssets
+import Web.Herringbone.Types
+import SpecHelper
+
+spec :: Spec
+spec = do
+    fromHUnitTest $ testWithInputs "getExtraExtensions"
+        test_getExtraExtensions
+        data_getExtraExtensions
+
+    fromHUnitTest $ testWithInputs "resolvePPs"
+        test_resolvePPs
+        data_resolvePPs
+
+    fromHUnitTest $ testWithInputs "lookupPP"
+        test_lookupPP
+        data_lookupPP
+
+    fromHUnitTest $ testWithInputs "locateAssets"
+        test_locateAssets
+        data_locateAssets
+
+test_getExtraExtensions :: (FilePath, FilePath, Maybe [Text]) -> Assertion
+test_getExtraExtensions (pathWithExts, path, expected) =
+    assertEqual' expected (getExtraExtensions pathWithExts path)
+
+data_getExtraExtensions :: [(FilePath, FilePath, Maybe [Text])]
+data_getExtraExtensions =
+    [ ("game.js"   , "game.js.coffee"     , Just ["coffee"])
+    , ("style.css" , "game.js.coffee"     , Nothing)
+    , ("game.js"   , "game.js.coffee.erb" , Just ["erb", "coffee"])
+    ]
+
+allPPs :: PPs
+allPPs = fromList [pp1, pp2]
+
+test_resolvePPs :: (PPs, FilePath, FilePath, Maybe [PP]) -> Assertion
+test_resolvePPs (pps, assetPath, sourcePath, expected) =
+    assertEqual' expected (resolvePPs pps assetPath sourcePath)
+
+data_resolvePPs :: [(PPs, FilePath, FilePath, Maybe [PP])]
+data_resolvePPs =
+    [ (noPPs  , "test.js"   , "test.js"         , Just [])
+    , (noPPs  , "test.js"   , "test.js.pp1"     , Nothing)
+    , (allPPs , "test.js"   , "test.js.pp1"     , Just [pp1])
+    , (allPPs , "test.js"   , "test.js.pp1.pp2" , Just [pp2, pp1])
+    , (allPPs , "style.css" , "test.js.pp1"     , Nothing)
+    ]
+
+test_lookupPP :: (Text, PPs, Maybe PP) -> Assertion
+test_lookupPP (ext, pps, expected) =
+    assertEqual' expected (lookupPP ext pps)
+
+data_lookupPP :: [(Text, PPs, Maybe PP)]
+data_lookupPP =
+    [ ("sass" , noPPs  , Nothing)
+    , ("sass" , allPPs , Nothing)
+    , ("pp1"  , allPPs , Just pp1)
+    ]
+
+test_locateAssets :: (LogicalPath, [(FilePath, [PP])]) -> Assertion
+test_locateAssets (logPath, expected) = do
+    assets <- locateAssets testHB logPath
+    assertSameElems expected assets
+
+data_locateAssets :: [(LogicalPath, [(FilePath, [PP])])]
+data_locateAssets =
+    [ (lp "locateAssets.js",  [(base1 </> "locateAssets.js", [])])
+    , (lp "locateAssets.css", [ (base1 </> "locateAssets.css", [])
+                              , (base1 </> "locateAssets.css.pp1", [pp1])
+                              , (base2 </> "locateAssets.css", [])
+                              ])
+    , (lp "html/locateAssets.html", [(base1 </> "html/locateAssets.html", [])])
+    ]
+    where
+    base1 = "test/resources/assets"
+    base2 = "test/resources/assets2"
diff --git a/test/PreprocessStdIOSpec.hs b/test/PreprocessStdIOSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/PreprocessStdIOSpec.hs
@@ -0,0 +1,9 @@
+module PreprocessStdIOSpec where
+
+import Test.Hspec
+import SpecHelper
+
+spec :: Spec
+spec = do
+    it "should use stdIO" $ do
+        testWithExpectedResult "hello.txt"
diff --git a/test/SpecHelper.hs b/test/SpecHelper.hs
new file mode 100644
--- /dev/null
+++ b/test/SpecHelper.hs
@@ -0,0 +1,9 @@
+module SpecHelper (
+    module AssertionsHelper,
+    module LazinessHelper,
+    module TestHerringbone
+) where
+
+import AssertionsHelper
+import LazinessHelper
+import TestHerringbone
diff --git a/test/TestHerringbone.hs b/test/TestHerringbone.hs
new file mode 100644
--- /dev/null
+++ b/test/TestHerringbone.hs
@@ -0,0 +1,55 @@
+module TestHerringbone where
+
+import Data.Text (Text)
+import qualified Data.Text.Encoding as T
+import Data.Monoid
+import Network.Wai.Handler.Warp
+
+import Web.Herringbone
+import Web.Herringbone.Preprocessor.StdIO
+import Web.Herringbone.Preprocessor.CoffeeScript
+import Web.Herringbone.Preprocessor.Sass
+
+mkMockPP :: Text -> PP
+mkMockPP ext = PP { ppExtension = ext
+                  , ppAction = \sourceData -> do
+                        return . Right $
+                            "Preprocessed as: " <> T.encodeUtf8 ext <> "\n" <>
+                            sourceData
+                  }
+
+pp1 :: PP
+pp1 = mkMockPP "pp1"
+
+pp2 :: PP
+pp2 = mkMockPP "pp2"
+
+pp3 :: PP
+pp3 = mkMockPP "pp3"
+
+failingPP :: PP
+failingPP = PP { ppExtension = "fails"
+               , ppAction = const . return . Left $ "Oh snap!"
+               }
+
+sed :: PP
+sed = makeStdIOPP "sed" "sed" ["-e", "s/e/u/"]
+
+testHB :: Herringbone
+testHB = herringbone
+    ( addSourceDir  "test/resources/assets"
+    . addSourceDir  "test/resources/assets2"
+    . setDestDir    "test/resources/compiled_assets"
+    . addPreprocessors [ pp1
+                       , pp2
+                       , pp3
+                       , failingPP
+                       , coffeeScript
+                       , sass
+                       , scss
+                       , sed
+                       ]
+    )
+
+runTestHB :: Int -> IO ()
+runTestHB port = run port (toApplication testHB)
diff --git a/test/WaiAdapterTest.hs b/test/WaiAdapterTest.hs
new file mode 100644
--- /dev/null
+++ b/test/WaiAdapterTest.hs
@@ -0,0 +1,15 @@
+module WaiAdapterTest where
+
+import Network.Wai
+import Network.Wai.Handler.Warp
+
+import Web.Herringbone
+import SpecHelper
+
+app :: Application
+app = toApplication testHB
+
+runApp :: IO ()
+runApp = do
+    putStrLn "running on port 3002..."
+    run 3002 app
