diff --git a/README.md b/README.md
deleted file mode 100644
--- a/README.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# hex-text
-
-`hex-text` is a small library for converting between `ByteString`s and their representations as hexidecimal numbers encoded as `Text`.
-
-# Motivation
-
-When using Stripe for payments, Stripe sends a signature as a hexidecimal `Text` value. The `cryptonite` package can be used to verify the signature, but it requires `ByteString` values, not `Text`.
-
-# Example usage
-
-A `ByteString` is a list of bytes. A byte is a number between 0 and 255, represented by the `Word8` type. In a fixed-width hexidecimal representation, the lowest byte 0 is represented by the hex string `00`, and the greatest byte 255 is represented by the hex string `ff`. So, for example, the `ByteString` consisting of bytes \[ 1, 2, 3, 253, 254, 255 \] is represented as `010203fdfeff`.
-
-```haskell
-λ> import Text.Hex (encodeHex)
-λ> import Data.ByteString (pack)
-
-λ> (encodeHex . pack) [1, 2, 3, 253, 254, 255]
-"010203fdfeff"
-```
diff --git a/Setup.hs b/Setup.hs
deleted file mode 100644
--- a/Setup.hs
+++ /dev/null
@@ -1,3 +0,0 @@
-import Distribution.Simple
-
-main = defaultMain
diff --git a/changelog.md b/changelog.md
new file mode 100644
--- /dev/null
+++ b/changelog.md
@@ -0,0 +1,9 @@
+### 0.1.0.8 (2022-12-31)
+
+Switch testing to `hspec`
+
+Raise minimum required GHC to 8.10
+
+### 0.1.0.7 (2022-11-10)
+
+Start of changelog
diff --git a/hex-text.cabal b/hex-text.cabal
--- a/hex-text.cabal
+++ b/hex-text.cabal
@@ -1,7 +1,7 @@
 cabal-version: 3.0
 
 name: hex-text
-version: 0.1.0.7
+version: 0.1.0.8
 category: Text
 
 synopsis: ByteString-Text hexidecimal conversions
@@ -19,10 +19,7 @@
 license: MIT
 license-file: license.txt
 
-build-type: Simple
-
-extra-source-files:
-    README.md
+extra-source-files: *.md
 
 source-repository head
     type: git
@@ -30,11 +27,12 @@
 
 common base
     default-language: Haskell2010
+    default-extensions: NoImplicitPrelude
     build-depends:
-        base ^>= 4.9 || ^>= 4.10 || ^>= 4.11 || ^>= 4.12 || ^>= 4.13 || ^>= 4.14 || ^>= 4.15 || ^>= 4.16 || ^>= 4.17
-      , base16-bytestring ^>= 1.0
-      , bytestring ^>= 0.10 || ^>= 0.11
-      , text ^>= 1.2 || ^>= 2.0
+        base ^>= 4.14 || ^>= 4.15 || ^>= 4.16 || ^>= 4.17
+      , base16-bytestring ^>= 1.0.2
+      , bytestring ^>= 0.10.12 || ^>= 0.11
+      , text ^>= 1.2.4 || ^>= 2.0
 
 library
     import: base
@@ -47,4 +45,6 @@
     hs-source-dirs: test
     type: exitcode-stdio-1.0
     main-is: Main.hs
-    build-depends: hex-text
+    build-depends:
+        hex-text
+      , hspec ^>= 2.8.5 || ^>= 2.9 || ^>= 2.10
diff --git a/readme.md b/readme.md
new file mode 100644
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,27 @@
+# hex-text
+
+`hex-text` is a small library for converting between `ByteString`s and their
+representations as hexidecimal numbers encoded as `Text`.
+
+## Motivation
+
+When using Stripe for payments, Stripe sends a signature as a hexidecimal `Text`
+value. The `cryptonite` package can be used to verify the signature, but it
+requires `ByteString` values, not `Text`.
+
+## Example usage
+
+A `ByteString` is a list of bytes. A byte is a number between 0 and 255,
+represented by the `Word8` type. In a fixed-width hexidecimal representation,
+the lowest byte 0 is represented by the hex string `00`, and the greatest byte
+255 is represented by the hex string `ff`. So, for example, the `ByteString`
+consisting of bytes \[ 1, 2, 3, 253, 254, 255 \] is represented as
+`010203fdfeff`.
+
+```haskell
+λ> import Text.Hex (encodeHex)
+λ> import Data.ByteString (pack)
+
+λ> (encodeHex . pack) [1, 2, 3, 253, 254, 255]
+"010203fdfeff"
+```
diff --git a/src/Text/Hex.hs b/src/Text/Hex.hs
--- a/src/Text/Hex.hs
+++ b/src/Text/Hex.hs
@@ -19,6 +19,8 @@
 
     ) where
 
