diff --git a/ASCII/QuasiQuoters.hs b/ASCII/QuasiQuoters.hs
deleted file mode 100644
--- a/ASCII/QuasiQuoters.hs
+++ /dev/null
@@ -1,120 +0,0 @@
-module ASCII.QuasiQuoters ( char, string ) where
-
-import ASCII.Char                  ( Char )
-import ASCII.Superset              ( toCharMaybe, toCharListMaybe )
-import ASCII.TemplateHaskell       ( isCharExp, isCharPat, isStringExp, isStringPat )
-import Control.Monad               ( (>=>), return )
-import Control.Monad.Fail          ( MonadFail, fail )
-import Data.Maybe                  ( Maybe (..) )
-import Language.Haskell.TH.Quote   ( QuasiQuoter (..) )
-import Language.Haskell.TH.Syntax  ( Q, Exp, Pat )
-
-import qualified Data.Char   as Unicode
-import qualified Data.String as Unicode
-
-{- $setup
-
->>> :set -fno-warn-overlapping-patterns
->>> :set -XNoQuasiQuotes
->>> :set -XNoViewPatterns
->>> import qualified ASCII
->>> import qualified Data.String
->>> import qualified Data.Text
->>> import qualified Data.ByteString.Builder
->>> import Data.Word (Word8)
->>> import ASCII.Char
->>> import ASCII.QuasiQuoters
-
--}
-
-{- | Produces an expression or a pattern corresponding to an ASCII character.
-
-The result will have an 'ASCII.Superset.CharSuperset' constraint; since this is polymorphic, use with a type signature to specify the particular you want is recommended.
-
-The quasi-quoted string must consist of a single character that is within the ASCII character set.
-
->>> :set -XQuasiQuotes
-
->>> [char|e|] :: ASCII.Char
-SmallLetterE
-
->>> [char|e|] :: Word8
-101
-
-Use in a pattern context requires enabling the @ViewPatterns@ language extension.
-
->>> :set -XViewPatterns
-
->>> case Tilde of [char|@|] -> 1; [char|~|] -> 2; _ -> 3
-2
-
--}
-
-char :: QuasiQuoter
-char = expPatQQ requireOneAscii isCharExp isCharPat
-
-{- | Produces an expression or a pattern corresponding to an ASCII string.
-
-The result will have an 'ASCII.Superset.StringSuperset' constraint; since this is polymorphic, use with a type signature to specify the particular you want is recommended.
-
-The quasi-quoted string must consist only of characters are within the ASCII character set.
-
->>> :set -XQuasiQuotes
-
->>> [string|Hello!|] :: [ASCII.Char]
-[CapitalLetterH,SmallLetterE,SmallLetterL,SmallLetterL,SmallLetterO,ExclamationMark]
-
->>> [string|Hello!|] :: Data.String.String
-"Hello!"
-
->>> [string|Hello!|] :: Data.Text.Text
-"Hello!"
-
->>> Data.ByteString.Builder.toLazyByteString [string|Hello!|]
-"Hello!"
-
-
-Use in a pattern context requires enabling the @ViewPatterns@ language extension.
-
->>> :set -XViewPatterns
-
->>> case [CapitalLetterH, SmallLetterI] of [string|Bye|] -> 1; [string|Hi|] -> 2; _ -> 3
-2
-
--}
-
-string :: QuasiQuoter
-string = expPatQQ requireAsciiList isStringExp isStringPat
-
-requireOneAscii :: Unicode.String -> Q Char
-requireOneAscii = requireOne >=> requireAscii
-
-oneMaybe :: [a] -> Maybe a
-oneMaybe xs = case xs of [x] -> Just x; _ -> Nothing
-
-requireOne :: Unicode.String -> Q Unicode.Char
-requireOne = oneMaybe || "Must be exactly one character."
-
-requireAscii :: Unicode.Char -> Q Char
-requireAscii = toCharMaybe || "Must be an ASCII character."
-
-requireAsciiList :: Unicode.String -> Q [Char]
-requireAsciiList = toCharListMaybe || "Must be only ASCII characters."
-
-(||) :: (a -> Maybe b) -> Unicode.String -> a -> Q b
-f || msg = \a -> case f a of Just b -> return b; Nothing -> fail msg
-
-expPatQQ :: (Unicode.String -> Q a) -> (a -> Q Exp) -> (a -> Q Pat) -> QuasiQuoter
-expPatQQ f a b =
-    QuasiQuoter
-        { quoteExp  = f >=> a
-        , quotePat  = f >=> b
-        , quoteType = notType
-        , quoteDec  = notDec
-        }
-
-notType :: MonadFail m => a -> m b
-notType _ = fail "Cannot be used in a type context."
-
-notDec :: MonadFail m => a -> m b
-notDec _ = fail "Cannot be used in a declaration context."
diff --git a/ASCII/TemplateHaskell.hs b/ASCII/TemplateHaskell.hs
deleted file mode 100644
--- a/ASCII/TemplateHaskell.hs
+++ /dev/null
@@ -1,135 +0,0 @@
-module ASCII.TemplateHaskell (
-
-    {- * Characters          -} charExp,     charPat,
-    {- * Character lists     -} charListExp, charListPat,
-    {- * Character supersets -} isCharExp,   isCharPat,
-    {- * String supersets    -} isStringExp, isStringPat
-
-  ) where
-
-import qualified ASCII.Char       as  ASCII
-import qualified ASCII.Superset   as  S
-
-import Data.Data                  ( Data )
-import Data.Maybe                 ( Maybe (..) )
-import Language.Haskell.TH.Syntax ( Q, Exp, Pat, dataToExpQ, dataToPatQ )
-
-{- $setup
-
->>> :set -XNoViewPatterns
->>> import ASCII.Char (Char (..))
->>> import ASCII.Refinement (ASCII)
->>> import ASCII.Superset (toCharOrFail)
->>> import ASCII.TemplateHaskell (charExp)
->>> import Control.Monad ((>>=))
->>> import Data.Word (Word8)
-
--}
-
-exp :: Data a => a -> Q Exp
-exp = dataToExpQ (\_ -> Nothing)
-
-pat :: Data a => a -> Q Pat
-pat = dataToPatQ (\_ -> Nothing)
-
-{- |
-
->>> $(toCharOrFail 'F' >>= charExp)
-CapitalLetterF
-
->>> $(toCharOrFail '\DEL' >>= charExp)
-Delete
-
--}
-
-charExp :: ASCII.Char -> Q Exp
-charExp = exp
-
-{- |
-
->>> :{
->>> case SmallLetterS of
->>>     $(toCharOrFail 'r' >>= charPat) -> 1
->>>     $(toCharOrFail 's' >>= charPat) -> 2
->>>     _                               -> 3
->>> :}
-2
-
-This is the same as:
-
->>> :{
->>> case SmallLetterS of
->>>     SmallLetterR -> 1
->>>     SmallLetterS -> 2
->>>     _            -> 3
->>> :}
-2
-
--}
-
-charPat :: ASCII.Char -> Q Pat
-charPat = pat
-
-{- |
-
->>> $(charListExp [CapitalLetterH, SmallLetterI])
-[CapitalLetterH,SmallLetterI]
-
--}
-
-charListExp :: [ASCII.Char] -> Q Exp
-charListExp = exp
-
-{- |
-
->>> :{
->>> case [CapitalLetterH, SmallLetterI] of
->>>     $(charListPat [CapitalLetterH, SmallLetterA]) -> 1
->>>     $(charListPat [CapitalLetterH, SmallLetterI]) -> 2
->>>     _                                             -> 3
->>> :}
-2
-
--}
-
-charListPat :: [ASCII.Char] -> Q Pat
-charListPat = pat
-
-{- |
-
->>> $(isCharExp CapitalLetterA) :: ASCII.Char
-CapitalLetterA
-
->>> $(isCharExp CapitalLetterA) :: Word8
-65
-
->>> $(isCharExp CapitalLetterA) :: ASCII Word8
-asciiUnsafe 65
-
--}
-
-isCharExp :: ASCII.Char -> Q Exp
-isCharExp x = [| S.fromChar $(charExp x) |]
-
-{- |
-
->>> :set -XViewPatterns
-
->>> :{
->>> case (66 :: Word8) of
->>>     $(isCharPat CapitalLetterA) -> 1
->>>     $(isCharPat CapitalLetterB) -> 2
->>>     _                           -> 3
->>> :}
-2
-
--}
-
-isCharPat :: ASCII.Char -> Q Pat
-isCharPat x = [p| (S.toCharMaybe -> Just $(charPat x)) |]
-
-isStringExp :: [ASCII.Char] -> Q Exp
-isStringExp xs = [| S.fromCharList $(charListExp xs) |]
-
-isStringPat :: [ASCII.Char] -> Q Pat
-isStringPat xs = [p| (S.toCharListMaybe -> Just $(charListPat xs)) |]
diff --git a/ascii-th.cabal b/ascii-th.cabal
--- a/ascii-th.cabal
+++ b/ascii-th.cabal
@@ -1,7 +1,7 @@
 cabal-version: 2.0
 
 name: ascii-th
