diff --git a/ASCII/Group.hs b/ASCII/Group.hs
deleted file mode 100644
--- a/ASCII/Group.hs
+++ /dev/null
@@ -1,113 +0,0 @@
-{- |
-
-ASCII characters are broadly categorized into two groups: /control codes/ and /printable characters/.
-
--}
-
-module ASCII.Group
-  (
-    {- * The @Group@ type -} Group (..),
-    {- * Functions -} charGroup, inGroup
-    {- * Notes -} {- $notes -}
-  )
-  where
-
-import ASCII.Char    ( Char )
-import Data.Bool     ( Bool )
-import Data.Data     ( Data )
-import Data.Eq       ( Eq, (==) )
-import Data.Hashable ( Hashable )
-import Data.Ord      ( Ord, (<) )
-import GHC.Generics  ( Generic )
-import Prelude       ( Enum, Bounded )
-import Text.Show     ( Show )
-
-import qualified ASCII.Char as Char
-
-{- $setup
-
->>> import Prelude
->>> import ASCII.Char
-
--}
-
-data Group =
-    Control -- ^ 33 of the ASCII characters are /control codes/. A few of these are still in use, but most are obsolete relics of the early days of computing.
-  | Printable -- ^ 95 of the ASCII characters are /printable characters/ such as letters and numbers, mostly corresponding to the keys on an American English keyboard.
-
-deriving stock instance Eq Group
-
-deriving stock instance Ord Group
-
-deriving stock instance Enum Group
-
-deriving stock instance Bounded Group
-
-deriving stock instance Show Group
-
-deriving stock instance Data Group
-
-deriving stock instance Generic Group
-
-deriving anyclass instance Hashable Group
-
-{- | Determine which group a particular character belongs to.
-
->>> map charGroup [CapitalLetterA,EndOfTransmission]
-[Printable,Control]
-
--}
-
-charGroup :: Char -> Group
-charGroup x =
-    case x of
-        _ | (x < Char.Space) -> Control
-        Char.Delete          -> Control
-        _                    -> Printable
-
-{- | Test whether a character belongs to a particular group.
-
->>> inGroup Printable EndOfTransmission
-False
-
->>> inGroup Control EndOfTransmission
-True
-
--}
-
-inGroup :: Group -> Char -> Bool
-inGroup g x = charGroup x == g
-
-{- $notes
-
-Space is a printable character (perhaps surprisingly, given that it is invisible).
-
->>> charGroup Space
-Printable
-
-Tab is a control code (perhaps surprisingly, given that space is a printable character).
-
->>> charGroup HorizontalTab
-Control
-
-A few examples of printable characters:
-
->>> all (inGroup Printable) [CapitalLetterA,SmallLetterZ,Digit4,Tilde]
-True
-
-A few examples of control characters:
-
->>> all (inGroup Control) [Null,Substitute,UnitSeparator,Delete]
-True
-
-There are 33 control codes.
-
->>> length (filter (inGroup Control) allCharacters)
-33
-
-There are 95 printable characters.
-
->>> length (filter (inGroup Printable) allCharacters)
-95
-
--}
diff --git a/ascii-group.cabal b/ascii-group.cabal
--- a/ascii-group.cabal
+++ b/ascii-group.cabal
@@ -1,7 +1,7 @@
 cabal-version: 2.0
 
 name: ascii-group
-version: 1.0.0.4
+version: 1.0.0.6
 synopsis: ASCII character groups
 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,6 +33,7 @@
     default-extensions: DeriveDataTypeable
     default-extensions: DeriveGeneric
     ghc-options: -Wall
+    hs-source-dirs: library
 
     build-depends: ascii-char ^>= 1.0
 
@@ -42,3 +41,14 @@
     build-depends: hashable   >= 1.2  && < 1.4
 
     exposed-modules: ASCII.Group
+
+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, ascii-group, base
diff --git a/changelog.txt b/changelog.txt
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,3 +1,4 @@
 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
