diff --git a/ascii-superset.cabal b/ascii-superset.cabal
--- a/ascii-superset.cabal
+++ b/ascii-superset.cabal
@@ -1,11 +1,15 @@
-cabal-version: 2.0
+cabal-version: 3.0
 
 name: ascii-superset
-version: 1.0.1.10
+version: 1.0.1.12
 synopsis: Representing ASCII with refined supersets
 category: Data, Text
 
-description: This package defines classes which describe what subset of a type is valid as ASCII, as well as a type constructor representing a value of a superset that is known to be valid ASCII.
+description:
+    This package defines classes which describe what
+    subset of a type is valid as ASCII, as well as a
+    type constructor representing a value of a
+    superset that is known to be valid ASCII.
 
 license: Apache-2.0
 license-file: license.txt
@@ -13,47 +17,60 @@
 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: StandaloneDeriving
-    default-extensions: DerivingStrategies
-    default-extensions: GeneralizedNewtypeDeriving
-    default-extensions: DeriveDataTypeable
-    default-extensions: DeriveGeneric
-    ghc-options: -Wall -fno-warn-unused-imports
+    ghc-options: -Wall
+
+    default-extensions:
+        NoImplicitPrelude
+
+    build-depends:
+        ascii-char ^>= 1.0
+      , base >= 4.11 && < 4.17
+      , text ^>= 1.2.3
+
+library
+    import: base
+    ghc-options: -fno-warn-unused-imports
     hs-source-dirs: library
 
-    build-depends: ascii-char ^>= 1.0
+    default-extensions:
+        DeriveDataTypeable
+        DeriveGeneric
+        DerivingStrategies
+        GeneralizedNewtypeDeriving
+        StandaloneDeriving
 
-    build-depends: base       >= 4.11 && < 4.17
-    build-depends: bytestring ^>= 0.10 || ^>= 0.11
-    build-depends: hashable   >= 1.2  && < 1.5
-    build-depends: text       ^>= 1.2.3
+    build-depends:
+        bytestring ^>= 0.10 || ^>= 0.11
+      , hashable >= 1.2 && < 1.5
 
-    exposed-modules: ASCII.Superset
-    exposed-modules: ASCII.Isomorphism
-    exposed-modules: ASCII.Refinement
-    exposed-modules: ASCII.Lift
+    exposed-modules:
+        ASCII.Superset
+      , ASCII.Isomorphism
+      , ASCII.Refinement
+      , ASCII.Lift
 
-test-suite test
+test-suite test-ascii-superset
+    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-char, ascii-superset, base, text
+    main-is: Main.hs
+
+    default-extensions:
+        OverloadedStrings
+        QuasiQuotes
+        TemplateHaskell
+
+    build-depends:
+        ascii-superset
+      , hedgehog ^>= 1.0 || ^>= 1.1
diff --git a/changelog.txt b/changelog.txt
deleted file mode 100644
--- a/changelog.txt
+++ /dev/null
@@ -1,26 +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-01-25 - Add some comments
-
-1.0.1.0 - 2021-01-27
-
-    New functions:
-        - 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.0.1.8 - 2021-11-13 - Support hashable 1.4
-
-1.0.0.10 - 2022-01-09 - Support GHC 9.2
diff --git a/library/ASCII/Isomorphism.hs b/library/ASCII/Isomorphism.hs
--- a/library/ASCII/Isomorphism.hs
+++ b/library/ASCII/Isomorphism.hs
@@ -1,9 +1,9 @@
 module ASCII.Isomorphism ( CharIso (..), asChar, StringIso (..) ) where
 
-import ASCII.Char     ( Char )
-import ASCII.Superset ( CharSuperset (..), StringSuperset (..) )
-import Data.Function  ( id, (.) )
-import Data.List      ( map )
+import ASCII.Char (Char)
+import ASCII.Superset (CharSuperset (..), StringSuperset (..))
+import Data.Function (id, (.))
+import Data.List (map)
 
 class CharSuperset char => CharIso char
   where
diff --git a/library/ASCII/Lift.hs b/library/ASCII/Lift.hs
--- a/library/ASCII/Lift.hs
+++ b/library/ASCII/Lift.hs
@@ -2,9 +2,9 @@
 
 module ASCII.Lift ( Lift (..) ) where
 
