diff --git a/Example.hs b/Example.hs
--- a/Example.hs
+++ b/Example.hs
@@ -1,6 +1,6 @@
 {-# LANGUAGE OverloadedStrings #-}
 
-module Main where
+module Example where
 
 import           Control.Monad      (unless)
 import           Data.Aeson
diff --git a/changelog.md b/changelog.md
--- a/changelog.md
+++ b/changelog.md
@@ -1,4 +1,12 @@
-# 0.3
+# 0.3.0.2
+
++ Switch to hspec for tests.
+
+# 0.3.0.1
+
++ Enable GHC 8.
+
+# 0.3.0.0
 
 + Rewrite.
 
diff --git a/hjsonpointer.cabal b/hjsonpointer.cabal
--- a/hjsonpointer.cabal
+++ b/hjsonpointer.cabal
@@ -1,5 +1,5 @@
 name:                   hjsonpointer
-version:                0.3.0.1
+version:                0.3.0.2
 synopsis:               JSON Pointer library
 homepage:               https://github.com/seagreen/hjsonpointer
 license:                MIT
@@ -22,37 +22,30 @@
   ghc-options:          -Wall
   build-depends:        aeson                >= 0.7  && < 0.12
                       , base                 >= 4.6  && < 4.10
-                      , QuickCheck           >= 2.8  && < 2.9
+                      , QuickCheck           >= 2.8  && < 2.10
                       , unordered-containers >= 0.2  && < 0.3
                       , text                 >= 1.2  && < 1.3
                       , vector               >= 0.10 && < 0.12
 
 test-suite unit
   type:                 exitcode-stdio-1.0
-  hs-source-dirs:       tests
+  hs-source-dirs:       test
+                        ./
   main-is:              Unit.hs
+  other-modules:        Example
   default-language:     Haskell2010
   ghc-options:          -Wall
   default-extensions:   OverloadedStrings
   build-depends:        aeson
                       , base
                       , hjsonpointer
+                      , QuickCheck
                       , unordered-containers
                       , text
                       , vector
-                      , http-types                 >= 0.8 && < 0.10
-                      , HUnit                      >= 1.2 && < 1.4
-                      , test-framework             >= 0.8 && < 0.9
-                      , test-framework-hunit       >= 0.3 && < 0.4
-                      , test-framework-quickcheck2 >= 0.3 && < 0.4
-
-executable example
-  main-is:              Example.hs
-  default-language:     Haskell2010
-  ghc-options:          -Wall
-  build-depends:        aeson
-                      , base
-                      , hjsonpointer
+                      , hspec                >= 2.2 && < 2.3
+                      , http-types           >= 0.8 && < 0.10
+                      , HUnit                >= 1.2 && < 1.4
 
 source-repository head
   type:               git
diff --git a/test/Unit.hs b/test/Unit.hs
new file mode 100644
--- /dev/null
+++ b/test/Unit.hs
@@ -0,0 +1,93 @@
+
+module Main where
+
+import           Control.Arrow
+import           Data.Aeson
+import qualified Data.Aeson.Pointer                   as P
+import           Data.Text                            (Text)
+import qualified Data.Text                            as T
+import           Data.Text.Encoding
+import qualified Data.Vector                          as V
+import           Network.HTTP.Types.URI               (urlDecode)
+
+import           Test.Hspec
+import           Test.QuickCheck                      (property)
+
+import           Test.HUnit                           hiding (Test)
+
+-- For GHCs before 7.10:
+import           Control.Applicative
+import           Data.Foldable
+import           Data.Monoid
+
+import qualified Example
+
+main :: IO ()
+main = hspec $ do
+  describe "example" $ do
+    it "compiles and runs without errors" Example.main
+  describe "pointers" $ do
+    it "can be stored as JSON without changing its value" (property roundtrip)
+    it "can be represented in a JSON string value" jsonString
+    it "can be represented in a URI fragment identifier" uriFragment
+
+roundtrip :: P.Pointer -> Bool
+roundtrip a = Just a == decode (encode a)
+
+jsonString :: Assertion
+jsonString = traverse_ resolvesTo
+  [ (""      , specExample)
+  , ("/foo"  , Array $ V.fromList ["bar", "baz"])
+  , ("/foo/0", String "bar")
+  , ("/"     , Number 0)
+  , ("/a~1b" , Number 1)
+  , ("/c%d"  , Number 2)
+  , ("/e^f"  , Number 3)
+  , ("/g|h"  , Number 4)
+  , ("/i\\j" , Number 5)
+  , ("/k\"l" , Number 6)
+  , ("/ "    , Number 7)
+  , ("/m~0n" , Number 8)
+  ]
+
+uriFragment :: Assertion
+uriFragment = traverse_ resolvesTo . fmap (first decodeFragment) $
+  [ ("#"      , specExample)
+  , ("#/foo"  , Array $ V.fromList ["bar", "baz"])
+  , ("#/foo/0", String "bar")
+  , ("#/"     , Number 0)
+  , ("#/a~1b" , Number 1)
+  , ("#/c%25d", Number 2)
+  , ("#/e%5Ef", Number 3)
+  , ("#/g%7Ch", Number 4)
+  , ("#/i%5Cj", Number 5)
+  , ("#/k%22l", Number 6)
+  , ("#/%20"  , Number 7)
+  , ("#/m~0n" , Number 8)
+  ]
+  where
+    decodeFragment :: Text -> Text
+    decodeFragment = T.drop 1 . decodeUtf8 . urlDecode True . encodeUtf8
+
+resolvesTo :: (Text, Value) -> Assertion
+resolvesTo (t, expected) =
+  case P.unescape t of
+    Left e  -> assertFailure (show e <> " error for pointer: " <> show t)
+    Right p -> assertEqual
+                 ("Resolved value for pointer: " <> show t)
+                 (Right expected)
+                 (P.resolve p specExample)
+
+specExample :: Value
+specExample = object
+  [ "foo"  .= (["bar", "baz"] :: [Text])
+  , ""     .= (0 :: Int)
+  , "a/b"  .= (1 :: Int)
+  , "c%d"  .= (2 :: Int)
+  , "e^f"  .= (3 :: Int)
+  , "g|h"  .= (4 :: Int)
+  , "i\\j" .= (5 :: Int)
+  , "k\"l" .= (6 :: Int)
+  , " "    .= (7 :: Int)
+  , "m~n"  .= (8 :: Int)
+  ]
diff --git a/tests/Unit.hs b/tests/Unit.hs
deleted file mode 100644
--- a/tests/Unit.hs
+++ /dev/null
@@ -1,90 +0,0 @@
-
-module Main where
-
-import           Control.Arrow
-import           Data.Aeson
-import qualified Data.Aeson.Pointer                   as P
-import           Data.Text                            (Text)
-import qualified Data.Text                            as T
-import           Data.Text.Encoding
-import qualified Data.Vector                          as V
-import           Network.HTTP.Types.URI               (urlDecode)
-import           Test.Framework                       (defaultMain, testGroup)
-import           Test.Framework.Providers.HUnit       (testCase)
-import           Test.Framework.Providers.QuickCheck2 (testProperty)
-import           Test.HUnit                           hiding (Test)
-
--- For GHCs before 7.10:
-import           Control.Applicative
-import           Data.Foldable
-import           Data.Monoid
-
-main :: IO ()
-main = defaultMain
-  [ testGroup "unit"
-    [ testProperty "Storing a Pointer as JSON doesn't change its value" roundtrip
-    , testCase "Can be represented in a JSON string value" jsonString
-    , testCase "Can be represented in a URI fragment identifier" uriFragment
-    ]
-  ]
-
-roundtrip :: P.Pointer -> Bool
-roundtrip a = Just a == decode (encode a)
-
-jsonString :: Assertion
-jsonString = traverse_ resolvesTo
-  [ (""      , specExample)
-  , ("/foo"  , Array $ V.fromList ["bar", "baz"])
-  , ("/foo/0", String "bar")
-  , ("/"     , Number 0)
-  , ("/a~1b" , Number 1)
-  , ("/c%d"  , Number 2)
-  , ("/e^f"  , Number 3)
-  , ("/g|h"  , Number 4)
-  , ("/i\\j" , Number 5)
-  , ("/k\"l" , Number 6)
-  , ("/ "    , Number 7)
-  , ("/m~0n" , Number 8)
-  ]
-
-uriFragment :: Assertion
-uriFragment = traverse_ resolvesTo . fmap (first decodeFragment) $
-  [ ("#"      , specExample)
-  , ("#/foo"  , Array $ V.fromList ["bar", "baz"])
-  , ("#/foo/0", String "bar")
-  , ("#/"     , Number 0)
-  , ("#/a~1b" , Number 1)
-  , ("#/c%25d", Number 2)
-  , ("#/e%5Ef", Number 3)
-  , ("#/g%7Ch", Number 4)
-  , ("#/i%5Cj", Number 5)
-  , ("#/k%22l", Number 6)
-  , ("#/%20"  , Number 7)
-  , ("#/m~0n" , Number 8)
-  ]
-  where
-    decodeFragment :: Text -> Text
-    decodeFragment = T.drop 1 . decodeUtf8 . urlDecode True . encodeUtf8
-
-resolvesTo :: (Text, Value) -> Assertion
-resolvesTo (t, expected) =
-  case P.unescape t of
-    Left e  -> assertFailure (show e <> " error for pointer: " <> show t)
-    Right p -> assertEqual
-                 ("Resolved value for pointer: " <> show t)
-                 (Right expected)
-                 (P.resolve p specExample)
-
-specExample :: Value
-specExample = object
-  [ "foo"  .= (["bar", "baz"] :: [Text])
-  , ""     .= (0 :: Int)
-  , "a/b"  .= (1 :: Int)
-  , "c%d"  .= (2 :: Int)
-  , "e^f"  .= (3 :: Int)
-  , "g|h"  .= (4 :: Int)
-  , "i\\j" .= (5 :: Int)
-  , "k\"l" .= (6 :: Int)
-  , " "    .= (7 :: Int)
-  , "m~n"  .= (8 :: Int)
-  ]