-version: 1.0.0.4
+version: 1.0.0.6
 synopsis: Template Haskell support for ASCII
 category: Data, Text
 
@@ -18,8 +18,6 @@
 
 build-type: Simple
 
-tested-with: GHC == 9.0.1, GHC == 8.10.1, GHC == 8.8.1, GHC == 8.6.5, GHC == 8.4.3
-
 extra-source-files: changelog.txt
 
 source-repository head
@@ -33,6 +31,7 @@
     default-extensions: QuasiQuotes
     default-extensions: ViewPatterns
     ghc-options: -Wall
+    hs-source-dirs: library
 
     build-depends: ascii-char       ^>= 1.0
     build-depends: ascii-superset   ^>= 1.0
@@ -42,3 +41,16 @@
 
     exposed-modules: ASCII.TemplateHaskell
     exposed-modules: ASCII.QuasiQuoters
+
+test-suite test
+    type: exitcode-stdio-1.0
+    default-language: Haskell2010
+    default-extensions: NoImplicitPrelude
+    default-extensions: OverloadedStrings
+    default-extensions: QuasiQuotes
+    default-extensions: TemplateHaskell
+    default-extensions: ViewPatterns
+    ghc-options: -Wall -fno-warn-overlapping-patterns
+    hs-source-dirs: test
+    main-is: test.hs
+    build-depends: ascii-char, ascii-superset, ascii-th, base, bytestring, text
diff --git a/changelog.txt b/changelog.txt
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,3 +1,4 @@
 1.0.0.0 - 2020-05-05 - Initial release
 1.0.0.2 - 2020-05-18 - Support GHC 8.10
 1.0.0.4 - 2021-02-10 - Support GHC 9.0
