diff --git a/haskell-postal.cabal b/haskell-postal.cabal
--- a/haskell-postal.cabal
+++ b/haskell-postal.cabal
@@ -1,5 +1,5 @@
 name:                haskell-postal
-version:             0.2.0.1
+version:             0.2.1.0
 synopsis:            Haskell binding for the libpostal library
 description:         Provides an interface for the libpostal NLP library
 homepage:            https://github.com/netom/haskell-postal#readme
@@ -25,9 +25,12 @@
     , text >= 1.2 && < 1.3
     , inline-c >= 0.6 && < 0.7
 
-executable haskell-postal
-  hs-source-dirs:      src
-  main-is:             main.hs
+test-suite spec
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:
+      test
+    , src
+  main-is:             Spec.hs
   default-language:    Haskell2010
   pkgconfig-depends:   libpostal
   ghc-options:         -Wall
@@ -36,8 +39,10 @@
     , bytestring >= 0.10 && < 0.11
     , text >= 1.2 && < 1.3
     , inline-c >= 0.6 && < 0.7
+    , hspec >= 2.5 && < 2.6
   other-modules:
       NLP.Postal
+    , NLP.Postal.PostalSpec
 
 source-repository head
   type:     git
diff --git a/src/NLP/Postal.hs b/src/NLP/Postal.hs
--- a/src/NLP/Postal.hs
+++ b/src/NLP/Postal.hs
@@ -31,6 +31,7 @@
 import Data.Monoid ((<>))
 import Foreign.C.Types
 import qualified Language.C.Inline as C
+import qualified Language.C.Inline.Unsafe as CU
 import Language.C.Inline.Context as CX
 
 import Foreign.Ptr
@@ -59,22 +60,22 @@
 -- |Calls libpostal_setup() to set up the library
 -- Make sure you call this before anything else
 setup :: IO Int
-setup = fromIntegral <$> [C.exp| int { libpostal_setup() } |]
+setup = fromIntegral <$> [CU.exp| int { libpostal_setup() } |]
 
 -- |Calls libpostal_setup_parser() to set up the address parser
 -- Call this before trying to parse addresses
 setupParser :: IO Int
-setupParser = fromIntegral <$> [C.exp| int { libpostal_setup_parser() } |]
+setupParser = fromIntegral <$> [CU.exp| int { libpostal_setup_parser() } |]
 
 -- |Calls libpostal_setup_language_classifier() to set up the classifier
 -- Call this before doing address normaliation
 setupLanguageClassifier :: IO Int
-setupLanguageClassifier =  fromIntegral <$> [C.exp| int { libpostal_setup_language_classifier() } |]
+setupLanguageClassifier =  fromIntegral <$> [CU.exp| int { libpostal_setup_language_classifier() } |]
 
 -- |Returns default parser options
 -- returned by libpostal_get_address_parser_default_options()
 getAddressParserDefaultOptions :: IO (Ptr AddressParserOptions)
