diff --git a/ascii.cabal b/ascii.cabal
--- a/ascii.cabal
+++ b/ascii.cabal
@@ -1,11 +1,12 @@
-cabal-version: 2.0
+cabal-version: 3.0
 
 name: ascii
-version: 1.1.1.2
+version: 1.1.1.4
 synopsis: The ASCII character set and encoding
 category: Data, Text
 
-description: This package provides a variety of ways to work with ASCII text.
+description:
+    This package provides a variety of ways to work with ASCII text.
 
 license: Apache-2.0
 license-file: license.txt
@@ -13,60 +14,76 @@
 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
     ghc-options: -Wall
+
+    default-extensions:
+        NoImplicitPrelude
+
+    build-depends:
+        base >= 4.11 && < 4.17
+      , text ^>= 1.2.3
+
+library
+    import: base
     hs-source-dirs: library
 
-    build-depends: base       >= 4.11 && < 4.17
-    build-depends: bytestring ^>= 0.10 || ^>= 0.11
-    build-depends: text       ^>= 1.2.3
+    build-depends:
+        bytestring ^>= 0.10 || ^>= 0.11
 
     exposed-modules: ASCII
 
     build-depends: ascii-char ^>= 1.0
-    reexported-modules: ASCII.Char
+    reexported-modules:
+        ASCII.Char
 
     build-depends: ascii-group ^>= 1.0
-    reexported-modules: ASCII.Group
+    reexported-modules:
+        ASCII.Group
 
     build-depends: ascii-case ^>= 1.0
-    reexported-modules: ASCII.Case
+    reexported-modules:
+        ASCII.Case
 
     build-depends: ascii-predicates ^>= 1.0
-    reexported-modules: ASCII.Predicates
-    reexported-modules: ASCII.Lists
-    reexported-modules: ASCII.ListsAndPredicates
+    reexported-modules:
+        ASCII.Predicates
+      , ASCII.Lists
+      , ASCII.ListsAndPredicates
 
     build-depends: ascii-superset ^>= 1.0.1
-    reexported-modules: ASCII.Superset
-    reexported-modules: ASCII.Isomorphism
-    reexported-modules: ASCII.Refinement
-    reexported-modules: ASCII.Lift
+    reexported-modules:
+        ASCII.Superset
+      , ASCII.Isomorphism
+      , ASCII.Refinement
+      , ASCII.Lift
 
     build-depends: ascii-th ^>= 1.0
-    reexported-modules: ASCII.TemplateHaskell
-    reexported-modules: ASCII.QuasiQuoters
+    reexported-modules:
+        ASCII.TemplateHaskell
+      , ASCII.QuasiQuoters
 
-test-suite test
+test-suite test-ascii
+    import: base
     type: exitcode-stdio-1.0
-    default-language: Haskell2010
-    default-extensions: NoImplicitPrelude
-    default-extensions: OverloadedStrings
-    default-extensions: QuasiQuotes
-    ghc-options: -Wall
     hs-source-dirs: test
