diff --git a/buffer-builder.cabal b/buffer-builder.cabal
--- a/buffer-builder.cabal
+++ b/buffer-builder.cabal
@@ -1,5 +1,5 @@
 name:                buffer-builder
-version:             0.2.0.2
+version:             0.2.0.3
 synopsis:            Library for efficiently building up buffers, one piece at a time
 description:
 
@@ -47,6 +47,7 @@
 stability:           experimental
 homepage:            https://github.com/chadaustin/buffer-builder
 cabal-version:       >=1.10
+extra-source-files:  test/*.hs
 
 library
   exposed-modules:
diff --git a/test/BufferTest.hs b/test/BufferTest.hs
new file mode 100644
--- /dev/null
+++ b/test/BufferTest.hs
@@ -0,0 +1,43 @@
+{-# LANGUAGE MagicHash, OverloadedStrings, TemplateHaskell #-}
+
+module BufferTest (tests, main) where
+
+import qualified Data.ByteString.Char8 as BSC
+import Test.Tasty
+import Test.Tasty.TH
+import Test.Tasty.QuickCheck
+import Test.Tasty.HUnit
+import Data.BufferBuilder
+
+case_append_bytes :: Assertion
+case_append_bytes = do
+    let result = runBufferBuilder $ do
+            appendChar8 'f'
+            appendChar8 'o'
+            appendChar8 'o'
+    assertEqual "matches" "foo" result
+
+case_append_string :: Assertion
+case_append_string = do
+    let result = runBufferBuilder $ do
+            appendChar8 'f'
+            appendChar8 'o'
+            appendChar8 'o'
+            appendBS "bar"
+    assertEqual "matches" "foobar" result
+
+case_append_literals :: Assertion
+case_append_literals = do
+    let result = runBufferBuilder $ do
+            appendLiteral "foo"#
+            appendLiteral "bar"#
+    assertEqual "matches" "foobar" result
+
+prop_match_intDec :: Int -> Bool
+prop_match_intDec i = runBufferBuilder (appendDecimalSignedInt i) == (BSC.pack $ show i)
+
+tests :: TestTree
+tests = $(testGroupGenerator)
+
+main :: IO ()
+main = defaultMain tests
diff --git a/test/JsonTest.hs b/test/JsonTest.hs
new file mode 100644
--- /dev/null
+++ b/test/JsonTest.hs
@@ -0,0 +1,122 @@
+{-# LANGUAGE MagicHash, OverloadedStrings, TemplateHaskell, ExistentialQuantification #-}
+
+module JsonTest where
+
+import Data.Foldable (foldMap)
+import Test.Tasty
+import Test.Tasty.TH
+import qualified Data.Attoparsec.ByteString as Atto
+import Test.Tasty.HUnit
+import Test.Tasty.QuickCheck
+import Data.Monoid ((<>), Monoid (mconcat, mempty))
+import Data.String (IsString (..))
+import Data.BufferBuilder.Json
+import qualified Data.Aeson as Aeson
+import qualified Data.Aeson.Parser as JsonParse
+import qualified Data.ByteString as BS
+import           Data.Text (Text)
+import qualified Data.Text as Text
+import qualified Data.Vector as Vector
+
+ae :: (IsString a, Show a, Eq a) => String -> a -> Assertion
+ae expected actual = assertEqual expected (fromString expected) actual
+
+case_encode_int :: IO ()
+case_encode_int = do
+    ae "-42" (encodeJson (-42::Int))
+    ae "9" (encodeJson (9::Int))
+
+case_encode_bool :: IO ()
+case_encode_bool = do
+    ae "true" (encodeJson True)
+    ae "false" (encodeJson False)
+
+case_encode_text :: IO ()
+case_encode_text = do
+    ae "\"hello\"" (encodeJson ("hello" :: Text))
+    ae "\"\"" (encodeJson ("" :: Text))
+
+    ae "\"\\\"\\\\\\n\\r\\t\"" (encodeJson (fromString ['\"', '\\', '\n', '\r', '\t'] :: Text))
+
+case_encode_object :: IO ()
+case_encode_object = do
+    ae "{\"key\":\"value\"}" (encodeJson ("key" .= ("value" :: Text)))
+    ae "{\"key\":\"value\",\"key2\":[5,6,7]}"
+        (encodeJson ("key" .= ("value" :: Text) <> "key2" .= ([5,6,7] :: [Int])))
+    ae "{}" $ encodeJson $ ((mempty :: ObjectBuilder) <> mempty) <> (mempty <> mempty)
+    ae "[]" (encodeJson ([] :: [Int]))
+
+case_monoid_laws :: IO ()
+case_monoid_laws = do
+    -- TODO QuickCheck
+    let a = "key" .= ("value" :: Text)
+        b = "key2" .= (999 :: Int)
+        c = "key3" .= ([1,2,3] :: [Int])
+    assertEqual "Left identity" (encodeJson a) (encodeJson (mempty <> a))
+    assertEqual "Right identity" (encodeJson a) (encodeJson (a <> mempty))
+    assertEqual "Associativity" (encodeJson (a <> (b <> c))) (encodeJson ((a <> b) <> c))
+    assertEqual "mconcat" (encodeJson (mconcat [a, b, c])) (encodeJson (a <> b <> c))
+
+data JsonValue = forall a. ToJson a => JsonValue a
+
+instance ToJson JsonValue where
+    toJson (JsonValue a) = toJson a
+
+instance Show JsonValue where
+    show jv = show $ encodeJson jv
+
+shrink10x :: Gen a -> Gen a
+shrink10x a = sized $ \size ->
+    let newSize = max 1 (size `div` 10)
+    in resize newSize a
+
+instance Arbitrary JsonValue where
+    arbitrary = do
+        i <- fmap (`mod` 9) arbitrary :: Gen Int
+        case i of
+            0 -> fmap (JsonValue . array)
+                (shrink10x arbitrary :: Gen [JsonValue])
+            1 -> fmap (JsonValue . Vector.fromList)
+                (shrink10x arbitrary :: Gen [JsonValue])
+            2 -> fmap (JsonValue . Text.pack) (arbitrary :: Gen String)
+            3 -> fmap JsonValue (arbitrary :: Gen Int)
+            4 -> fmap JsonValue (arbitrary :: Gen Double)
+            5 -> return $ JsonValue True
+            6 -> return $ JsonValue False
+            7 -> return $ JsonValue (Nothing :: Maybe Int)
+            8 -> fmap JsonValue (shrink10x arbitrary :: Gen JsonObject)
+            _ -> error "Andy made a mistake"
+
+newtype JsonObject = JsonObject [(Text, JsonValue)]
+
+instance Arbitrary JsonObject where
+    arbitrary = do
+        p1 <- arbitrary
+        return $ JsonObject [(Text.pack k, v) | (k, v) <- p1]
+
+instance ToJson JsonObject where
+    toJson (JsonObject pairs) =
+        toJson $ foldMap makePair pairs
+      where
+        makePair (k, v) = k .= v
+
+decodeJsonFragment :: Aeson.FromJSON j => BS.ByteString -> Maybe j
+decodeJsonFragment str = case parsed of
+    Right r -> case Aeson.fromJSON r of
+        Aeson.Success a -> Just a
+        _              -> Nothing
+    _ -> Nothing
+  where
+    parsed = Atto.parseOnly JsonParse.value' str
+
+prop_always_produces_legal_json :: JsonValue -> Bool
+prop_always_produces_legal_json jv =
+    case decodeJsonFragment $ encodeJson jv :: Maybe Aeson.Value of
+        Just _ -> True
+        Nothing -> False
+
+tests :: TestTree
+tests = $(testGroupGenerator)
+
+main :: IO ()
+main = defaultMain tests
diff --git a/test/Utf8Test.hs b/test/Utf8Test.hs
new file mode 100644
--- /dev/null
+++ b/test/Utf8Test.hs
@@ -0,0 +1,30 @@
+{-# LANGUAGE MagicHash, OverloadedStrings, TemplateHaskell #-}
+
+module Utf8Test (tests, main) where
+
+import Test.Tasty
+import Test.Tasty.TH
+import Test.Tasty.HUnit
+import Data.BufferBuilder.Utf8
+import qualified Data.Text as Text
+import qualified Data.Text.Encoding as TE
+
+case_append_string :: Assertion
+case_append_string = do
+    let str = "foo\NUL\1234"
+    let bb = runUtf8Builder $ do
+            appendString str
+    assertEqual "matches" (Text.pack str) $ TE.decodeUtf8 bb
+
+case_append_chars :: Assertion
+case_append_chars = do
+    let bb = runUtf8Builder $ do
+            appendChar '\NUL'
+            appendChar '\x1234'
+    assertEqual "matches" (TE.encodeUtf8 "\NUL\x1234") bb
+
+tests :: TestTree
+tests = $(testGroupGenerator)
+
+main :: IO ()
+main = defaultMain tests
