diff --git a/Text/Casing.hs b/Text/Casing.hs
deleted file mode 100644
--- a/Text/Casing.hs
+++ /dev/null
@@ -1,144 +0,0 @@
-{-# LANGUAGE GeneralizedNewtypeDeriving #-}
-{-# LANGUAGE DeriveTraversable #-}
--- | Conversions between several common identifier casing conventions:
---
--- - @PascalCase@ - no spacing between words, first letter in word is
--- uppercase, all others are lowercase.
--- - @camelCase@ - like @PascalCase@, but the very first letter is lowercase.
--- - @kebab-case@ - everything lowercase, dash delimits words.
--- - @snake_Case@ - underscores delimit words, case is unrestricted.
--- - @quiet_snake_case@ - underscores delimit words, everything lowercase.
--- - @SCREAMING_SNAKE_CASE@ - underscores delimit words, everything uppercase.
-module Text.Casing
-(
--- * Types
-Identifier (..)
--- * Parsing
-, fromHumps
-, fromKebab
-, fromSnake
-, fromWords
-, fromAny
--- * Generating
-, toCamel
-, toPascal
-, toSnake
-, toQuietSnake
-, toScreamingSnake
-, toKebab
-, toWords
--- * Shorthand functions
-, pascal
-, camel
-, snake
-, quietSnake
-, screamingSnake
-, kebab
-, wordify
--- * Miscellaneous
-, dropPrefix
-)
-where
-
-import Data.Char
-import Data.List (intersperse)
-import Data.List.Split (wordsBy)
-import Control.Applicative
-
--- | An opaque type that represents a parsed identifier.
-newtype Identifier a = Identifier { unIdentifier :: [a] }
-    deriving (Monad, Functor, Applicative, Show, Foldable, Traversable)
-
-wordCase :: String -> String
-wordCase "" = ""
-wordCase (x:xs) = toUpper x : map toLower xs
-
--- | Convert from "humped" casing (@camelCase@ or @PascalCase@)
-fromHumps :: String -> Identifier String
-fromHumps = Identifier . go
-    where
-        go "" = [""]
-        go (x:[]) = [x:[]]
-        go (x:y:xs)
-            | (not $ isUpper x) && (isUpper y) =
-                let (z:zs) = go (y:xs)
-                in [x]:(z:zs)
-            | otherwise =
-                let (z:zs) = go (y:xs)
-                in (x:z):zs
-
-fromWords :: String -> Identifier String
-fromWords = Identifier . words
-
--- | Convert from @kebab-cased-identifiers@
-fromKebab :: String -> Identifier String
-fromKebab = Identifier . wordsBy (== '-')
-
--- | Convert from @snake_cased@ (either flavor)
-fromSnake :: String -> Identifier String
-fromSnake = Identifier . wordsBy (== '_')
-
--- | Convert from anything, including mixed casing.
-fromAny :: String -> Identifier String
-fromAny str = fromHumps str >>= fromKebab >>= fromSnake >>= fromWords
-
--- | To @PascalCase@
-toPascal :: Identifier String -> String
-toPascal = concat . map wordCase . unIdentifier
-
--- | To @camelCase@
-toCamel :: Identifier String -> String
-toCamel (Identifier (x:xs)) = concat $ map toLower x:map wordCase xs
-
--- | To @kebab-case@
-toKebab :: Identifier String -> String
-toKebab = concat . intersperse "-" . map (map toLower) . unIdentifier
-
--- | To @snake_Case@
-toSnake :: Identifier String -> String
-toSnake = concat . intersperse "_" . unIdentifier
-
--- | To @quiet_snake_case@
-toQuietSnake :: Identifier String -> String
-toQuietSnake = map toLower . toSnake
-
--- | To @SCREAMING_SNAKE_CASE@
-toScreamingSnake :: Identifier String -> String
-toScreamingSnake = map toUpper . toSnake
-
--- | To @word Case@
-toWords :: Identifier String -> String
-toWords = unwords . unIdentifier
-
--- | Directly convert to @PascalCase@ through 'fromAny'
-pascal :: String -> String
-pascal = toPascal . fromAny
-
--- | Directly convert to @camelCase@ through 'fromAny'
-camel :: String -> String
-camel = toCamel . fromAny
-
--- | Directly convert to @snake_Case@ through 'fromAny'
-snake :: String -> String
-snake = toSnake . fromAny
-
--- | Directly convert to @quiet_snake_case@ through 'fromAny'
-quietSnake :: String -> String
-quietSnake = toQuietSnake . fromAny
-
--- | Directly convert to @SCREAMING_SNAKE_CASE@ through 'fromAny'
-screamingSnake :: String -> String
-screamingSnake = toScreamingSnake . fromAny
-
--- | Directly convert to @kebab-case@ through 'fromAny'
-kebab :: String -> String
-kebab = toKebab . fromAny
-
--- | Directly convert to @word Case@ through 'fromAny'
-wordify :: String -> String
-wordify = toWords . fromAny
-
--- | Drop the first word from a parsed identifier. Typical usage is between
--- parsing and writing, e.g.: @toKebab . dropPrefix . fromAny $ "strHelloWorld" == "hello-world"@
-dropPrefix :: Identifier String -> Identifier String
-dropPrefix = Identifier . drop 1 . unIdentifier
diff --git a/casing.cabal b/casing.cabal
--- a/casing.cabal
+++ b/casing.cabal
@@ -1,26 +1,40 @@
--- Initial casing.cabal generated by cabal init.  For further 
+-- Initial casing.cabal generated by cabal init. For further 
 -- documentation, see http://haskell.org/cabal/users-guide/
 
-name:                casing
-version:             0.1.2.1
-synopsis:            Convert between various source code casing conventions
-description:         Converts between camelCase, PascalCase, kebab-case, and
-                     three flavors of snake_case.
-license:             MIT
-license-file:        LICENSE
-author:              Tobias Dammers
-maintainer:          tdammers@gmail.com
--- copyright:           
-category:            Text
-build-type:          Simple
--- extra-source-files:  
-cabal-version:       >=1.10
+name: casing
+version: 0.1.3.0
+synopsis: Convert between various source code casing conventions
+description: Converts between camelCase, PascalCase, kebab-case, and three
+             flavors of snake_case.
+license: MIT
+license-file: LICENSE
+author: Tobias Dammers
+maintainer: tdammers@gmail.com
+-- copyright: 
+category: Text
+build-type: Simple
+-- extra-source-files: 
+cabal-version: >=1.10
 
+source-repository head
+    type: git
+    location: https://bitbucket.org/tdammers/casing
+
 library
-  exposed-modules:     Text.Casing
-  -- other-modules:       
-  -- other-extensions:    
-  build-depends:       base >=4.8 && <5
-               ,       split
-  -- hs-source-dirs:      
-  default-language:    Haskell2010
+  exposed-modules: Text.Casing
+  -- other-modules: 
+  -- other-extensions: 
+  build-depends: base >=4.8 && <5
+               , split
+  hs-source-dirs: src
+  default-language: Haskell2010
+
+test-suite tests
+  main-is: Spec.hs
+  hs-source-dirs: tests
+  type: exitcode-stdio-1.0
+  default-language: Haskell2010
+  build-depends: base >=4.8 && <5
+               , casing
+               , tasty
+               , tasty-hunit
diff --git a/src/Text/Casing.hs b/src/Text/Casing.hs
new file mode 100644
--- /dev/null
+++ b/src/Text/Casing.hs
@@ -0,0 +1,161 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE DeriveTraversable #-}
+-- | Conversions between several common identifier casing conventions:
+--
+-- - @PascalCase@ - no spacing between words, first letter in word is
+-- uppercase, all others are lowercase.
+-- - @camelCase@ - like @PascalCase@, but the very first letter is lowercase.
+-- - @kebab-case@ - everything lowercase, dash delimits words.
+-- - @snake_Case@ - underscores delimit words, case is unrestricted.
+-- - @quiet_snake_case@ - underscores delimit words, everything lowercase.
+-- - @SCREAMING_SNAKE_CASE@ - underscores delimit words, everything uppercase.
+module Text.Casing
+(
+-- * Types
+Identifier (..)
+-- * Parsing
+, fromHumps
+, fromKebab
+, fromSnake
+, fromWords
+, fromAny
+-- * Generating
+, toCamel
+, toPascal
+, toSnake
+, toQuietSnake
+, toScreamingSnake
+, toKebab
+, toWords
+-- * Shorthand functions
+, pascal
+, camel
+, snake
+, quietSnake
+, screamingSnake
+, kebab
+, wordify
+-- * Miscellaneous
+, dropPrefix
+)
+where
+
+import Data.Char
+import Data.List (intersperse)
+import Data.List.Split (wordsBy)
+import Control.Applicative
+
+-- | An opaque type that represents a parsed identifier.
+newtype Identifier a = Identifier { unIdentifier :: [a] }
+    deriving (Monad, Functor, Applicative, Show, Foldable, Traversable, Eq)
+
+wordCase :: String -> String
+wordCase "" = ""
+wordCase (x:xs) = toUpper x : map toLower xs
+
+-- | Convert from "humped" casing (@camelCase@ or @PascalCase@)
+fromHumps :: String -> Identifier String
+fromHumps = Identifier . go
+    where
+        go "" = [""]
+        go (x:[]) = [x:[]]
+        go xxs@(x:xs)
+          | isUpper x =
+              case xs of
+                "" ->
+                  [[x]]
+                (y:_) ->
+                  if isUpper y then
+                    [x]:go xs
+                  else
+                    let cur = x:takeWhile (not . isUpper) xs
+                        rem = dropWhile (not . isUpper) xs
+                    in 
+                    if null rem then
+                      [cur]
+                    else
+                      cur:go rem
+          | otherwise =
+              let cur = takeWhile (not . isUpper) xxs
+                  rem = dropWhile (not . isUpper) xxs
+              in
+              if null rem then
+                [cur]
+              else
+                cur:go rem
+
+fromWords :: String -> Identifier String
+fromWords = Identifier . words
+
+-- | Convert from @kebab-cased-identifiers@
+fromKebab :: String -> Identifier String
+fromKebab = Identifier . wordsBy (== '-')
+
+-- | Convert from @snake_cased@ (either flavor)
+fromSnake :: String -> Identifier String
+fromSnake = Identifier . wordsBy (== '_')
+
+-- | Convert from anything, including mixed casing.
+fromAny :: String -> Identifier String
+fromAny str = fromHumps str >>= fromKebab >>= fromSnake >>= fromWords
+
+-- | To @PascalCase@
+toPascal :: Identifier String -> String
+toPascal = concat . map wordCase . unIdentifier
+
+-- | To @camelCase@
+toCamel :: Identifier String -> String
+toCamel (Identifier (x:xs)) = concat $ map toLower x:map wordCase xs
+
+-- | To @kebab-case@
+toKebab :: Identifier String -> String
+toKebab = concat . intersperse "-" . map (map toLower) . unIdentifier
+
+-- | To @snake_Case@
+toSnake :: Identifier String -> String
+toSnake = concat . intersperse "_" . unIdentifier
+
+-- | To @quiet_snake_case@
+toQuietSnake :: Identifier String -> String
+toQuietSnake = map toLower . toSnake
+
+-- | To @SCREAMING_SNAKE_CASE@
+toScreamingSnake :: Identifier String -> String
+toScreamingSnake = map toUpper . toSnake
+
+-- | To @word Case@
+toWords :: Identifier String -> String
+toWords = unwords . unIdentifier
+
+-- | Directly convert to @PascalCase@ through 'fromAny'
+pascal :: String -> String
+pascal = toPascal . fromAny
+
+-- | Directly convert to @camelCase@ through 'fromAny'
+camel :: String -> String
+camel = toCamel . fromAny
+
+-- | Directly convert to @snake_Case@ through 'fromAny'
+snake :: String -> String
+snake = toSnake . fromAny
+
+-- | Directly convert to @quiet_snake_case@ through 'fromAny'
+quietSnake :: String -> String
+quietSnake = toQuietSnake . fromAny
+
+-- | Directly convert to @SCREAMING_SNAKE_CASE@ through 'fromAny'
+screamingSnake :: String -> String
+screamingSnake = toScreamingSnake . fromAny
+
+-- | Directly convert to @kebab-case@ through 'fromAny'
+kebab :: String -> String
+kebab = toKebab . fromAny
+
+-- | Directly convert to @word Case@ through 'fromAny'
+wordify :: String -> String
+wordify = toWords . fromAny
+
+-- | Drop the first word from a parsed identifier. Typical usage is between
+-- parsing and writing, e.g.: @toKebab . dropPrefix . fromAny $ "strHelloWorld" == "hello-world"@
+dropPrefix :: Identifier String -> Identifier String
+dropPrefix = Identifier . drop 1 . unIdentifier
diff --git a/tests/Spec.hs b/tests/Spec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Spec.hs
@@ -0,0 +1,250 @@
+module Main where
+
+import Test.Tasty
+import Test.Tasty.HUnit
+import Text.Casing
+
+main :: IO ()
+main = defaultMain tests
+
+tests :: TestTree
+tests = testGroup "tests"
+  [ testGroup "parsing"
+    [ testGroup "fromHumps"
+      [ testCase "no splits 1" $ do
+          assertEqual ""
+            (Identifier ["hello"])
+            (fromHumps "hello")
+      , testCase "no splits 2" $ do
+          assertEqual ""
+            (Identifier ["Hello"])
+            (fromHumps "Hello")
+      , testCase "non-alpha, no split" $ do
+          assertEqual ""
+            (Identifier ["-hello"])
+            (fromHumps "-hello")
+      , testCase "non-alpha, split" $ do
+          assertEqual ""
+            (Identifier ["-hello-", "World"])
+            (fromHumps "-hello-World")
+      , testCase "simple split camel" $ do
+          assertEqual ""
+            (Identifier ["hello", "World"])
+            (fromHumps "helloWorld")
+      , testCase "simple split pascal" $ do
+          assertEqual ""
+            (Identifier ["Hello", "World"])
+            (fromHumps "HelloWorld")
+      , testCase "single letter abbrev split" $ do
+          assertEqual ""
+            (Identifier ["Hello", "A", "World"])
+            (fromHumps "HelloAWorld")
+      , testCase "multi letter abbrev split" $ do
+          assertEqual ""
+            (Identifier ["Hello", "X", "M", "L", "World"])
+            (fromHumps "HelloXMLWorld")
+      , testCase "single letter upper" $ do
+          assertEqual ""
+            (Identifier ["A"])
+            (fromHumps "A")
+      , testCase "single letter lower" $ do
+          assertEqual ""
+            (Identifier ["a"])
+            (fromHumps "a")
+      ]
+    , testGroup "fromKebab"
+      [ testCase "no splits" $ do
+          assertEqual ""
+            (Identifier ["hello"])
+            (fromKebab "hello")
+      , testCase "single split" $ do
+          assertEqual ""
+            (Identifier ["hello", "world"])
+            (fromKebab "hello-world")
+      , testCase "leading split" $ do
+          assertEqual ""
+            (Identifier ["world"])
+            (fromKebab "-world")
+      , testCase "trailing split" $ do
+          assertEqual ""
+            (Identifier ["world"])
+            (fromKebab "world-")
+      , testCase "multiple dashes" $ do
+          assertEqual ""
+            (Identifier ["hello", "world"])
+            (fromKebab "hello---world")
+      ]
+    , testGroup "fromSnake"
+      [ testCase "no splits" $ do
+          assertEqual ""
+            (Identifier ["hello"])
+            (fromSnake "hello")
+      , testCase "single split" $ do
+          assertEqual ""
+            (Identifier ["hello", "world"])
+            (fromSnake "hello_world")
+      , testCase "leading split" $ do
+          assertEqual ""
+            (Identifier ["world"])
+            (fromSnake "_world")
+      , testCase "trailing split" $ do
+          assertEqual ""
+            (Identifier ["world"])
+            (fromSnake "world_")
+      , testCase "multiple dashes" $ do
+          assertEqual ""
+            (Identifier ["hello", "world"])
+            (fromSnake "hello___world")
+      ]
+    , testGroup "fromWords"
+      [ testCase "no splits" $ do
+          assertEqual ""
+            (Identifier ["hello"])
+            (fromWords "hello")
+      , testCase "single split" $ do
+          assertEqual ""
+            (Identifier ["hello", "world"])
+            (fromWords "hello world")
+      , testCase "leading split" $ do
+          assertEqual ""
+            (Identifier ["world"])
+            (fromWords " world")
+      , testCase "trailing split" $ do
+          assertEqual ""
+            (Identifier ["world"])
+            (fromWords "world ")
+      , testCase "multiple dashes" $ do
+          assertEqual ""
+            (Identifier ["hello", "world"])
+            (fromWords "hello   world")
+      ]
+    , testGroup "fromAny"
+      [ testCase "no splits 1" $ do
+          assertEqual ""
+            (Identifier ["hello"])
+            (fromAny "hello")
+      , testCase "no splits 2" $ do
+          assertEqual ""
+            (Identifier ["Hello"])
+            (fromAny "Hello")
+      , testCase "simple split camel" $ do
+          assertEqual ""
+            (Identifier ["hello", "World"])
+            (fromAny "helloWorld")
+      , testCase "simple split pascal" $ do
+          assertEqual ""
+            (Identifier ["Hello", "World"])
+            (fromAny "HelloWorld")
+      , testCase "single letter abbrev split" $ do
+          assertEqual ""
+            (Identifier ["Hello", "A", "World"])
+            (fromAny "HelloAWorld")
+      , testCase "multi letter abbrev split" $ do
+          assertEqual ""
+            (Identifier ["Hello", "X", "M", "L", "World"])
+            (fromAny "HelloXMLWorld")
+      , testCase "single letter upper" $ do
+          assertEqual ""
+            (Identifier ["A"])
+            (fromAny "A")
+      , testCase "single letter lower" $ do
+          assertEqual ""
+            (Identifier ["a"])
+            (fromAny "a")
+      , testCase "no splits" $ do
+          assertEqual ""
+            (Identifier ["hello"])
+            (fromAny "hello")
+      , testCase "single split" $ do
+          assertEqual ""
+            (Identifier ["hello", "world"])
+            (fromAny "hello-world")
+      , testCase "leading split" $ do
+          assertEqual ""
+            (Identifier ["world"])
+            (fromAny "-world")
+      , testCase "trailing split" $ do
+          assertEqual ""
+            (Identifier ["world"])
+            (fromAny "world-")
+      , testCase "multiple dashes" $ do
+          assertEqual ""
+            (Identifier ["hello", "world"])
+            (fromAny "hello---world")
+      , testCase "no splits" $ do
+          assertEqual ""
+            (Identifier ["hello"])
+            (fromAny "hello")
+      , testCase "single split" $ do
+          assertEqual ""
+            (Identifier ["hello", "world"])
+            (fromAny "hello_world")
+      , testCase "leading split" $ do
+          assertEqual ""
+            (Identifier ["world"])
+            (fromAny "_world")
+      , testCase "trailing split" $ do
+          assertEqual ""
+            (Identifier ["world"])
+            (fromAny "world_")
+      , testCase "multiple dashes" $ do
+          assertEqual ""
+            (Identifier ["hello", "world"])
+            (fromAny "hello___world")
+      , testCase "no splits" $ do
+          assertEqual ""
+            (Identifier ["hello"])
+            (fromAny "hello")
+      , testCase "single split" $ do
+          assertEqual ""
+            (Identifier ["hello", "world"])
+            (fromAny "hello world")
+      , testCase "leading split" $ do
+          assertEqual ""
+            (Identifier ["world"])
+            (fromAny " world")
+      , testCase "trailing split" $ do
+          assertEqual ""
+            (Identifier ["world"])
+            (fromAny "world ")
+      , testCase "multiple dashes" $ do
+          assertEqual ""
+            (Identifier ["hello", "world"])
+            (fromAny "hello   world")
+      ]
+    ]
+  , testGroup "writing"
+    [ testGroup "toPascal"
+      [ testCase "toPascal simple" $ do
+          assertEqual ""
+            "HelloWorld"
+            (toPascal $ Identifier ["hello", "world"])
+      ]
+    , testGroup "toCamel"
+      [ testCase "toCamel simple" $ do
+          assertEqual ""
+            "helloWorld"
+            (toCamel $ Identifier ["hello", "world"])
+      ]
+    , testGroup "toKebab"
+      [ testCase "toKebab simple" $ do
+          assertEqual ""
+            "hello-world"
+            (toKebab $ Identifier ["hello", "world"])
+      ]
+    , testGroup "toSnake"
+      [ testCase "toSnake simple" $ do
+          assertEqual ""
+            "hello_world"
+            (toSnake $ Identifier ["hello", "world"])
+      , testCase "toScreamingSnake simple" $ do
+          assertEqual ""
+            "HELLO_WORLD"
+            (toScreamingSnake $ Identifier ["hello", "world"])
+      , testCase "toQuietSnake simple" $ do
+          assertEqual ""
+            "hello_world"
+            (toQuietSnake $ Identifier ["Hello", "World"])
+      ]
+    ]
+  ]