-    main-is: test.hs
-    build-depends: ascii, base, text
+    main-is: Main.hs
+
+    build-depends:
+        ascii
+      , hedgehog ^>= 1.0 || ^>= 1.1
+
+    default-extensions:
+        OverloadedStrings
+        QuasiQuotes
+        TemplateHaskell
diff --git a/changelog.txt b/changelog.txt
deleted file mode 100644
--- a/changelog.txt
+++ /dev/null
@@ -1,47 +0,0 @@
-1.0.0.0 - 2020-05-05 - Initial release
-
-1.0.0.2 - 2020-05-18 - Support GHC 8.10
-
-1.0.1.0 - 2021-01-27
-
-    New functions:
-        - byteStringToUnicodeStringMaybe
-        - unicodeStringToByteStringMaybe
-        - byteListToUnicodeStringMaybe
-        - unicodeStringToByteListMaybe
-        - convertCharMaybe
-        - convertCharOrFail
-        - convertStringMaybe
-        - convertStringOrFail
-
-1.0.1.2 - 2021-02-09 - Support bytestring-0.11
-
-1.0.1.4 - 2021-02-10 - Support GHC 9.0
-
-1.0.1.6 - 2021-09-26
-
-    - Add a test suite
-    - Raise 'text' lower bound to 1.2.3
-
-1.1.0.0 - 2021-09-26
-
-    The dependency on the 'data-ascii' package is removed,
-    and the following modules are no longer re-exported:
-      - Data.Ascii
-      - Data.Ascii.Blaze
-      - Data.Ascii.ByteString
-      - Data.Ascii.Word8
-
-1.1.1.0 - 2021-09-26
-
-    New functions:
-        - isAlphaNum
-        - isLetter
-        - isDigit
-        - isOctDigit
-        - isHexDigit
-        - isSpace
-        - isPunctuation
-        - isSymbol
-
-1.1.1.2 - 2022-01-09 - Support GHC 9.2
diff --git a/library/ASCII.hs b/library/ASCII.hs
--- a/library/ASCII.hs
+++ b/library/ASCII.hs
@@ -45,35 +45,35 @@
   )
   where
 
-import ASCII.Case          ( Case (..) )
-import ASCII.Char          ( Char )
-import ASCII.Group         ( Group (..) )
-import ASCII.Isomorphism   ( CharIso, StringIso )
-import ASCII.Lift          ( Lift )
-import ASCII.QuasiQuoters  ( char, string )
-import ASCII.Refinement    ( ASCII, validateChar, validateString )
-import ASCII.Superset      ( CharSuperset, StringSuperset )
+import ASCII.Case (Case (..))
+import ASCII.Char (Char)
+import ASCII.Group (Group (..))
+import ASCII.Isomorphism (CharIso, StringIso)
+import ASCII.Lift (Lift)
+import ASCII.QuasiQuoters (char, string)
+import ASCII.Refinement (ASCII, validateChar, validateString)
+import ASCII.Superset (CharSuperset, StringSuperset)
 
-import Control.Monad       ( (>=>) )
-import Control.Monad.Fail  ( MonadFail )
-import Data.Bool           ( Bool (..) )
-import Data.Foldable       ( any )
-import Data.Function       ( (.) )
-import Data.Int            ( Int )
-import Data.Maybe          ( Maybe, maybe )
-import Data.Word           ( Word8 )
+import Control.Monad ((>=>))
+import Control.Monad.Fail (MonadFail)
+import Data.Bool (Bool (..))
+import Data.Foldable (any)
+import Data.Function ((.))
+import Data.Int (Int)
+import Data.Maybe (Maybe, maybe)
+import Data.Word (Word8)
 
-import qualified  ASCII.Case
-import qualified  ASCII.Group
-import qualified  ASCII.Isomorphism
-import qualified  ASCII.Lift
-import qualified  ASCII.Predicates
-import qualified  ASCII.Superset
+import qualified ASCII.Case
+import qualified ASCII.Group
+import qualified ASCII.Isomorphism
+import qualified ASCII.Lift
+import qualified ASCII.Predicates
+import qualified ASCII.Superset
 
