diff --git a/ASCII/Char.hs b/ASCII/Char.hs
deleted file mode 100644
--- a/ASCII/Char.hs
+++ /dev/null
@@ -1,139 +0,0 @@
-{- |
-
-The 'Char' type has 128 nullary constructors, listed in order according to each character's 7-bit numeric code.
-
--}
-
-module ASCII.Char
-  (
-    {- * The @Char@ type -} Char (..),
-    {- * Conversions with @Int@ -} toInt, fromIntMaybe, fromIntUnsafe,
-    {- * Enumeration -} allCharacters
-    {- * Notes -} {- $notes -}
-  )
-  where
-
-import Data.Bool     ( otherwise )
-import Data.Data     ( Data )
-import Data.Eq       ( Eq, (==), (/=) )
-import Data.Ord      ( Ord, (<), (>) )
-import Data.Hashable ( Hashable )
-import Data.Int      ( Int )
-import Data.Maybe    ( Maybe (..) )
-import Prelude       ( Enum, enumFromTo, toEnum, fromEnum,
-                       Bounded, minBound, maxBound )
-import Text.Show     ( Show )
-import GHC.Generics  ( Generic )
-
-import qualified Data.Char as C
-
-{- $setup
-
->>> import Prelude hiding (Char)
-
--}
-
--- | A character in the ASCII character set.
-
-data Char =
-      Null | StartOfHeading | StartOfText | EndOfText | EndOfTransmission | Enquiry | Acknowledgement | Bell | Backspace | HorizontalTab | LineFeed | VerticalTab | FormFeed | CarriageReturn | ShiftOut | ShiftIn | DataLinkEscape
-
-    | DeviceControl1 | DeviceControl2 | DeviceControl3 | DeviceControl4
-
-    | NegativeAcknowledgement | SynchronousIdle | EndOfTransmissionBlock | Cancel | EndOfMedium | Substitute | Escape
-
-    | FileSeparator | GroupSeparator | RecordSeparator | UnitSeparator
-
-    | Space | ExclamationMark | QuotationMark | NumberSign | DollarSign | PercentSign | Ampersand | Apostrophe | LeftParenthesis | RightParenthesis | Asterisk | PlusSign | Comma | HyphenMinus | FullStop | Slash
-
-    | Digit0 | Digit1 | Digit2 | Digit3 | Digit4 | Digit5 | Digit6 | Digit7 | Digit8 | Digit9
-
-    | Colon | Semicolon | LessThanSign | EqualsSign | GreaterThanSign | QuestionMark | AtSign
-
-    | CapitalLetterA | CapitalLetterB | CapitalLetterC | CapitalLetterD | CapitalLetterE | CapitalLetterF | CapitalLetterG | CapitalLetterH | CapitalLetterI | CapitalLetterJ | CapitalLetterK | CapitalLetterL | CapitalLetterM | CapitalLetterN | CapitalLetterO | CapitalLetterP | CapitalLetterQ | CapitalLetterR | CapitalLetterS | CapitalLetterT | CapitalLetterU | CapitalLetterV | CapitalLetterW | CapitalLetterX | CapitalLetterY | CapitalLetterZ
-
-    | LeftSquareBracket | Backslash | RightSquareBracket | Caret | Underscore | GraveAccent
-
-    | SmallLetterA | SmallLetterB | SmallLetterC | SmallLetterD | SmallLetterE | SmallLetterF | SmallLetterG | SmallLetterH | SmallLetterI | SmallLetterJ | SmallLetterK | SmallLetterL | SmallLetterM | SmallLetterN | SmallLetterO | SmallLetterP | SmallLetterQ | SmallLetterR | SmallLetterS | SmallLetterT | SmallLetterU | SmallLetterV | SmallLetterW | SmallLetterX | SmallLetterY | SmallLetterZ
-
-    | LeftCurlyBracket | VerticalLine | RightCurlyBracket | Tilde | Delete
-
--- | ASCII characters can be compared for equality using '(==)'. Comparisons are case-sensitive; @'SmallLetterA' '/=' 'CapitalLetterA'@.
-deriving stock instance Eq Char
-
--- | ASCII characters are ordered; for example, the letter /A/ is "less than" ('<') the letter /B/ because it appears earlier in the list. The ordering of ASCII characters is the same as the ordering of the corresponding Unicode 'C.Char's.
-deriving stock instance Ord Char
-
--- | The 'Enum' instance allows us to use range syntax, for example @['SmallLetterA' .. 'SmallLetterZ']@ is a list all lower-case letters from /a/ to /z/. Instead of 'toEnum' and 'fromEnum', consider using 'toInt' and 'fromIntMaybe'.
-deriving stock instance Enum Char
-
--- | The least character is 'Null', and the greatest character is 'Delete'. You can write @(['minBound' .. 'maxBound'] :: [ASCII.'Char'])@ to get a list of all the ASCII characters.
-deriving stock instance Bounded Char
-
--- | 'show' produces the name of a constructor. For example, the character @e@ is shown as “@SmallLetterE@”. See "ASCII.Char" for the complete list of constructor names.
-deriving stock instance Show Char
-
--- | The 'Data' instance allows ASCII characters to be used with generic programming in the “SYB” style. (See the <https://hackage.haskell.org/package/syb syb> package and the 2003 paper <https://www.microsoft.com/en-us/research/wp-content/uploads/2003/01/hmap.pdf Scrap Your Boilerplate> by Ralf Lämmel and Simon Peyton Jones.)
-deriving stock instance Data Char
-
--- | The 'Generic' instance allows ASCII characters to be used with generic programming in the “generic deriving” style. (See the <https://hackage.haskell.org/package/generic-data generic-data> package and the 2010 paper <http://dreixel.net/research/pdf/gdmh.pdf A generic deriving mechanism for Haskell> by José Pedro Magalhães, Atze Dijkstra, Johan Jeuring, and Andres Löh.)
-deriving stock instance Generic Char
-
--- | The 'Hashable' instance lets us collect ASCII characters in hash-based sets, and it lets us use ASCII characters as keys in hash-based maps. (See the @unordered-containers@ package.)
-deriving anyclass instance Hashable Char
-
-{- | Converts an ASCII character to its corresponding numeric value between 0 and 127.
-
->>> map toInt [Null, CapitalLetterA, SmallLetterA, Delete]
-[0,65,97,127]
-
--}
-
-toInt :: Char -> Int
-toInt = Prelude.fromEnum
-
-{- | Returns 'Just' the ASCII character corresponding to a numeric value between 0 and 127, or 'Nothing' for numbers outside this range.
-
->>> map fromIntMaybe [-1, 0, 65, 127, 128]
-[Nothing,Just Null,Just CapitalLetterA,Just Delete,Nothing]
-
--}
-
-fromIntMaybe :: Int -> Maybe Char
-fromIntMaybe x | x < 0     = Nothing
-               | x > 127   = Nothing
-               | otherwise = Just (fromIntUnsafe x)
-
-{- | The inverse of 'toInt'.
-
-This is marked as /unsafe/ because it is undefined for numbers below 0 or above 127. The safe variant of this function is 'fromIntMaybe'.
-
->>> map fromIntUnsafe [65, 66, 67]
-[CapitalLetterA,CapitalLetterB,CapitalLetterC]
-
--}
-
-fromIntUnsafe :: Int -> Char
-fromIntUnsafe = Prelude.toEnum
-
-allCharacters :: [Char]
-allCharacters = Prelude.enumFromTo Prelude.minBound Prelude.maxBound
-
-{- $notes
-
-There are 128 characters in total.
-
->>> length allCharacters
-128
-
-Null is the first character.
-
->>> minBound :: Char
-Null
-
-Delete is the last character.
-
->>> maxBound :: Char
-Delete
-
--}
diff --git a/ascii-char.cabal b/ascii-char.cabal
--- a/ascii-char.cabal
+++ b/ascii-char.cabal
@@ -1,7 +1,7 @@
 cabal-version: 2.0
 
 name: ascii-char
-version: 1.0.0.8
+version: 1.0.0.10
 synopsis: A Char type representing an ASCII character
 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
@@ -35,8 +33,20 @@
     default-extensions: StandaloneDeriving
     default-extensions: DerivingStrategies
     ghc-options: -Wall -fno-warn-unused-imports
+    hs-source-dirs: library
 
     build-depends: base     >= 4.11 && < 4.16
     build-depends: hashable >= 1.2  && < 1.4
 
     exposed-modules: ASCII.Char
+
+test-suite test
+    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, base
diff --git a/changelog.txt b/changelog.txt
--- a/changelog.txt
+++ b/changelog.txt
@@ -7,3 +7,5 @@
 1.0.0.6 - 2021-01-27 - Minor documentation fix
 
 1.0.0.8 - 2021-02-10 - Support GHC 9.0
+
+1.0.0.10 - 2021-09-26 - Add a test suite
diff --git a/library/ASCII/Char.hs b/library/ASCII/Char.hs
new file mode 100644
--- /dev/null
+++ b/library/ASCII/Char.hs
@@ -0,0 +1,133 @@
+{- |
+
+The 'Char' type has 128 nullary constructors, listed in order according to each character's 7-bit numeric code.
+
+-}
+
+module ASCII.Char
+  (
+    {- * The @Char@ type -} Char (..),
+    {- * Conversions with @Int@ -} toInt, fromIntMaybe, fromIntUnsafe,
+    {- * Enumeration -} allCharacters
+    {- * Notes -} {- $notes -}
+  )
+  where
+
+import Data.Bool     ( otherwise )
+import Data.Data     ( Data )
+import Data.Eq       ( Eq, (==), (/=) )
+import Data.Ord      ( Ord, (<), (>) )
+import Data.Hashable ( Hashable )
+import Data.Int      ( Int )
+import Data.Maybe    ( Maybe (..) )
+import Prelude       ( Enum, enumFromTo, toEnum, fromEnum,
+                       Bounded, minBound, maxBound )
+import Text.Show     ( Show )
+import GHC.Generics  ( Generic )
+
+import qualified Data.Char as C
+
+-- | A character in the ASCII character set.
+
+data Char =
+      Null | StartOfHeading | StartOfText | EndOfText | EndOfTransmission | Enquiry | Acknowledgement | Bell | Backspace | HorizontalTab | LineFeed | VerticalTab | FormFeed | CarriageReturn | ShiftOut | ShiftIn | DataLinkEscape
+
+    | DeviceControl1 | DeviceControl2 | DeviceControl3 | DeviceControl4
+
+    | NegativeAcknowledgement | SynchronousIdle | EndOfTransmissionBlock | Cancel | EndOfMedium | Substitute | Escape
+
+    | FileSeparator | GroupSeparator | RecordSeparator | UnitSeparator
+
+    | Space | ExclamationMark | QuotationMark | NumberSign | DollarSign | PercentSign | Ampersand | Apostrophe | LeftParenthesis | RightParenthesis | Asterisk | PlusSign | Comma | HyphenMinus | FullStop | Slash
+
+    | Digit0 | Digit1 | Digit2 | Digit3 | Digit4 | Digit5 | Digit6 | Digit7 | Digit8 | Digit9
+
+    | Colon | Semicolon | LessThanSign | EqualsSign | GreaterThanSign | QuestionMark | AtSign
+
+    | CapitalLetterA | CapitalLetterB | CapitalLetterC | CapitalLetterD | CapitalLetterE | CapitalLetterF | CapitalLetterG | CapitalLetterH | CapitalLetterI | CapitalLetterJ | CapitalLetterK | CapitalLetterL | CapitalLetterM | CapitalLetterN | CapitalLetterO | CapitalLetterP | CapitalLetterQ | CapitalLetterR | CapitalLetterS | CapitalLetterT | CapitalLetterU | CapitalLetterV | CapitalLetterW | CapitalLetterX | CapitalLetterY | CapitalLetterZ
+
+    | LeftSquareBracket | Backslash | RightSquareBracket | Caret | Underscore | GraveAccent
+
+    | SmallLetterA | SmallLetterB | SmallLetterC | SmallLetterD | SmallLetterE | SmallLetterF | SmallLetterG | SmallLetterH | SmallLetterI | SmallLetterJ | SmallLetterK | SmallLetterL | SmallLetterM | SmallLetterN | SmallLetterO | SmallLetterP | SmallLetterQ | SmallLetterR | SmallLetterS | SmallLetterT | SmallLetterU | SmallLetterV | SmallLetterW | SmallLetterX | SmallLetterY | SmallLetterZ
+
+    | LeftCurlyBracket | VerticalLine | RightCurlyBracket | Tilde | Delete
+
+-- | ASCII characters can be compared for equality using '(==)'. Comparisons are case-sensitive; @'SmallLetterA' '/=' 'CapitalLetterA'@.
+deriving stock instance Eq Char
+
+-- | ASCII characters are ordered; for example, the letter /A/ is "less than" ('<') the letter /B/ because it appears earlier in the list. The ordering of ASCII characters is the same as the ordering of the corresponding Unicode 'C.Char's.
+deriving stock instance Ord Char
+
+-- | The 'Enum' instance allows us to use range syntax, for example @['SmallLetterA' .. 'SmallLetterZ']@ is a list all lower-case letters from /a/ to /z/. Instead of 'toEnum' and 'fromEnum', consider using 'toInt' and 'fromIntMaybe'.
+deriving stock instance Enum Char
+
+-- | The least character is 'Null', and the greatest character is 'Delete'. You can write @(['minBound' .. 'maxBound'] :: [ASCII.'Char'])@ to get a list of all the ASCII characters.
+deriving stock instance Bounded Char
+
+-- | 'show' produces the name of a constructor. For example, the character @e@ is shown as “@SmallLetterE@”. See "ASCII.Char" for the complete list of constructor names.
+deriving stock instance Show Char
+
+-- | The 'Data' instance allows ASCII characters to be used with generic programming in the “SYB” style. (See the <https://hackage.haskell.org/package/syb syb> package and the 2003 paper <https://www.microsoft.com/en-us/research/wp-content/uploads/2003/01/hmap.pdf Scrap Your Boilerplate> by Ralf Lämmel and Simon Peyton Jones.)
+deriving stock instance Data Char
+
+-- | The 'Generic' instance allows ASCII characters to be used with generic programming in the “generic deriving” style. (See the <https://hackage.haskell.org/package/generic-data generic-data> package and the 2010 paper <http://dreixel.net/research/pdf/gdmh.pdf A generic deriving mechanism for Haskell> by José Pedro Magalhães, Atze Dijkstra, Johan Jeuring, and Andres Löh.)
+deriving stock instance Generic Char
+
+-- | The 'Hashable' instance lets us collect ASCII characters in hash-based sets, and it lets us use ASCII characters as keys in hash-based maps. (See the @unordered-containers@ package.)
+deriving anyclass instance Hashable Char
+
+{- | Converts an ASCII character to its corresponding numeric value between 0 and 127.
+
+>>> map toInt [Null, CapitalLetterA, SmallLetterA, Delete]
+[0,65,97,127]
+
+-}
+
+toInt :: Char -> Int
+toInt = Prelude.fromEnum
+
+{- | Returns 'Just' the ASCII character corresponding to a numeric value between 0 and 127, or 'Nothing' for numbers outside this range.
+
+>>> map fromIntMaybe [-1, 0, 65, 127, 128]
+[Nothing,Just Null,Just CapitalLetterA,Just Delete,Nothing]
+
+-}
+
+fromIntMaybe :: Int -> Maybe Char
+fromIntMaybe x | x < 0     = Nothing
+               | x > 127   = Nothing
+               | otherwise = Just (fromIntUnsafe x)
+
+{- | The inverse of 'toInt'.
+
+This is marked as /unsafe/ because it is undefined for numbers below 0 or above 127. The safe variant of this function is 'fromIntMaybe'.
+
+>>> map fromIntUnsafe [65, 66, 67]
+[CapitalLetterA,CapitalLetterB,CapitalLetterC]
+
+-}
+
+fromIntUnsafe :: Int -> Char
+fromIntUnsafe = Prelude.toEnum
+
+allCharacters :: [Char]
+allCharacters = Prelude.enumFromTo Prelude.minBound Prelude.maxBound
+
+{- $notes
+
+There are 128 characters in total.
+
+>>> length allCharacters
+128
+
+Null is the first character.
+
+>>> minBound :: Char
+Null
+
+Delete is the last character.
+
+>>> maxBound :: Char
+Delete
+
+-}
diff --git a/test/test.hs b/test/test.hs
new file mode 100644
--- /dev/null
+++ b/test/test.hs
@@ -0,0 +1,53 @@
+module Main where
+
+import ASCII.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.List (intercalate, map, null, length)
+import Data.Maybe (Maybe (..))
+import Data.Semigroup ((<>))
+import Numeric.Natural (Natural)
+import Prelude (minBound, maxBound)
+import System.Exit (die)
+import System.IO (IO, putStrLn)
+import Text.Show (show)
+
+main :: IO ()
+main = dieIfFailures $ do
+    test 1 $ map toInt [Null, CapitalLetterA, SmallLetterA, Delete] == [0, 65, 97, 127]
+    test 2 $ map fromIntMaybe [-1, 0, 65, 127, 128] == [Nothing, Just Null, Just CapitalLetterA, Just Delete, Nothing]
+    test 3 $ map fromIntUnsafe [65, 66, 67] == [CapitalLetterA, CapitalLetterB, CapitalLetterC]
+    test 4 $ length allCharacters == 128
+    test 5 $ (minBound :: Char) == Null
+    test 6 $ (maxBound :: Char) == Delete
+
+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
