diff --git a/ascii-th.cabal b/ascii-th.cabal
--- a/ascii-th.cabal
+++ b/ascii-th.cabal
@@ -1,11 +1,13 @@
-cabal-version: 2.0
+cabal-version: 3.0
 
 name: ascii-th
-version: 1.0.0.8
+version: 1.0.0.10
 synopsis: Template Haskell support for ASCII
 category: Data, Text
 
-description: This package defines Template Haskell support for ASCII, including quasi-quoters for expressing ASCII strings.
+description:
+    This package defines Template Haskell support for ASCII,
+    including quasi-quoters for expressing ASCII strings.
 
 license: Apache-2.0
 license-file: license.txt
@@ -13,44 +15,53 @@
 author: Chris Martin
 maintainer: Chris Martin, Julie Moronuki
 
-homepage:    https://github.com/typeclasses/ascii
+homepage: https://github.com/typeclasses/ascii
 bug-Reports: https://github.com/typeclasses/ascii/issues
 
 build-type: Simple
 
-extra-source-files: changelog.txt
-
 source-repository head
-    type:     git
+    type: git
     location: git://github.com/typeclasses/ascii.git
 
-library
+common base
     default-language: Haskell2010
-    default-extensions: NoImplicitPrelude
-    default-extensions: TemplateHaskell
-    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
+    default-extensions:
+        NoImplicitPrelude
+        QuasiQuotes
+        TemplateHaskell
+        ViewPatterns
 
-    build-depends: base             >= 4.11 && < 4.17
-    build-depends: template-haskell >= 2.13 && < 2.19
+    build-depends:
+        ascii-char ^>= 1.0
+      , ascii-superset ^>= 1.0
+      , base >= 4.11 && < 4.17
 
-    exposed-modules: ASCII.TemplateHaskell
-    exposed-modules: ASCII.QuasiQuoters
+library
+    import: base
+    hs-source-dirs: library
 
-test-suite test
+    build-depends:
+        template-haskell >= 2.13 && < 2.19
+
+    exposed-modules:
+        ASCII.TemplateHaskell
+      , ASCII.QuasiQuoters
+
+test-suite test-ascii-th
+    import: base
     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
+    ghc-options: -fno-warn-overlapping-patterns
     hs-source-dirs: test