-getAddressParserDefaultOptions = castPtr <$> [C.block|
+getAddressParserDefaultOptions = castPtr <$> [CU.block|
     void * {
         libpostal_address_parser_options_t options = libpostal_get_address_parser_default_options();
         void * hoptions = malloc(sizeof(options));
@@ -85,7 +86,7 @@
 -- |Returns default normalizer options
 -- returned by libpostal_get_default_options()
 getDefaultNormalizeOptions :: IO (Ptr NormalizeOptions)
-getDefaultNormalizeOptions = castPtr <$> [C.block|
+getDefaultNormalizeOptions = castPtr <$> [CU.block|
     void * {
         libpostal_normalize_options_t options = libpostal_get_default_options();
         void * hoptions = malloc(sizeof(options));
@@ -98,7 +99,7 @@
 -- parts as a list of key-value tuples (an association list).
 parseAddress :: Ptr AddressParserOptions -> T.Text -> IO [(T.Text, T.Text)]
 parseAddress options address = do
-        response <- [C.exp| void * { libpostal_parse_address($bs-ptr:bsAddress, * (libpostal_address_parser_options_t *) $(void * castedOptions)) } |]
+        response <- [CU.exp| void * { libpostal_parse_address($bs-ptr:bsAddress, * (libpostal_address_parser_options_t *) $(void * castedOptions)) } |]
 
         when (response == nullPtr) $ fail "libpostal_parse_address returned NULL a pointer"
 
@@ -133,7 +134,7 @@
                 , TE.decodeUtf8 $ fromForeignPtr (castForeignPtr compFPtr) 0 (fromIntegral compLen)
                 )
 
-        [C.exp| void { libpostal_address_parser_response_destroy((libpostal_address_parser_response_t *) $(void * response)) } |]
+        [CU.exp| void { libpostal_address_parser_response_destroy((libpostal_address_parser_response_t *) $(void * response)) } |]
 
         return result
 
@@ -147,7 +148,7 @@
 expandAddress :: Ptr NormalizeOptions -> T.Text -> IO [T.Text]
 expandAddress options address = do
     (numExpansions, expansions) <- C.withPtr $ \numExpansions ->
-        [C.exp| char * * { libpostal_expand_address($bs-ptr:bsAddress, * (libpostal_normalize_options_t *) $(void * cOptions), $(size_t * numExpansions) ) } |]
+        [CU.exp| char * * { libpostal_expand_address($bs-ptr:bsAddress, * (libpostal_normalize_options_t *) $(void * cOptions), $(size_t * numExpansions) ) } |]
 
     -- TODO: this also turns an array of (char *) to a list of Texts.
     result <- forM [0..numExpansions-1] $ \i -> do
@@ -163,7 +164,7 @@
 
         return $ TE.decodeUtf8 $ fromForeignPtr (castForeignPtr xpFPtr) 0 (fromIntegral xpLen)
 
-    [C.exp| void { libpostal_expansion_array_destroy($(char * * expansions), $(size_t numExpansions)) } |]
+    [CU.exp| void { libpostal_expansion_array_destroy($(char * * expansions), $(size_t numExpansions)) } |]
 
     return result
 
@@ -173,12 +174,12 @@
 
 -- |Calls libpostal_teardown_parser()
 tearDownParser :: IO ()
-tearDownParser = [C.exp| void { libpostal_teardown_parser() } |]
+tearDownParser = [CU.exp| void { libpostal_teardown_parser() } |]
 
 -- |Calls libpostal_teardown_language_classifier()
 tearDownLanguageClassifier :: IO ()
-tearDownLanguageClassifier = [C.exp| void { libpostal_teardown_language_classifier() } |]
+tearDownLanguageClassifier = [CU.exp| void { libpostal_teardown_language_classifier() } |]
 
 -- |Calls libpostal_teardown()
 tearDown :: IO ()
-tearDown = [C.exp| void { libpostal_teardown() } |]
+tearDown = [CU.exp| void { libpostal_teardown() } |]
diff --git a/src/main.hs b/src/main.hs
deleted file mode 100644
--- a/src/main.hs
+++ /dev/null
@@ -1,37 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-module Main where
-
-import Control.Monad
-import System.Environment
-import System.Exit
-import NLP.Postal
-
-import qualified Data.Text as T
-
-main :: IO ()
-main = do
-    args <- getArgs
-
-    when (length args /= 1) $ die "Usage: haskell-postal <address>"
-
-    let address = T.pack $ head args
-
-    putStrLn "Setting up libpostal..."
-    setup >>= \x -> when (x == 0) $ die "FAIL"
-
-    putStrLn "Setting up the parser..."
-    setupParser >>= \x -> when (x == 0) $ die "FAIL"
-
-    putStrLn "Setting up the language classifier..."
-    setupLanguageClassifier >>= \x -> when (x == 0) $ die "FAIL"
-
-    defParserOpts <- getAddressParserDefaultOptions
-    defNormalizeOptions <- getDefaultNormalizeOptions
-
-    parseAddress defParserOpts address >>= print
-    expandAddress defNormalizeOptions address >>= print
-
-    tearDownLanguageClassifier
-    tearDownParser
-    tearDown
diff --git a/test/NLP/Postal/PostalSpec.hs b/test/NLP/Postal/PostalSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/NLP/Postal/PostalSpec.hs
@@ -0,0 +1,49 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module NLP.Postal.PostalSpec (spec) where
+
+import Test.Hspec
+import NLP.Postal
+import Control.Monad (forM_)
+import Control.Monad.IO.Class (liftIO)
+import Data.List (lookup)
+import qualified Data.Text as T
+
+spec :: Spec
+spec = do
+    describe "setup" $ do
+        it "general setup runs successfully" $ do
+            r <- liftIO setup
+            r `shouldBe` 1
+        it "parser setup runs successfully" $ do
+             r <- liftIO setupParser
+             r `shouldBe` 1
+        it "language classifier setup runs successfully" $ do
+            r <- liftIO setupLanguageClassifier
+            r `shouldBe` 1
+
+    describe "expansion" $ do
+        it "expands an address correctly" $ do
+            defNormalizeOptions <- liftIO getDefaultNormalizeOptions
+            r <- liftIO $ expandAddress defNormalizeOptions "11 Wall Street New York, NY"
+            forM_ r $ \xp -> T.take 14 xp `shouldBe` "11 wall street"
+
+    describe "parser" $ do
+        it "parses an address correctly" $ do
+            defParserOpts <- liftIO getAddressParserDefaultOptions
+            r <- liftIO $ parseAddress defParserOpts "11 Wall Street New York, NY"
+            lookup "house_number" r `shouldBe` Just "11"
+            lookup "road" r `shouldBe` Just "wall street"
+            lookup "city" r `shouldBe` Just "new york"
+            lookup "state" r `shouldBe` Just "ny"
+
+    describe "teardown" $ do
+        it "language classifier teardown" $ do
+            r <- liftIO tearDownLanguageClassifier
+            r `shouldBe` ()
+        it "parser teardown" $ do
+            r <- liftIO tearDownParser
+            r `shouldBe` ()
+        it "teardown" $ do
+            r <- liftIO tearDown
+            r `shouldBe` ()
diff --git a/test/Spec.hs b/test/Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