-import qualified  Data.ByteString  as  BS
-import qualified  Data.Char        as  Unicode
-import qualified  Data.String      as  Unicode
-import qualified  Data.Text        as  Text
+import qualified Data.ByteString as BS
+import qualified Data.Char as Unicode
+import qualified Data.String as Unicode
+import qualified Data.Text as Text
 
 {- $char
 
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,107 @@
+module Main (main) where
+
+import ASCII
+
+import ASCII.Char (Char (..))
+import ASCII.Refinement (asciiUnsafe)
+
+import Control.Monad (Monad (..), when)
+import Data.Bool (Bool (..), not)
+import Data.Function (($))
+import Data.Int (Int)
+import Data.List (map)
+import Data.Maybe (Maybe (..))
+import Data.Text (Text)
+import Data.Word (Word8)
+import System.Exit (exitFailure)
+import System.IO (IO)
+
+import Hedgehog (Property, assert, checkParallel, discover, property, withTests,
+                 (===))
+
+main :: IO ()
+main = checkParallel $$(discover) >>= \ok -> when (not ok) exitFailure
+
+prop_letter_printable :: Property
+prop_letter_printable = withTests 1 $ property $
+    charGroup CapitalLetterA === Printable
+
+prop_eot_control :: Property
+prop_eot_control = withTests 1 $ property $
+    charGroup EndOfTransmission === Control
+
+prop_eot_not_printable :: Property
+prop_eot_not_printable = withTests 1 $ property $
+    assert $ not $ inGroup Printable EndOfTransmission
+
+prop_is_eot_control :: Property
+prop_is_eot_control = withTests 1 $ property $
+    assert $ inGroup Control EndOfTransmission
+
+prop_inGroup_printable :: Property
+prop_inGroup_printable = withTests 1 $ property $
+    map (inGroup Printable) ([-1, 5, 65, 97, 127, 130] :: [Int]) === [False, False, True, True, False, False]
+
+prop_cases :: Property
+prop_cases = withTests 1 $ property $
+    map letterCase [SmallLetterA, CapitalLetterA, ExclamationMark] === [Just LowerCase, Just UpperCase, Nothing]
+
+prop_cases_string_qq :: Property
+prop_cases_string_qq = withTests 1 $ property $
+    map letterCase ([string|Hey!|] :: [ASCII Word8]) === [Just UpperCase, Just LowerCase, Just LowerCase, Nothing]
+
+prop_is_upper_char :: Property
+prop_is_upper_char = withTests 1 $ property $
+    map (isCase UpperCase) [SmallLetterA, CapitalLetterA, ExclamationMark] === [False, True, False]
+
+prop_is_upper_string_qq :: Property
+prop_is_upper_string_qq = withTests 1 $ property $
+    map (isCase UpperCase) ([string|Hey!|] :: [ASCII Word8]) === [True, False, False, False]
+
+prop_is_upper_int :: Property
+prop_is_upper_int = withTests 1 $ property $
+    map (isCase UpperCase) ([-1, 65, 97, 150] :: [Int]) === [False, True, False, False]
+
+prop_to_upper_letter :: Property
+prop_to_upper_letter = withTests 1 $ property $
+    toCaseChar UpperCase SmallLetterA === CapitalLetterA
+
+prop_to_upper_char_qq :: Property
+prop_to_upper_char_qq = withTests 1 $ property $
+    ([char|a|] :: ASCII Word8, toCaseChar UpperCase [char|a|] :: ASCII Word8) === (asciiUnsafe 97, asciiUnsafe 65)
+
+prop_to_upper_various :: Property
+prop_to_upper_various = withTests 1 $ property $
+    toCaseString UpperCase [CapitalLetterH,SmallLetterE,SmallLetterY,ExclamationMark] === [CapitalLetterH, CapitalLetterE, CapitalLetterY, ExclamationMark]
+
+prop_to_upper_string_qq :: Property
+prop_to_upper_string_qq = withTests 1 $ property $
+    (toCaseString UpperCase [string|Hey!|] :: ASCII Text) === asciiUnsafe "HEY!"
+
+prop_to_int :: Property
+prop_to_int = withTests 1 $ property $
+    map charToInt [Null, CapitalLetterA, SmallLetterA, Delete] === [0, 65, 97, 127]
+
+prop_to_word8 :: Property
+prop_to_word8 = withTests 1 $ property $
+    map charToWord8 [Null, CapitalLetterA, SmallLetterA, Delete] === [0, 65, 97, 127]
+
+prop_to_text :: Property
+prop_to_text = withTests 1 $ property $
+    charListToText [CapitalLetterH, SmallLetterI, ExclamationMark] === "Hi!"
+
+prop_lift_to_word8 :: Property
+prop_lift_to_word8 = withTests 1 $ property $
+    (lift CapitalLetterA :: Word8) === 65
+
+prop_lift_to_text :: Property
+prop_lift_to_text = withTests 1 $ property $
+    (lift [CapitalLetterH,SmallLetterI,ExclamationMark] :: Text) === "Hi!"
+
+prop_bytes_to_string :: Property
+prop_bytes_to_string = withTests 1 $ property $
+    ASCII.byteListToUnicodeStringMaybe [0x48, 0x54, 0x54, 0x50] === Just "HTTP"
+
+prop_bytes_to_string_fail :: Property
+prop_bytes_to_string_fail = withTests 1 $ property $
+    ASCII.byteListToUnicodeStringMaybe [0x48, 0x54, 0x54, 0x80] === Nothing
diff --git a/test/test.hs b/test/test.hs
deleted file mode 100644
--- a/test/test.hs
+++ /dev/null
@@ -1,72 +0,0 @@
-module Main where
-
-import ASCII
-
-import ASCII.Char (Char (..))
-import ASCII.Refinement (asciiUnsafe)
-
-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.Int (Int)
-import Data.List (intercalate, map, null)
-import Data.Maybe (Maybe (..))
-import Data.Semigroup ((<>))
-import Data.Text (Text)
-import Data.Word (Word8)
-import Numeric.Natural (Natural)
-import System.Exit (die)
-import System.IO (IO, putStrLn)
-import Text.Show (show)
-
-main :: IO ()
-main = dieIfFailures $ do
-    test 1 $ map charGroup [CapitalLetterA, EndOfTransmission] == [Printable, Control]
-    test 2 $ inGroup Printable EndOfTransmission == False
-    test 3 $ inGroup Control EndOfTransmission == True
-    test 4 $ map (inGroup Printable) ([-1, 5, 65, 97, 127, 130] :: [Int]) == [False, False, True, True, False, False]
-    test 5 $ map letterCase [SmallLetterA, CapitalLetterA, ExclamationMark] == [Just LowerCase, Just UpperCase, Nothing]
-    test 6 $ map letterCase ([string|Hey!|] :: [ASCII Word8]) == [Just UpperCase, Just LowerCase, Just LowerCase, Nothing]
-    test 7 $ map (isCase UpperCase) [SmallLetterA, CapitalLetterA, ExclamationMark] == [False, True, False]
-    test 8 $ map (isCase UpperCase) ([string|Hey!|] :: [ASCII Word8]) == [True, False, False, False]
-    test 9 $ map (isCase UpperCase) ([-1, 65, 97, 150] :: [Int]) == [False, True, False, False]
-    test 10 $ toCaseChar UpperCase SmallLetterA == CapitalLetterA
-    test 11 $ ([char|a|] :: ASCII Word8, toCaseChar UpperCase [char|a|] :: ASCII Word8) == (asciiUnsafe 97, asciiUnsafe 65)
-    test 12 $ toCaseString UpperCase [CapitalLetterH,SmallLetterE,SmallLetterY,ExclamationMark] == [CapitalLetterH, CapitalLetterE, CapitalLetterY, ExclamationMark]
-    test 13 $ (toCaseString UpperCase [string|Hey!|] :: ASCII Text) == asciiUnsafe "HEY!"
-    test 14 $ map charToInt [Null, CapitalLetterA, SmallLetterA, Delete] == [0, 65, 97, 127]
-    test 15 $ map charToWord8 [Null, CapitalLetterA, SmallLetterA, Delete] == [0, 65, 97, 127]
-    test 16 $ charListToText [CapitalLetterH, SmallLetterI, ExclamationMark] == "Hi!"
-    test 17 $ (lift CapitalLetterA :: Word8) == 65
-    test 18 $ (lift [CapitalLetterH,SmallLetterI,ExclamationMark] :: Text) == "Hi!"
-    test 19 $ ASCII.byteListToUnicodeStringMaybe [0x48, 0x54, 0x54, 0x50] == Just "HTTP"
-    test 20 $ ASCII.byteListToUnicodeStringMaybe [0x48, 0x54, 0x54, 0x80] == Nothing
-
-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