+import Prelude (either, Maybe (..), const)
+
 -- base16-bytestring
 import qualified Data.ByteString.Base16 as Base16
 import qualified Data.ByteString.Base16.Lazy as LazyBase16
diff --git a/test/Main.hs b/test/Main.hs
--- a/test/Main.hs
+++ b/test/Main.hs
@@ -1,54 +1,43 @@
-import Data.Foldable (Foldable (fold))
-import Data.Semigroup ()
-import Data.String (IsString (fromString))
-import Numeric.Natural (Natural)
-import System.Exit (die)
 import Text.Hex (decodeHex, encodeHex, lazilyEncodeHex)
 
+import Test.Hspec (hspec, describe, it, shouldBe, Spec)
+
+import Prelude ((.), ($), cycle, IO, fmap, Maybe (..))
+import Data.String (fromString)
+
 import qualified Data.ByteString as ByteString
 import qualified Data.ByteString.Lazy as LazyByteString
 import qualified Data.Text as Text
 import qualified Data.Text.Lazy as LazyText
 
 main :: IO ()
-main = run tests
-
-tests :: [Natural]
-tests = encodeHexTests ++ decodeHexTests ++ lazilyEncodeHexTests
-
-run :: [Natural] -> IO ()
-run [] = putStrLn "Okay"
-run xs = die $ "Test failures: " ++ show (xs :: [Natural])
-
-infix 0 #
-(#) :: a -> Bool -> [a]
-x # True  = []
-x # False = [x]
+main = hspec $ do
+    encodeHexTests
+    decodeHexTests
+    lazilyEncodeHexTests
 
-encodeHexTests :: [Natural]
-encodeHexTests = fold
-  [ 1 # (encodeHex . ByteString.singleton) 192
-      == fromString "c0"
-  , 2 # (encodeHex . ByteString.singleton) 168
-      == fromString "a8"
-  , 3 # (encodeHex . ByteString.pack) [192, 168, 1, 2]
-      == fromString "c0a80102"
-  ]
+encodeHexTests :: Spec
+encodeHexTests = describe "encodeHex" $ do
+    it "can encode a single byte" $ do
+        let f = encodeHex . ByteString.singleton
+        f 192 `shouldBe` fromString "c0"
+        f 168 `shouldBe` fromString "a8"
+    it "can encode many bytes" $ do
+        let f = encodeHex . ByteString.pack
+        f [192, 168, 1, 2] `shouldBe` fromString "c0a80102"
 
-decodeHexTests :: [Natural]
-decodeHexTests = fold
-  [ 4 # (fmap ByteString.unpack . decodeHex . Text.pack) "c0a80102"
-      == Just [192,168,1,2]
-  , 5 # (fmap ByteString.unpack . decodeHex . Text.pack) "c0a8010"
-      == Nothing
-  , 6 # (fmap ByteString.unpack . decodeHex . Text.pack) "x0a80102"
-      == Nothing
-  , 7 # (fmap ByteString.unpack . decodeHex . Text.pack) "C0A80102"
-      == Just [192,168,1,2]
-  ]
+decodeHexTests :: Spec
+decodeHexTests = describe "decodeHex" $ do
+    let f = fmap ByteString.unpack . decodeHex . Text.pack
+    it "can decode" $ do
+        f "c0a80102" `shouldBe` Just [192,168,1,2]
+        f "C0A80102" `shouldBe` Just [192,168,1,2]
+    it "can fail" $ do
+        f "c0a8010" `shouldBe` Nothing
+        f "x0a80102" `shouldBe` Nothing
 
-lazilyEncodeHexTests :: [Natural]
-lazilyEncodeHexTests = fold
-  [ 8 # (LazyText.take 8 . lazilyEncodeHex . LazyByteString.pack . cycle) [1, 2, 3]
-      == fromString "01020301"
-  ]
+lazilyEncodeHexTests :: Spec
+lazilyEncodeHexTests = describe "lazilyEncodeHex" $ do
+    let f = lazilyEncodeHex . LazyByteString.pack
+    it "can decode part of an infinite list" $
+        (LazyText.take 8 . f . cycle) [1, 2, 3] `shouldBe` fromString "01020301"