+1.0.0.6 - 2021-09-26 - Add a test suite
diff --git a/library/ASCII/QuasiQuoters.hs b/library/ASCII/QuasiQuoters.hs
new file mode 100644
--- /dev/null
+++ b/library/ASCII/QuasiQuoters.hs
@@ -0,0 +1,105 @@
+module ASCII.QuasiQuoters ( char, string ) where
+
+import ASCII.Char                  ( Char )
+import ASCII.Superset              ( toCharMaybe, toCharListMaybe )
+import ASCII.TemplateHaskell       ( isCharExp, isCharPat, isStringExp, isStringPat )
+import Control.Monad               ( (>=>), return )
+import Control.Monad.Fail          ( MonadFail, fail )
+import Data.Maybe                  ( Maybe (..) )
+import Language.Haskell.TH.Quote   ( QuasiQuoter (..) )
+import Language.Haskell.TH.Syntax  ( Q, Exp, Pat )
+
+import qualified Data.Char   as Unicode
+import qualified Data.String as Unicode
+
+{- | Produces an expression or a pattern corresponding to an ASCII character.
+
+The result will have an 'ASCII.Superset.CharSuperset' constraint; since this is polymorphic, use with a type signature to specify the particular you want is recommended.
+
+The quasi-quoted string must consist of a single character that is within the ASCII character set.
+
+>>> :set -XQuasiQuotes
+
+>>> [char|e|] :: ASCII.Char
+SmallLetterE
+
+>>> [char|e|] :: Word8
+101
+
+Use in a pattern context requires enabling the @ViewPatterns@ language extension.
+
+>>> :set -XViewPatterns
+
+>>> case Tilde of [char|@|] -> 1; [char|~|] -> 2; _ -> 3
+2
+
+-}
+
+char :: QuasiQuoter
+char = expPatQQ requireOneAscii isCharExp isCharPat
+
+{- | Produces an expression or a pattern corresponding to an ASCII string.
+
+The result will have an 'ASCII.Superset.StringSuperset' constraint; since this is polymorphic, use with a type signature to specify the particular you want is recommended.
+
+The quasi-quoted string must consist only of characters are within the ASCII character set.
+
+>>> :set -XQuasiQuotes
+
+>>> [string|Hello!|] :: [ASCII.Char]
+[CapitalLetterH,SmallLetterE,SmallLetterL,SmallLetterL,SmallLetterO,ExclamationMark]
+
+>>> [string|Hello!|] :: Data.String.String
+"Hello!"
+
+>>> [string|Hello!|] :: Data.Text.Text
+"Hello!"
+
+>>> Data.ByteString.Builder.toLazyByteString [string|Hello!|]
+"Hello!"
+
+
+Use in a pattern context requires enabling the @ViewPatterns@ language extension.
+
+>>> :set -XViewPatterns
+
+>>> case [CapitalLetterH, SmallLetterI] of [string|Bye|] -> 1; [string|Hi|] -> 2; _ -> 3
+2
+
+-}
+
+string :: QuasiQuoter
+string = expPatQQ requireAsciiList isStringExp isStringPat
+
+requireOneAscii :: Unicode.String -> Q Char
+requireOneAscii = requireOne >=> requireAscii
+
+oneMaybe :: [a] -> Maybe a
+oneMaybe xs = case xs of [x] -> Just x; _ -> Nothing
+
+requireOne :: Unicode.String -> Q Unicode.Char
+requireOne = oneMaybe || "Must be exactly one character."
+
+requireAscii :: Unicode.Char -> Q Char
+requireAscii = toCharMaybe || "Must be an ASCII character."
+
+requireAsciiList :: Unicode.String -> Q [Char]
+requireAsciiList = toCharListMaybe || "Must be only ASCII characters."
+
+(||) :: (a -> Maybe b) -> Unicode.String -> a -> Q b
+f || msg = \a -> case f a of Just b -> return b; Nothing -> fail msg
+
+expPatQQ :: (Unicode.String -> Q a) -> (a -> Q Exp) -> (a -> Q Pat) -> QuasiQuoter
+expPatQQ f a b =
+    QuasiQuoter
+        { quoteExp  = f >=> a
+        , quotePat  = f >=> b
+        , quoteType = notType
+        , quoteDec  = notDec
+        }
+
+notType :: MonadFail m => a -> m b
+notType _ = fail "Cannot be used in a type context."
+
+notDec :: MonadFail m => a -> m b
+notDec _ = fail "Cannot be used in a declaration context."
diff --git a/library/ASCII/TemplateHaskell.hs b/library/ASCII/TemplateHaskell.hs
new file mode 100644
--- /dev/null
+++ b/library/ASCII/TemplateHaskell.hs
@@ -0,0 +1,123 @@
+module ASCII.TemplateHaskell (
+
+    {- * Characters          -} charExp,     charPat,
+    {- * Character lists     -} charListExp, charListPat,
+    {- * Character supersets -} isCharExp,   isCharPat,
+    {- * String supersets    -} isStringExp, isStringPat
+
+  ) where
+
+import qualified ASCII.Char       as  ASCII
+import qualified ASCII.Superset   as  S
+
+import Data.Data                  ( Data )
+import Data.Maybe                 ( Maybe (..) )
+import Language.Haskell.TH.Syntax ( Q, Exp, Pat, dataToExpQ, dataToPatQ )
+
+exp :: Data a => a -> Q Exp
+exp = dataToExpQ (\_ -> Nothing)
+
+pat :: Data a => a -> Q Pat
+pat = dataToPatQ (\_ -> Nothing)
+
+{- |
+
+>>> $(toCharOrFail 'F' >>= charExp)
+CapitalLetterF
+
+>>> $(toCharOrFail '\DEL' >>= charExp)
+Delete
+
+-}
+
+charExp :: ASCII.Char -> Q Exp
+charExp = exp
+
+{- |
+
+>>> :{
+>>> case SmallLetterS of
+>>>     $(toCharOrFail 'r' >>= charPat) -> 1
+>>>     $(toCharOrFail 's' >>= charPat) -> 2
+>>>     _                               -> 3
+>>> :}
+2
+
+This is the same as:
+
+>>> :{
+>>> case SmallLetterS of
+>>>     SmallLetterR -> 1
+>>>     SmallLetterS -> 2
+>>>     _            -> 3
+>>> :}
+2
+
+-}
+
+charPat :: ASCII.Char -> Q Pat
+charPat = pat
+
+{- |
+
+>>> $(charListExp [CapitalLetterH, SmallLetterI])
+[CapitalLetterH,SmallLetterI]
+
+-}
+
+charListExp :: [ASCII.Char] -> Q Exp
+charListExp = exp
+
+{- |
+
+>>> :{
+>>> case [CapitalLetterH, SmallLetterI] of
+>>>     $(charListPat [CapitalLetterH, SmallLetterA]) -> 1
+>>>     $(charListPat [CapitalLetterH, SmallLetterI]) -> 2
+>>>     _                                             -> 3
+>>> :}
+2
+
+-}
+
+charListPat :: [ASCII.Char] -> Q Pat
+charListPat = pat
+
+{- |
+
+>>> $(isCharExp CapitalLetterA) :: ASCII.Char
+CapitalLetterA
+
+>>> $(isCharExp CapitalLetterA) :: Word8
+65
+
+>>> $(isCharExp CapitalLetterA) :: ASCII Word8
+asciiUnsafe 65
+
+-}
+
+isCharExp :: ASCII.Char -> Q Exp
+isCharExp x = [| S.fromChar $(charExp x) |]
+
+{- |
+
+>>> :set -XViewPatterns
+
+>>> :{
+>>> case (66 :: Word8) of
+>>>     $(isCharPat CapitalLetterA) -> 1
+>>>     $(isCharPat CapitalLetterB) -> 2
+>>>     _                           -> 3
+>>> :}
+2
+
+-}
+
+isCharPat :: ASCII.Char -> Q Pat
+isCharPat x = [p| (S.toCharMaybe -> Just $(charPat x)) |]
+
+isStringExp :: [ASCII.Char] -> Q Exp
+isStringExp xs = [| S.fromCharList $(charListExp xs) |]
+
+isStringPat :: [ASCII.Char] -> Q Pat
+isStringPat xs = [p| (S.toCharListMaybe -> Just $(charListPat xs)) |]
diff --git a/test/test.hs b/test/test.hs
new file mode 100644
--- /dev/null
+++ b/test/test.hs
@@ -0,0 +1,73 @@
+module Main where
+
+import ASCII.QuasiQuoters
+import ASCII.TemplateHaskell
+
+import ASCII.Char (Char (..))
+import ASCII.Refinement (ASCII, asciiUnsafe)
+import ASCII.Superset (toCharOrFail)
+
+import Control.Applicative (Applicative (..))
+import Control.Monad (Monad (..))
+import Data.Bool (Bool (..))
+import Data.Eq (Eq ((==)))
+import Data.Function ((.), ($))
+import Data.Functor (Functor (..))
+import Data.List (intercalate, map, null)
+import Data.Semigroup ((<>))
+import Data.String (String)
+import Data.Text (Text)
+import Data.Word (Word8)
+import Numeric.Natural (Natural)
+import Prelude (Integer)
+import System.Exit (die)
+import System.IO (IO, putStrLn)
+import Text.Show (show)
+
+import qualified Data.ByteString.Builder as BS.Builder
+
+main :: IO ()
+main = dieIfFailures $ do
+    test 1 $ ([char|e|] :: Char) == SmallLetterE
+    test 2 $ ([char|e|] :: Word8) == 101
+    test 3 $ (case Tilde of [char|@|] -> 1 :: Integer; [char|~|] -> 2; _ -> 3) == 2
+    test 4 $ ([string|Hello!|] :: [Char]) == [CapitalLetterH, SmallLetterE, SmallLetterL, SmallLetterL, SmallLetterO, ExclamationMark]
+    test 5 $ ([string|Hello!|] :: String) == "Hello!"
+    test 6 $ ([string|Hello!|] :: Text) == "Hello!"
+    test 7 $ (BS.Builder.toLazyByteString [string|Hello!|] == "Hello!")
+    test 8 $ (case [CapitalLetterH, SmallLetterI] of [string|Bye|] -> 1 :: Integer; [string|Hi|] -> 2; _ -> 3) == 2
+    test 9 $ $(toCharOrFail 'F' >>= charExp) == CapitalLetterF
+    test 10 $ $(toCharOrFail '\DEL' >>= charExp) == Delete
+    test 11 $ case SmallLetterS of { $(toCharOrFail 'r' >>= charPat) -> 1 :: Integer; $(toCharOrFail 's' >>= charPat) -> 2; _ -> 3 } == 2
+    test 12 $ $(charListExp [CapitalLetterH, SmallLetterI]) == [CapitalLetterH, SmallLetterI]
+    test 13 $ case [CapitalLetterH, SmallLetterI] of { $(charListPat [CapitalLetterH, SmallLetterA]) -> 1 :: Integer; $(charListPat [CapitalLetterH, SmallLetterI]) -> 2; _ -> 3 } == 2
+    test 14 $ ($(isCharExp CapitalLetterA) :: Char) == CapitalLetterA
+    test 15 $ ($(isCharExp CapitalLetterA) :: Word8) == 65
+    test 16 $ ($(isCharExp CapitalLetterA) :: ASCII Word8) == asciiUnsafe 65
+    test 17 $ case (66 :: Word8) of { $(isCharPat CapitalLetterA) -> 1 :: Integer; $(isCharPat CapitalLetterB) -> 2; _ -> 3 } == 2
+
+dieIfFailures :: Failures a -> IO a
+dieIfFailures (Failures fs x) =
+    if null fs
+        then do putStrLn "💯"; return x
+        else die $ intercalate " " (map (("🔥" <> ) . show) fs)
+
+type TestNumber = Natural
+
+test :: TestNumber -> Bool -> Failures ()
+test n t = Failures (if t then [] else [n]) ()
+
+data Failures a = Failures [TestNumber] a
+
+instance Functor Failures
+  where
+    fmap f (Failures a x) = Failures a (f x)
+
+instance Applicative Failures
+  where
+    pure x = Failures [] x
+    Failures a f <*> Failures b x = Failures (a <> b) (f x)
+
+instance Monad Failures
+  where
+    Failures a x >>= f = let Failures b y = f x in Failures (a <> b) y
