diff --git a/simple-templates.cabal b/simple-templates.cabal
--- a/simple-templates.cabal
+++ b/simple-templates.cabal
@@ -2,7 +2,7 @@
 -- documentation, see http://haskell.org/cabal/users-guide/
 
 name:                simple-templates
-version:             0.8.0.0
+version:             0.8.0.1
 synopsis:            A basic template language for the Simple web framework
 description:
   A basic template language for the Simple web framework. The language supports
@@ -51,6 +51,9 @@
     , scientific
     , simple-templates
     , vector
+  other-modules:
+      Web.Simple.Templates.LanguageSpec
+    , Web.Simple.Templates.ParserSpec
   default-language: Haskell2010
 
 source-repository head
diff --git a/test/Web/Simple/Templates/LanguageSpec.hs b/test/Web/Simple/Templates/LanguageSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Web/Simple/Templates/LanguageSpec.hs
@@ -0,0 +1,18 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Web.Simple.Templates.LanguageSpec where
+
+import Data.Aeson
+import Test.HUnit
+import Test.Hspec
+import Web.Simple.Templates.Language
+
+spec :: Spec
+spec = describe "Web.Simple.Templates.Language" $ do
+  describe "valueToText" $ do
+    it "renders whole number as decimal" $ do
+      let val = Number 4563
+      assertEqual "" "4563" (valueToText val)
+    it "renders rational number with decimal point" $ do
+      let val = Number 3432.5
+      assertEqual "" "3432.5" (valueToText val)
+
diff --git a/test/Web/Simple/Templates/ParserSpec.hs b/test/Web/Simple/Templates/ParserSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/Web/Simple/Templates/ParserSpec.hs
@@ -0,0 +1,179 @@
+{-# LANGUAGE OverloadedStrings #-}
+module Web.Simple.Templates.ParserSpec where
+
+import Data.Aeson
+import qualified Data.Attoparsec.Text as A
+import Data.Maybe
+import Data.Scientific
+import qualified Data.Vector as V
+import Test.HUnit
+import Test.Hspec
+import Web.Simple.Templates.Parser
+import Web.Simple.Templates.Types
+
+spec :: Spec
+spec = describe "Web.Simple.Templates.Parser" $ do
+  describe "pEscapedDollar" $ do
+    it "reads $$ as escaped dollar" $ do
+      let parsedStr = A.parseOnly pEscapedDollar "$$"
+      assertEqual "" (Right "$") parsedStr
+    it "fails on single $" $ do
+      let parsedStr = A.maybeResult $ A.parse pEscapedDollar "$ "
+      assert $ isNothing parsedStr
+  describe "pRaw" $ do
+    it "fails on dollar-sign" $ do
+      let parsedStr = A.maybeResult $ A.parse pRaw "$blor doop"
+      assert $ isNothing parsedStr
+    it "reads non-dollar-sign text" $ do
+      let parsedStr = A.parseOnly pRaw "blor doop"
+      assertEqual "" (Right $ ASTLiteral (String "blor doop")) parsedStr
+    it "reads text until dollar sign" $ do
+      let parsedStr = A.parseOnly pRaw "blor doop$"
+      assertEqual "" (Right $ ASTLiteral (String "blor doop")) parsedStr
+    it "reads text with escaped dollar sing" $ do
+      let parsedStr = A.parseOnly pRaw "you owe me $$4"
+      assertEqual "" (Right $ ASTLiteral (String "you owe me $4")) parsedStr
+
+  describe "pLiteral" $ do
+    it "reads a number" $ do
+      let parsedStr = A.parseOnly pLiteral "12345.66"
+      assertEqual "" (Right $ fromLiteral (12345.66 :: Scientific)) parsedStr
+    it "reads a string" $ do
+      let parsedStr = A.parseOnly pLiteral "\"hello\""
+      assertEqual "" (Right $ fromLiteral ("hello" :: String)) parsedStr
+    it "reads a string with escaped quote" $ do
+      let parsedStr = A.parseOnly pLiteral "\"hello \\ \\\"world\""
+      assertEqual "" (Right $ fromLiteral ("hello \\ \"world" :: String))
+                     parsedStr
+    it "reads array" $ do
+      let parsedStr = A.parseOnly pLiteral "[1]"
+      let expected = ASTArray $ V.fromList [fromLiteral (1 :: Int)]
+      assertEqual "" (Right expected) parsedStr
+    it "reads 'true' as boolean" $ do
+      let parsedStr = A.parseOnly pLiteral "true"
+      assertEqual "" (Right $ fromLiteral True) parsedStr
+    it "reads 'false' as boolean" $ do
+      let parsedStr = A.parseOnly pLiteral "false"
+      assertEqual "" (Right $ fromLiteral False) parsedStr
+    it "reads null as null value" $ do
+      let parsedStr = A.parseOnly pLiteral "null"
+      assertEqual "" (Right $ ASTLiteral Null) parsedStr
+
+  describe "pArray" $ do
+    it "matches array with literals" $ do
+      let parsedStr = A.parseOnly pArray "[1  ,2 ,3 ,  4]"
+      let expected = ASTArray $ V.fromList $ map fromLiteral
+                      [ 1 :: Int
+                      , 2
+                      , 3
+                      , 4 ]
+      assertEqual "" (Right expected) parsedStr
+    it "matches array with mixed literals" $ do
+      let parsedStr = A.parseOnly pArray "[1,\"hello\"]"
+      let expected = ASTArray $ V.fromList
+                      [ fromLiteral (1 :: Int)
+                      , fromLiteral ("hello" :: String)]
+      assertEqual "" (Right expected) parsedStr
+
+  describe "pIdentifier" $ do
+    it "matches alphunumeric string starting with a letter" $ do
+      let parsedStr = A.parseOnly pIdentifier "l33Th8x0r"
+      assertEqual "" (Right "l33Th8x0r") parsedStr
+    it "must start with a letter" $ do
+      let parsedStr = A.maybeResult $ A.parse pIdentifier "3rd"
+      assert $ isNothing parsedStr
+    it "allows underscores and dashes" $ do
+      let parsedStr = A.parseOnly pIdentifier "l33Th-8x_r"
+      assertEqual "" (Right "l33Th-8x_r") parsedStr
+
+  describe "pIndex" $ do
+    it "matches an variable name on the left and an identifier on the right" $
+      do
+        let parsedStr = A.parseOnly pIndex "foo.bar.baz"
+        assertEqual "" (Right $ ASTIndex (ASTVar "foo") ["bar", "baz"])
+                       parsedStr
+
+  describe "pVar" $ do
+    it "matches an identifier" $ do
+      let parsedStr = A.parseOnly pVar "foo"
+      assertEqual "" (Right $ ASTVar "foo") parsedStr
+
+  describe "pFunc" $ do
+    it "matches no argument function call" $ do
+      let parsedStr = A.parseOnly pFunc "foo()"
+      assertEqual "" (Right $ ASTFunc "foo" []) parsedStr
+    it "matches function call with one argument" $ do
+      let parsedStr = A.parseOnly pFunc "foo(1234)"
+      assertEqual "" (Right $ ASTFunc "foo" [ASTLiteral $ Number 1234])
+                     parsedStr
+    it "matches function call with many arguments" $ do
+      let parsedStr = A.parseOnly pFunc "foo(1234, \"hello\", 5432)"
+      assertEqual "" (Right $ ASTFunc "foo" [ ASTLiteral $ Number 1234
+                                            , ASTLiteral $ String "hello"
+                                            , ASTLiteral $ Number 5432])
+                     parsedStr
+    it "matches nested function calls" $ do
+      let parsedStr = A.parseOnly pFunc "foo(bar())"
+      assertEqual "" (Right $ ASTFunc "foo" [ASTFunc "bar" []]) parsedStr
+    it "matches function calls with index" $ do
+      let parsedStr = A.parseOnly pFunc "foo(bar.baz)"
+      assertEqual ""
+        (Right $ ASTFunc "foo" [ASTIndex (ASTVar "bar") ["baz"]])
+        parsedStr
+
+  describe "pIf" $ do
+    it "matches if with no false branch" $ do
+      let parsedStr = A.parseOnly pIf "if(1234)$hello world$endif"
+      assertEqual "" (Right $
+                        ASTIf (ASTLiteral $ Number 1234)
+                          (ASTRoot [ASTLiteral $ String "hello world"])
+                          Nothing)
+                     parsedStr
+    it "matches if with nested statement in true branch" $ do
+      let parsedStr = A.parseOnly pIf "if(1234)$hello $123$ world$endif"
+      assertEqual "" (Right $
+                        ASTIf (ASTLiteral $ Number 1234)
+                          (ASTRoot [ ASTLiteral $ String "hello "
+                                  , ASTLiteral $ Number 123
+                                  , ASTLiteral $ String " world"])
+                          Nothing)
+                     parsedStr
+    it "matches with a space after" $ do
+      let parsedStr = A.parseOnly pIf "if 1234$hello world$endif"
+      assertEqual "" (Right $
+                        ASTIf (ASTLiteral $ Number 1234)
+                          (ASTRoot [ASTLiteral $ String "hello world"])
+                          Nothing)
+                     parsedStr
+    it "matches with if/else" $ do
+      let parsedStr = A.parseOnly pIf "if(true)$$else$$endif"
+      assertEqual "" (Right $
+                        ASTIf (ASTLiteral $ Bool True)
+                          (ASTRoot [])
+                          (Just $ ASTRoot []))
+                      parsedStr
+
+  describe "pFor" $ do
+    it "matches basic for loop" $ do
+      let parsedStr = A.parseOnly pFor "for(i in [])$$i$$endfor"
+      assertEqual "" (Right $
+                        ASTFor Nothing "i" (ASTArray (V.fromList []))
+                          (ASTRoot [ASTVar "i"]) Nothing) parsedStr
+    it "matches with space after for" $ do
+      let parsedStr = A.parseOnly pFor "for i in []$$i$$endfor"
+      assertEqual "" (Right $
+                        ASTFor Nothing "i" (ASTArray (V.fromList []))
+                          (ASTRoot [ASTVar "i"]) Nothing) parsedStr
+    it "matches with separator" $ do
+      let parsedStr = A.parseOnly pFor "for(i in [])$$i$$sep$hello$endfor"
+      assertEqual "" (Right $
+                        ASTFor Nothing "i" (ASTArray (V.fromList []))
+                          (ASTRoot [ASTVar "i"])
+                          (Just $ ASTRoot [ASTLiteral $ String "hello"]))
+                     parsedStr
+    it "matches for loop with index" $ do
+      let parsedStr = A.parseOnly pFor "for(i,v in [])$$i$$endfor"
+      assertEqual "" (Right $
+                        ASTFor (Just "i") "v" (ASTArray (V.fromList []))
+                          (ASTRoot [ASTVar "i"]) Nothing) parsedStr
+