-import ASCII.Char       ( Char )
-import ASCII.Refinement ( ASCII )
-import ASCII.Superset   ( CharSuperset, StringSuperset )
+import ASCII.Char (Char)
+import ASCII.Refinement (ASCII)
+import ASCII.Superset (CharSuperset, StringSuperset)
 
 import qualified ASCII.Refinement as R
 import qualified ASCII.Superset as S
diff --git a/library/ASCII/Refinement.hs b/library/ASCII/Refinement.hs
--- a/library/ASCII/Refinement.hs
+++ b/library/ASCII/Refinement.hs
@@ -7,23 +7,23 @@
   where
 
 import qualified ASCII.Char as ASCII
-import qualified ASCII.Superset as S
 import qualified ASCII.Isomorphism as I
+import qualified ASCII.Superset as S
 
-import ASCII.Superset    ( CharSuperset, StringSuperset )
-import Data.Bool         ( Bool (..) )
-import Data.Data         ( Data )
-import Data.Eq           ( Eq )
-import Data.Function     ( (.), ($), id )
-import Data.Hashable     ( Hashable )
-import Data.List         ( map )
-import Data.Maybe        ( Maybe (..) )
-import Data.Monoid       ( Monoid )
-import Data.Ord          ( Ord, (>) )
-import Data.Semigroup    ( Semigroup )
-import GHC.Generics      ( Generic )
-import Prelude           ( succ )
-import Text.Show         ( Show, showString, showsPrec, showParen, showList )
+import ASCII.Superset (CharSuperset, StringSuperset)
+import Data.Bool (Bool (..))
+import Data.Data (Data)
+import Data.Eq (Eq)
+import Data.Function (id, ($), (.))
+import Data.Hashable (Hashable)
+import Data.List (map)
+import Data.Maybe (Maybe (..))
+import Data.Monoid (Monoid)
+import Data.Ord (Ord, (>))
+import Data.Semigroup (Semigroup)
+import GHC.Generics (Generic)
+import Prelude (succ)
+import Text.Show (Show, showList, showParen, showString, showsPrec)
 
 {- | This type constructor indicates that a value from some ASCII superset is valid ASCII. The type parameter is the ASCII superset, which should be a type with an instance of either 'CharSuperset' or 'StringSuperset'.
 
diff --git a/library/ASCII/Superset.hs b/library/ASCII/Superset.hs
--- a/library/ASCII/Superset.hs
+++ b/library/ASCII/Superset.hs
@@ -10,27 +10,27 @@
 
     ) where
 
-import Control.Monad      ( return )
-import Control.Monad.Fail ( MonadFail (fail) )
-import Data.Bool          ( Bool, (&&) )
-import Data.Function      ( (.), id )
-import Data.Functor       ( fmap )
-import Data.Ord           ( (<=), (>=) )
-import Data.Maybe         ( Maybe (..) )
+import Control.Monad (return)
+import Control.Monad.Fail (MonadFail (fail))
+import Data.Bool (Bool, (&&))
+import Data.Function (id, (.))
+import Data.Functor (fmap)
+import Data.Maybe (Maybe (..))
+import Data.Ord ((<=), (>=))
 