-    main-is: test.hs
-    build-depends: ascii-char, ascii-superset, ascii-th, base, bytestring, text
+    main-is: Main.hs
+
+    default-extensions:
+        OverloadedStrings
+
+    build-depends:
+        ascii-th
+      , bytestring ^>= 0.10 || ^>= 0.11
+      , hedgehog ^>= 1.0 || ^>= 1.1
+      , text ^>= 1.2.3
diff --git a/changelog.txt b/changelog.txt
deleted file mode 100644
--- a/changelog.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-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
-1.0.0.8 - 2022-01-09 - Support GHC 9.2
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,109 @@
+module Main (main) where
+
+import ASCII.QuasiQuoters (char, string)
+import ASCII.TemplateHaskell (charExp, charListExp, charListPat, charPat,
+                              isCharExp, isCharPat)
+
+import ASCII.Char (Char (..))
+import ASCII.Refinement (ASCII, asciiUnsafe)
+import ASCII.Superset (toCharOrFail)
+
+import Control.Monad (Monad (..), when)
+import Data.Bool (not)
+import Data.Function (($))
+import Data.String (String)
+import Data.Text (Text)
+import Data.Word (Word8)
+import Prelude (Integer)
+import System.Exit (exitFailure)
+import System.IO (IO)
+
+import qualified Data.ByteString.Builder as BS.Builder
+
+import Hedgehog (Property, checkParallel, discover, property, withTests, (===))
+
+main :: IO ()
+main = checkParallel $$(discover) >>= \ok -> when (not ok) exitFailure
+
+prop_smallE :: Property
+prop_smallE = withTests 1 $ property $
+    ([char|e|] :: Char) === SmallLetterE
+
+prop_smallE_word8 :: Property
+prop_smallE_word8 = withTests 1 $ property $
+    ([char|e|] :: Word8) === 101
+
+prop_tilde_pattern :: Property
+prop_tilde_pattern = withTests 1 $ property $
+    2 === case Tilde of
+        [char|@|] -> 1 :: Integer
+        [char|~|] -> 2
+        _ -> 3
+
+prop_hello_list :: Property
+prop_hello_list = withTests 1 $ property $
+    ([string|Hello!|] :: [Char]) === [CapitalLetterH, SmallLetterE, SmallLetterL, SmallLetterL, SmallLetterO, ExclamationMark]
+
+prop_hello_string :: Property
+prop_hello_string = withTests 1 $ property $
+    ([string|Hello!|] :: String) === "Hello!"
+
+prop_hello_text :: Property
+prop_hello_text = withTests 1 $ property $
+    ([string|Hello!|] :: Text) === "Hello!"
+
+prop_string_qq_expression :: Property
+prop_string_qq_expression = withTests 1 $ property $
+    BS.Builder.toLazyByteString [string|Hello!|] === "Hello!"
+
+prop_string_qq_pattern :: Property
+prop_string_qq_pattern = withTests 1 $ property $
+    2 === case [CapitalLetterH, SmallLetterI] of
+        [string|Bye|] -> 1 :: Integer
+        [string|Hi|] -> 2
+        _ -> 3
+
+prop_char_splice_letter :: Property
+prop_char_splice_letter = withTests 1 $ property $
+    $(toCharOrFail 'F' >>= charExp) === CapitalLetterF
+
+prop_char_splice_del :: Property
+prop_char_splice_del = withTests 1 $ property $
+    $(toCharOrFail '\DEL' >>= charExp) === Delete
+
+prop_char_splice_pattern :: Property
+prop_char_splice_pattern = withTests 1 $ property $
+    2 === case SmallLetterS of
+        $(toCharOrFail 'r' >>= charPat) -> 1 :: Integer
+        $(toCharOrFail 's' >>= charPat) -> 2
+        _ -> 3
+
+prop_char_list_splice :: Property
+prop_char_list_splice = withTests 1 $ property $
+    $(charListExp [CapitalLetterH, SmallLetterI]) === [CapitalLetterH, SmallLetterI]
+
+prop_char_list_splice_pattern :: Property
+prop_char_list_splice_pattern = withTests 1 $ property $
+    2 === case [CapitalLetterH, SmallLetterI] of
+        $(charListPat [CapitalLetterH, SmallLetterA]) -> 1 :: Integer
+        $(charListPat [CapitalLetterH, SmallLetterI]) -> 2
+        _ -> 3
+
+prop_polymorphic_char_splice :: Property
+prop_polymorphic_char_splice = withTests 1 $ property $
+    ($(isCharExp CapitalLetterA) :: Char) === CapitalLetterA
+
+prop_polymorphic_char_splice_word8 :: Property
+prop_polymorphic_char_splice_word8 = withTests 1 $ property $
+    ($(isCharExp CapitalLetterA) :: Word8) === 65
+
+prop_polymorphic_char_splice_ascii_word8 :: Property
+prop_polymorphic_char_splice_ascii_word8 = withTests 1 $ property $
+    ($(isCharExp CapitalLetterA) :: ASCII Word8) === asciiUnsafe 65
+
+prop_polymorphic_char_splice_pattern :: Property
+prop_polymorphic_char_splice_pattern = withTests 1 $ property $
+    2 === case (66 :: Word8) of
+        $(isCharPat CapitalLetterA) -> 1 :: Integer
+        $(isCharPat CapitalLetterB) -> 2
+        _ -> 3
diff --git a/test/test.hs b/test/test.hs
deleted file mode 100644
--- a/test/test.hs
+++ /dev/null
@@ -1,73 +0,0 @@
-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