diff --git a/library/ASCII/Group.hs b/library/ASCII/Group.hs
new file mode 100644
--- /dev/null
+++ b/library/ASCII/Group.hs
@@ -0,0 +1,106 @@
+{- |
+
+ASCII characters are broadly categorized into two groups: /control codes/ and /printable characters/.
+
+-}
+
+module ASCII.Group
+  (
+    {- * The @Group@ type -} Group (..),
+    {- * Functions -} charGroup, inGroup
+    {- * Notes -} {- $notes -}
+  )
+  where
+
+import ASCII.Char    ( Char )
+import Data.Bool     ( Bool )
+import Data.Data     ( Data )
+import Data.Eq       ( Eq, (==) )
+import Data.Hashable ( Hashable )
+import Data.Ord      ( Ord, (<) )
+import GHC.Generics  ( Generic )
+import Prelude       ( Enum, Bounded )
+import Text.Show     ( Show )
+
+import qualified ASCII.Char as Char
+
+data Group =
+    Control -- ^ 33 of the ASCII characters are /control codes/. A few of these are still in use, but most are obsolete relics of the early days of computing.
+  | Printable -- ^ 95 of the ASCII characters are /printable characters/ such as letters and numbers, mostly corresponding to the keys on an American English keyboard.
+
+deriving stock instance Eq Group
+
+deriving stock instance Ord Group
+
+deriving stock instance Enum Group
+
+deriving stock instance Bounded Group
+
+deriving stock instance Show Group
+
+deriving stock instance Data Group
+
+deriving stock instance Generic Group
+
+deriving anyclass instance Hashable Group
+
+{- | Determine which group a particular character belongs to.
+
+>>> map charGroup [CapitalLetterA,EndOfTransmission]
+[Printable,Control]
+
+-}
+
+charGroup :: Char -> Group
+charGroup x =
+    case x of
+        _ | (x < Char.Space) -> Control
+        Char.Delete          -> Control
+        _                    -> Printable
+
+{- | Test whether a character belongs to a particular group.
+
+>>> inGroup Printable EndOfTransmission
+False
+
+>>> inGroup Control EndOfTransmission
+True
+
+-}
+
+inGroup :: Group -> Char -> Bool
+inGroup g x = charGroup x == g
+
+{- $notes
+
+Space is a printable character (perhaps surprisingly, given that it is invisible).
+
+>>> charGroup Space
+Printable
+
+Tab is a control code (perhaps surprisingly, given that space is a printable character).
+
+>>> charGroup HorizontalTab
+Control
+
+A few examples of printable characters:
+
+>>> all (inGroup Printable) [CapitalLetterA,SmallLetterZ,Digit4,Tilde]
+True
+
+A few examples of control characters:
+
+>>> all (inGroup Control) [Null,Substitute,UnitSeparator,Delete]
+True
+
+There are 33 control codes.
+
+>>> length (filter (inGroup Control) allCharacters)
+33
+
+There are 95 printable characters.
+
+>>> length (filter (inGroup Printable) allCharacters)
+95
+
+-}
diff --git a/test/test.hs b/test/test.hs
new file mode 100644
--- /dev/null
+++ b/test/test.hs
@@ -0,0 +1,57 @@
+module Main where
+
+import ASCII.Group
+
+import ASCII.Char (Char (..), allCharacters)
+
+import Control.Applicative (Applicative (..))
+import Control.Monad (Monad (..))
+import Data.Bool (Bool (..), not)
+import Data.Eq (Eq ((==)))
+import Data.Foldable (all)
+import Data.Function ((.), ($))
+import Data.Functor (Functor (..))
+import Data.List (intercalate, map, null, length, filter)
+import Data.Semigroup ((<>))
+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 $ not $ inGroup Printable EndOfTransmission
+    test 3 $ inGroup Control EndOfTransmission
+    test 4 $ charGroup Space == Printable
+    test 5 $ charGroup HorizontalTab == Control
+    test 6 $ all (inGroup Printable) [CapitalLetterA, SmallLetterZ, Digit4, Tilde]
+    test 7 $ all (inGroup Control) [Null, Substitute, UnitSeparator, Delete]
+    test 8 $ length (filter (inGroup Control) allCharacters) == 33
+    test 9 $ length (filter (inGroup Printable) allCharacters) == 95
+
+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