-import qualified ASCII.Char               as  ASCII
-import qualified Data.Bool                as  Bool
-import qualified Data.ByteString          as  BS
-import qualified Data.ByteString.Lazy     as  LBS
-import qualified Data.ByteString.Builder  as  BSB
-import qualified Data.Char                as  Unicode
-import qualified Data.Int                 as  Int
-import qualified Data.List                as  List
-import qualified Data.Text                as  T
-import qualified Data.Text.Lazy           as  LT
-import qualified Data.Text.Lazy.Builder   as  TB
-import qualified Data.Word                as  Word
-import qualified Numeric.Natural          as  Nat
+import qualified ASCII.Char as ASCII
+import qualified Data.Bool as Bool
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Builder as BSB
+import qualified Data.ByteString.Lazy as LBS
+import qualified Data.Char as Unicode
+import qualified Data.Int as Int
+import qualified Data.List as List
+import qualified Data.Text as T
+import qualified Data.Text.Lazy as LT
+import qualified Data.Text.Lazy.Builder as TB
+import qualified Data.Word as Word
+import qualified Numeric.Natural as Nat
 import qualified Prelude
 
 
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,57 @@
+module Main (main) where
+
+import ASCII.Refinement (ASCII, asciiUnsafe)
+
+import qualified ASCII.Lift as Lift
+import qualified ASCII.Refinement as Refinement
+
+import ASCII.Char (Char (..))
+
+import Control.Monad (Monad (..), when)
+import Data.Bool (not)
+import Data.Function (($))
+import Data.Int (Int)
+import Data.List (map)
+import Data.Maybe (Maybe (..))
+import Data.String (String)
+import Data.Text (Text)
+import Data.Word (Word8)
+import System.Exit (exitFailure)
+import System.IO (IO)
+
+import Hedgehog (Property, checkParallel, discover, property, withTests, (===))
+
+main :: IO ()
+main = checkParallel $$(discover) >>= \ok -> when (not ok) exitFailure
+
+prop_lift_letter :: Property
+prop_lift_letter = withTests 1 $ property $
+    (Lift.lift CapitalLetterA :: Word8) === 65
+
+prop_lift_list :: Property
+prop_lift_list = withTests 1 $ property $
+    (Lift.lift [CapitalLetterH,SmallLetterI,ExclamationMark] :: Text) === "Hi!"
+
+prop_validate_char :: Property
+prop_validate_char = withTests 1 $ property $
+    (map Refinement.validateChar [-1, 65, 97, 128] :: [Maybe (ASCII Int)]) === [Nothing, Just (asciiUnsafe 65), Just (asciiUnsafe 97), Nothing]
+
+prop_from_char_list :: Property
+prop_from_char_list = withTests 1 $ property $
+    (Refinement.fromCharList [CapitalLetterH,SmallLetterI,ExclamationMark] :: ASCII Text) === asciiUnsafe "Hi!"
+
+prop_to_char_list :: Property
+prop_to_char_list = withTests 1 $ property $
+    Refinement.toCharList (Refinement.substituteString "Piñata" :: ASCII Text) === [CapitalLetterP, SmallLetterI, Substitute, SmallLetterA, SmallLetterT, SmallLetterA]
+
+prop_substitute_string :: Property
+prop_substitute_string = withTests 1 $ property $
+    (Refinement.substituteString "Cristóbal" :: ASCII Text) === asciiUnsafe "Crist\SUBbal"
+
+prop_validate_text :: Property
+prop_validate_text = withTests 1 $ property $
+    (map Refinement.validateString ["Hello", "Cristóbal"] :: [Maybe (ASCII Text)]) === [Just (asciiUnsafe "Hello"), Nothing]
+
+prop_validate_string :: Property
+prop_validate_string = withTests 1 $ property $
+    (map Refinement.validateString ["Hello", "Cristóbal"] :: [Maybe (ASCII String)]) === [Just (asciiUnsafe "Hello"), Nothing]
diff --git a/test/test.hs b/test/test.hs
deleted file mode 100644
--- a/test/test.hs
+++ /dev/null
@@ -1,65 +0,0 @@
-module Main where
-
-import ASCII.Refinement (ASCII, asciiUnsafe)
-
-import qualified ASCII.Lift as Lift
-import qualified ASCII.Refinement as Refinement
-
-import ASCII.Char (Char (..))
-
-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.String (String)
-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 $ (Lift.lift CapitalLetterA :: Word8) == 65
-    test 2 $ (Lift.lift [CapitalLetterH,SmallLetterI,ExclamationMark] :: Text) == "Hi!"
-
-    test 3 $ (map Refinement.validateChar [-1, 65, 97, 128] :: [Maybe (ASCII Int)]) == [Nothing, Just (asciiUnsafe 65), Just (asciiUnsafe 97), Nothing]
-    test 4 $ (Refinement.fromCharList [CapitalLetterH,SmallLetterI,ExclamationMark] :: ASCII Text) == asciiUnsafe "Hi!"
-    test 5 $ Refinement.toCharList (Refinement.substituteString "Piñata" :: ASCII Text) == [CapitalLetterP, SmallLetterI, Substitute, SmallLetterA, SmallLetterT, SmallLetterA]
-    test 6 $ (Refinement.substituteString "Cristóbal" :: ASCII Text) == asciiUnsafe "Crist\SUBbal"
-    test 7 $ (map Refinement.validateString ["Hello", "Cristóbal"] :: [Maybe (ASCII Text)]) == [Just (asciiUnsafe "Hello"), Nothing]
-    test 8 $ (map Refinement.validateString ["Hello", "Cristóbal"] :: [Maybe (ASCII String)]) == [Just (asciiUnsafe "Hello"), 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
