diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -13,10 +13,10 @@
 
 The author is a long-time user of `daemontools` due to its reliability
 and simplicity; however, `daemontools` is quirky and follows many
-unusual conventions.  
+unusual conventions.
 
-`angel` is an attempt to recreate `daemontools`'s capabilities (though 
-not the various bundled utility programs which are still quite useful) 
+`angel` is an attempt to recreate `daemontools`'s capabilities (though
+not the various bundled utility programs which are still quite useful)
 in a more intuitive and modern unix style.
 
 
@@ -24,7 +24,7 @@
 -------------
 
 `angel` is driven by a configuration file that contains a list of
-program specifications to run.  `angel` assumes every program listed in 
+program specifications to run.  `angel` assumes every program listed in
 the specification file should be running at all times.
 
 `angel` starts each program, and optionally sets the program's stdout
@@ -35,7 +35,7 @@
 If the program dies for any reason, `angel` waits a specified number
 of seconds (default, 5), then restarts the program.
 
-The `angel` process itself will respond to a HUP signal by 
+The `angel` process itself will respond to a HUP signal by
 re-processing its configuration file, and synchronizing the run
 states with the new configuration.  Specifically:
 
@@ -47,7 +47,7 @@
    normal supervision, will be restarted with the updated spec
  * If a program has been removed from the configuration file,
    the corresponding child process will be sent a TERM signal;
-   when it dies, supervision of the process will end, and 
+   when it dies, supervision of the process will end, and
    therefore, it will not be restarted
 
 Safety and Reliability
@@ -77,7 +77,7 @@
 Building
 --------
 
- 1. Install the haskell-platform (or somehow, ghc 7.0 + 
+ 1. Install the haskell-platform (or somehow, ghc 7.6 +
     cabal-install)
  2. Run `cabal install` in the project root (this directory)
  3. Either add the ~/.cabal/bin file to your $PATH or copy
@@ -85,8 +85,7 @@
 
 Notes:
 
- * I have not tried building `angel` against ghc 6.10 or earlier;
-   6.12, 7.0, 7.2, 7.4, and 7.6 are known to work
+ * Angel is recommended to be built on GHC 7.6 and newer.
 
 Configuration and Usage Example
 -------------------------------
@@ -144,7 +143,7 @@
  * `exec` is the exact command line to run (required)
  * `user` is the user the program will run as (optional, defaults to the
    user angel is launched as)
- * `stdout` is a path to a file where the program's standard output 
+ * `stdout` is a path to a file where the program's standard output
     should be appended (optional, defaults to /dev/null)
  * `stderr` is a path to a file where the program's standard error
     should be appended (optional, defaults to /dev/null)
@@ -184,7 +183,7 @@
 Assuming the above configuration was in a file called "example.conf",
 here's what a shell session might look like:
 
-    jamie@choo:~/random/angel$ angel example.conf 
+    jamie@choo:~/random/angel$ angel example.conf
     [2010/08/24 15:21:22] {main} Angel started
     [2010/08/24 15:21:22] {main} Using config file: example.conf
     [2010/08/24 15:21:22] {process-monitor} Must kill=0, must start=2
@@ -264,7 +263,7 @@
 You can run the test suite with
 
 ```
-make test
+cabal test
 ```
 
 FAQ
@@ -274,7 +273,7 @@
 
 Yes, angel `dup()`s file descriptors and makes effort to safely
 allow concurrent writes by child programs; you should DEFINITELY
-make sure your child program is doing stdout/stderr writes in 
+make sure your child program is doing stdout/stderr writes in
 line-buffered mode so this doesn't result in a complete interleaved
 mess in the log file.
 
diff --git a/Setup.hs b/Setup.hs
--- a/Setup.hs
+++ b/Setup.hs
@@ -1,3 +1,40 @@
 #!/usr/bin/env runhaskell
+import Control.Monad
 import Distribution.Simple
-main = defaultMain
+import Distribution.Simple.Setup
+import Distribution.Simple.Program.Run
+import Distribution.Simple.UserHooks
+import Distribution.PackageDescription
+import Distribution.Verbosity
+-- import System.Process
+
+
+main :: IO ()
+main = defaultMainWithHooks simpleUserHooks
+    {
+      preTest = myTestHook
+    }
+
+
+-------------------------------------------------------------------------------
+myTestHook
+    :: Args
+    -> TestFlags
+    -> IO HookedBuildInfo
+myTestHook _ _ = do
+    buildTestFixtures
+    return emptyHookedBuildInfo
+
+
+-------------------------------------------------------------------------------
+buildTestFixtures :: IO ()
+buildTestFixtures = do
+  runScript "ghc" ["--make", "test/test_jobs/StubbornJob.hs", "-o", "test/test_jobs/StubbornJob"]
+  runScript "ghc" ["--make", "test/test_jobs/CompliantJob.hs", "-o", "test/test_jobs/CompliantJob"]
+
+
+-------------------------------------------------------------------------------
+runScript :: String -> [String] -> IO ()
+runScript cmd args = do
+  putStrLn $ unwords [cmd, unwords args]
+  runProgramInvocation normal $ simpleProgramInvocation cmd args
diff --git a/angel.cabal b/angel.cabal
--- a/angel.cabal
+++ b/angel.cabal
@@ -1,5 +1,5 @@
 Name:                angel
-Version:             0.6.1
+Version:             0.6.2
 License:             BSD3
 License-file:        LICENSE
 Author:              Jamie Turner
@@ -18,7 +18,7 @@
 
 Stability:           Stable
 Category:            System
-Build-type:          Simple
+Build-type:          Custom
 
 Extra-source-files:  README.md
                      changelog.md
@@ -36,7 +36,7 @@
   Hs-Source-Dirs: src
   Main-is: Angel/Main.hs
 
-  Build-depends: base >= 4.0 && < 5
+  Build-depends: base >= 4.5 && < 5
   Build-depends: process >= 1.2.0.0 && < 2.0
   Build-depends: mtl
   Build-depends: configurator >= 0.1
@@ -74,8 +74,10 @@
                  Angel.PidFileSpec
                  Angel.UtilSpec
                  SpecHelper
-  Build-Depends:  base
-  Build-Depends:  hspec
+  Build-Depends: base
+  Build-Depends: tasty
+  Build-Depends: tasty-hunit
+  Build-Depends: tasty-quickcheck
   Build-depends: base >= 4.0 && < 5
   Build-depends: process >= 1.0 && < 2.0
   Build-depends: mtl
diff --git a/changelog.md b/changelog.md
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,6 @@
+0.6.2
+* Switch to cabal-only build strategy
+
 0.6.1
 * Fix build for GHC 7.10 by adding FlexibleContexts
 
diff --git a/test/Angel/ConfigSpec.hs b/test/Angel/ConfigSpec.hs
--- a/test/Angel/ConfigSpec.hs
+++ b/test/Angel/ConfigSpec.hs
@@ -10,131 +10,140 @@
 
 import SpecHelper
 
-spec :: Spec
-spec = do
-  describe "modifyProg" $ do
-    it "modifies exec" $
-      modifyProg prog "exec" (String "foo") `shouldBe`
-      prog { exec = Just "foo"}
-
-    it "errors for non-string execs" $
-      evaluate (modifyProg prog "exec" (Bool True)) `shouldThrow`
-      anyErrorCall
-
-    it "modifies delay for positive numbers" $
-      modifyProg prog "delay" (Number 1) `shouldBe`
-      prog { delay = Just 1}
-    it "modifies delay for 0" $
-      modifyProg prog "delay" (Number 0) `shouldBe`
-      prog { delay = Just 0}
-    it "errors on negative delays" $
-      evaluate (modifyProg prog "delay" (Number (-1))) `shouldThrow`
-      anyErrorCall
+spec :: TestTree
+spec = testGroup "Angel.Config"
+  [
+    testGroup "modifyProg"
+    [
+      testCase "modifies exec" $
+        modifyProg prog "exec" (String "foo") @?=
+        prog { exec = Just "foo"}
 
-    it "modifies stdout" $
-      modifyProg prog "stdout" (String "foo") `shouldBe`
-      prog { stdout = Just "foo"}
-    it "errors for non-string stdout" $
-      evaluate (modifyProg prog "stdout" (Bool True)) `shouldThrow`
-      anyErrorCall
+    , testCase "errors for non-string execs" $
+        evaluate (modifyProg prog "exec" (Bool True)) `shouldThrow`
+        anyErrorCall
 
-    it "modifies stderr" $
-      modifyProg prog "stderr" (String "foo") `shouldBe`
-      prog { stderr = Just "foo"}
-    it "errors for non-string stderr" $
-      evaluate (modifyProg prog "stderr" (Bool True)) `shouldThrow`
-      anyErrorCall
+    , testCase "modifies delay for positive numbers" $
+        modifyProg prog "delay" (Number 1) @?=
+        prog { delay = Just 1}
+    , testCase "modifies delay for 0" $
+        modifyProg prog "delay" (Number 0) @?=
+        prog { delay = Just 0}
+    , testCase "errors on negative delays" $
+        evaluate (modifyProg prog "delay" (Number (-1))) `shouldThrow`
+        anyErrorCall
 
-    it "modifies directory" $
-      modifyProg prog "directory" (String "foo") `shouldBe`
-      prog { workingDir = Just "foo"}
-    it "errors for non-string directory" $
-      evaluate (modifyProg prog "directory" (Bool True)) `shouldThrow`
-      anyErrorCall
+    , testCase "modifies stdout" $
+        modifyProg prog "stdout" (String "foo") @?=
+        prog { stdout = Just "foo"}
+    , testCase "errors for non-string stdout" $
+        evaluate (modifyProg prog "stdout" (Bool True)) `shouldThrow`
+        anyErrorCall
 
-    it "modifies pidfile" $
-      modifyProg prog "pidfile" (String "foo.pid") `shouldBe`
-      prog { pidFile = Just "foo.pid"}
-    it "errors for non-string path" $
-      evaluate (modifyProg prog "pidfile" (Bool True)) `shouldThrow`
-      anyErrorCall
+    , testCase "modifies stderr" $
+        modifyProg prog "stderr" (String "foo") @?=
+        prog { stderr = Just "foo"}
+    , testCase "errors for non-string stderr" $
+        evaluate (modifyProg prog "stderr" (Bool True)) `shouldThrow`
+        anyErrorCall
 
-    it "appends env to the empty list" $
-      modifyProg prog "env.foo" (String "bar") `shouldBe`
-      prog { env = [("foo", "bar")]}
-    it "errors for non-string value" $
-      evaluate (modifyProg prog "env.foo" (Bool True)) `shouldThrow`
-      anyErrorCall
-    it "prepends env to an existing list" $
-      modifyProg prog { env = [("previous", "value")]} "env.foo" (String "bar") `shouldBe`
-      prog { env = [("foo", "bar"), ("previous", "value")]}
+    , testCase "modifies directory" $
+        modifyProg prog "directory" (String "foo") @?=
+        prog { workingDir = Just "foo"}
+    , testCase "errors for non-string directory" $
+        evaluate (modifyProg prog "directory" (Bool True)) `shouldThrow`
+        anyErrorCall
 
-    it "interprets boolean False as Nothing" $
-      modifyProg prog "termgrace" (Bool False) `shouldBe`
-      prog { termGrace = Nothing }
-    it "interprets 0 as Nothing" $
-      modifyProg prog "termgrace" (Number 0) `shouldBe`
-      prog { termGrace = Nothing }
-    it "interprets > 0 as a set termGrace" $
-      modifyProg prog "termgrace" (Number 2) `shouldBe`
-      prog { termGrace = Just 2 }
-    it "interprets boolean True as an error" $
-      evaluate (modifyProg prog "termgrace" (Bool True)) `shouldThrow`
-      anyErrorCall
-    it "interprets negative numbers as an error" $
-      evaluate (modifyProg prog "termgrace" (Number (-1))) `shouldThrow`
-      anyErrorCall
-    it "interprets anything else as an error" $
-      evaluate (modifyProg prog "termgrace" (String "yeah")) `shouldThrow`
-      anyErrorCall
+    , testCase "modifies pidfile" $
+        modifyProg prog "pidfile" (String "foo.pid") @?=
+        prog { pidFile = Just "foo.pid"}
+    , testCase "errors for non-string path" $
+        evaluate (modifyProg prog "pidfile" (Bool True)) `shouldThrow`
+        anyErrorCall
 
-    it "does nothing for all other cases" $
-      modifyProg prog "bogus" (String "foo") `shouldBe`
-      prog
+    , testCase "appends env to the empty list" $
+        modifyProg prog "env.foo" (String "bar") @?=
+        prog { env = [("foo", "bar")]}
+    , testCase "errors for non-string value" $
+        evaluate (modifyProg prog "env.foo" (Bool True)) `shouldThrow`
+        anyErrorCall
+    , testCase "prepends env to an existing list" $
+        modifyProg prog { env = [("previous", "value")]} "env.foo" (String "bar") @?=
+        prog { env = [("foo", "bar"), ("previous", "value")]}
 
-  describe "expandByCount" $ do
-    it "doesn't affect empty hashes" $
-      expandByCount HM.empty `shouldBe`
-        HM.empty
-    it "doesn't affect hashes without counts" $
-      expandByCount (HM.fromList [baseProgPair]) `shouldBe`
-        HM.fromList [baseProgPair]
-    it "errors on mistyped count field" $
-      evaluate (expandByCount (HM.fromList [baseProgPair
-                                           , ("prog.count", String "wat")])) `shouldThrow`
+    , testCase "interprets boolean False as Nothing" $
+        modifyProg prog "termgrace" (Bool False) @?=
+        prog { termGrace = Nothing }
+    , testCase "interprets 0 as Nothing" $
+        modifyProg prog "termgrace" (Number 0) @?=
+        prog { termGrace = Nothing }
+    , testCase "interprets > 0 as a set termGrace" $
+        modifyProg prog "termgrace" (Number 2) @?=
+        prog { termGrace = Just 2 }
+    , testCase "interprets boolean True as an error" $
+        evaluate (modifyProg prog "termgrace" (Bool True)) `shouldThrow`
         anyErrorCall
-    it "errors on negative count field" $
-      evaluate (expandByCount (HM.fromList [ baseProgPair
-                                           , ("prog.count", Number (-1))])) `shouldThrow`
+    , testCase "interprets negative numbers as an error" $
+        evaluate (modifyProg prog "termgrace" (Number (-1))) `shouldThrow`
         anyErrorCall
-    it "generates no configs with a count of 0" $
-      expandByCount (HM.fromList [ baseProgPair
-                                 , ("prog.count", Number 0)]) `shouldBe`
-        HM.empty
-    it "expands with a count of 1" $
-      expandByCount (HM.fromList [baseProgPair, ("prog.count", Number 1)]) `shouldBe`
-        HM.fromList [ ("prog-1.exec", String "foo")
-                    , ("prog-1.env.ANGEL_PROCESS_NUMBER", String "1")]
-    it "expands with a count of > 1" $
-      expandByCount (HM.fromList [baseProgPair, ("prog.count", Number 2)]) `shouldBe`
-        HM.fromList [ ("prog-1.exec", String "foo")
-                    , ("prog-1.env.ANGEL_PROCESS_NUMBER", String "1")
-                    , ("prog-2.exec", String "foo")
-                    , ("prog-2.env.ANGEL_PROCESS_NUMBER", String "2")]
-    it "preserves explicit env variables" $
-      expandByCount (HM.fromList [baseProgPair, ("prog.env.FOO", String "bar")]) `shouldBe`
-        HM.fromList [ ("prog.exec",    String "foo")
-                    , ("prog.env.FOO", String "bar")]
-    it "expands pidfiles with a count of 1" $
-      expandByCount (HM.fromList [ baseProgPair
-                                 , ("prog.count", Number 1)
-                                 , ("prog.pidfile", String "foo.pid")]) `shouldBe`
-        HM.fromList [ ("prog-1.exec", String "foo")
-                    , ("prog-1.env.ANGEL_PROCESS_NUMBER", String "1")
-                    , ("prog-1.pidfile", String "foo-1.pid")] --TODO: try without expanding if count == 1
-  describe "processConfig internal API" $
-    it "can parse the example config" $
-      shouldReturnRight $ processConfig "example.conf"
+    , testCase "interprets anything else as an error" $
+        evaluate (modifyProg prog "termgrace" (String "yeah")) `shouldThrow`
+        anyErrorCall
+
+    , testCase "does nothing for all other cases" $
+        modifyProg prog "bogus" (String "foo") @?=
+        prog
+    ]
+
+  , testGroup "expandByCount"
+    [
+      testCase "doesn't affect empty hashes" $
+        expandByCount HM.empty @?=
+          HM.empty
+    , testCase "doesn't affect hashes without counts" $
+        expandByCount (HM.fromList [baseProgPair]) @?=
+          HM.fromList [baseProgPair]
+    , testCase "errors on mistyped count field" $
+        evaluate (expandByCount (HM.fromList [baseProgPair
+                                             , ("prog.count", String "wat")])) `shouldThrow`
+          anyErrorCall
+    , testCase "errors on negative count field" $
+        evaluate (expandByCount (HM.fromList [ baseProgPair
+                                             , ("prog.count", Number (-1))])) `shouldThrow`
+          anyErrorCall
+    , testCase "generates no configs with a count of 0" $
+        expandByCount (HM.fromList [ baseProgPair
+                                   , ("prog.count", Number 0)]) @?=
+          HM.empty
+    , testCase "expands with a count of 1" $
+        expandByCount (HM.fromList [baseProgPair, ("prog.count", Number 1)]) @?=
+          HM.fromList [ ("prog-1.exec", String "foo")
+                      , ("prog-1.env.ANGEL_PROCESS_NUMBER", String "1")]
+    , testCase "expands with a count of > 1" $
+        expandByCount (HM.fromList [baseProgPair, ("prog.count", Number 2)]) @?=
+          HM.fromList [ ("prog-1.exec", String "foo")
+                      , ("prog-1.env.ANGEL_PROCESS_NUMBER", String "1")
+                      , ("prog-2.exec", String "foo")
+                      , ("prog-2.env.ANGEL_PROCESS_NUMBER", String "2")]
+    , testCase "preserves explicit env variables" $
+        expandByCount (HM.fromList [baseProgPair, ("prog.env.FOO", String "bar")]) @?=
+          HM.fromList [ ("prog.exec",    String "foo")
+                      , ("prog.env.FOO", String "bar")]
+    , testCase "expands pidfiles with a count of 1" $
+        expandByCount (HM.fromList [ baseProgPair
+                                   , ("prog.count", Number 1)
+                                   , ("prog.pidfile", String "foo.pid")]) @?=
+          HM.fromList [ ("prog-1.exec", String "foo")
+                      , ("prog-1.env.ANGEL_PROCESS_NUMBER", String "1")
+                      , ("prog-1.pidfile", String "foo-1.pid")] --TODO: try without expanding if count == 1
+    ]
+
+  , testGroup "processConfig internal API"
+    [
+      testCase "can parse the example config" $
+        shouldReturnRight $ processConfig "example.conf"
+    ]
+  ]
   where prog = defaultProgram
         baseProgPair = ("prog.exec", String "foo")
         shouldReturnRight a = flip shouldSatisfy isRight =<< a
diff --git a/test/Angel/JobSpec.hs b/test/Angel/JobSpec.hs
--- a/test/Angel/JobSpec.hs
+++ b/test/Angel/JobSpec.hs
@@ -18,47 +18,57 @@
 
 import SpecHelper
 
-spec :: Spec
-spec =
-  describe "killProcess" $ do
-    describe "using SoftKill" $ do
-      it "cleanly kills well-behaved processes" $ runAngelM dummyOptions $ do
-        ph <- liftIO launchCompliantJob
-        killProcess $ SoftKill "thing" ph Nothing
-        liftIO $
-          patientlyGetProcessExitCode ph `shouldReturn` (Just $ Exited ExitSuccess)
-
-      it "does not forcefully kill stubborn processes" $ runAngelM dummyOptions $ do
-        ph <- liftIO launchStubbornJob
-        killProcess $ SoftKill "thing" ph Nothing
-        -- stubborn job gets marked as [defunct] here. no idea why. it should be able to survive a SIGTERM
-        liftIO $ do
-          patientlyGetProcessExitCode ph `shouldReturn` Nothing
-          hardKillProcessHandle ph -- cleanup
+spec :: TestTree
+spec = testGroup "Angel.Jon"
+  [
+    testGroup "killProcess"
+    [
+      testGroup "using SoftKill"
+      [
+        testCase "cleanly kills well-behaved processes" $ runAngelM dummyOptions $ do
+          ph <- liftIO launchCompliantJob
+          killProcess $ SoftKill "thing" ph Nothing
+          liftIO $
+            patientlyGetProcessExitCode ph `shouldReturn` (Just $ Exited ExitSuccess)
 
-    describe "using HardKill" $ do
-      it "cleanly kills well-behaved processes" $ runAngelM dummyOptions $ do
-        ph <- liftIO launchCompliantJob
-        killProcess $ HardKill "thing" ph Nothing 1
-        -- Can't geth the exiit status because the life check in Job "uses up" the waitpid
-        liftIO $
-          patientlyGetProcessExitCode ph `shouldReturn` Nothing
-      it "forcefully kills stubborn processes" $ runAngelM dummyOptions $ do
-        ph <- liftIO launchStubbornJob
-        killProcess $ HardKill "thing" ph Nothing 1
-        liftIO $
+      , testCase "does not forcefully kill stubborn processes" $ runAngelM dummyOptions $ do
+          ph <- liftIO launchStubbornJob
+          killProcess $ SoftKill "thing" ph Nothing
+          -- stubborn job gets marked as [defunct] here. no idea why. it should be able to survive a SIGTERM
+          liftIO $ do
+            patientlyGetProcessExitCode ph `shouldReturn` Nothing
+            hardKillProcessHandle ph -- cleanup
+      ],
+      testGroup "using HardKill"
+      [
+        testCase "cleanly kills well-behaved processes" $ runAngelM dummyOptions $ do
+          ph <- liftIO launchCompliantJob
+          killProcess $ HardKill "thing" ph Nothing 1
+          -- Can't geth the exiit status because the life check in Job "uses up" the waitpid
+          liftIO $
+            patientlyGetProcessExitCode ph `shouldReturn` Nothing
+      , testCase "forcefully kills stubborn processes" $ runAngelM dummyOptions $ do
+          ph <- liftIO launchStubbornJob
+          killProcess $ HardKill "thing" ph Nothing 1
+          liftIO $
 #if MIN_VERSION_unix(2,7,0)
-          patientlyGetProcessExitCode ph `shouldReturn` (Just $ Terminated sigKILL False)
+            patientlyGetProcessExitCode ph `shouldReturn` (Just $ Terminated sigKILL False)
 #else
-          patientlyGetProcessExitCode ph `shouldReturn` (Just $ Terminated sigKILL)
+            patientlyGetProcessExitCode ph `shouldReturn` (Just $ Terminated sigKILL)
 #endif
-    describe "with a logger" $
-      it "cleanly kills well-behaved loggers" $ runAngelM dummyOptions $ do
-        ph <- liftIO launchCompliantJob
-        lph <- liftIO launchCompliantJob
-        killProcess $ SoftKill "thing" ph (Just lph)
-        liftIO $
-          patientlyGetProcessExitCode lph `shouldReturn` (Just $ Exited ExitSuccess)
+      ],
+      testGroup "with a logger"
+      [
+        testCase "cleanly kills well-behaved loggers" $ runAngelM dummyOptions $ do
+          ph <- liftIO launchCompliantJob
+          lph <- liftIO launchCompliantJob
+          killProcess $ SoftKill "thing" ph (Just lph)
+          liftIO $
+            patientlyGetProcessExitCode lph `shouldReturn` (Just $ Exited ExitSuccess)
+      ]
+    ]
+  ]
+
 
 launchCompliantJob :: IO ProcessHandle
 launchCompliantJob = launchJob "CompliantJob"
diff --git a/test/Angel/LogSpec.hs b/test/Angel/LogSpec.hs
--- a/test/Angel/LogSpec.hs
+++ b/test/Angel/LogSpec.hs
@@ -12,10 +12,15 @@
 import SpecHelper
 
 
-spec :: Spec
-spec = do
-  describe "cleanCalendar" $ do
-    it "formats the time correctly" $ cleanCalendar dateTime `shouldBe` "2012/09/12 03:14:59"
+spec :: TestTree
+spec = testGroup "Angel.Log"
+  [
+    testGroup "cleanCalendar"
+    [
+      testCase "formats the time correctly" $
+        cleanCalendar dateTime @?= "2012/09/12 03:14:59"
+    ]
+  ]
   --where time = CalendarTime 2012 September 12 3 14 59 0 Tuesday 263 "Pacific" -25200 True
   where dateTime  = ZonedTime localTime zone
         localTime = LocalTime day tod
diff --git a/test/Angel/PidFileSpec.hs b/test/Angel/PidFileSpec.hs
--- a/test/Angel/PidFileSpec.hs
+++ b/test/Angel/PidFileSpec.hs
@@ -14,22 +14,26 @@
 
 import SpecHelper
 
-spec :: Spec
-spec =
-  describe "startWithPidFile" $ do
-    it "creates the pidfile and cleans up" $ do
-      startWithPidFile procSpec fileName jogOn $ \_pHandle -> do
-        fileShouldExist fileName
-        pid <- readFile fileName
-        null pid `shouldBe` False
-        all isNumber pid `shouldBe` True
-      fileShouldNotExist fileName
-    it "calls the error callback when pidfile can't be created and re-raises" $ do
-      called <- newIORef False
-      let onPidError = const $ writeIORef called True
-      (res :: Either SomeException ()) <- try $ startWithPidFile procSpec badPidFile jogOn onPidError
-      readIORef called `shouldReturn` True
-      isLeft res `shouldBe` True
+spec :: TestTree
+spec = testGroup "Angel.PidFile"
+  [
+    testGroup "startWithPidFile"
+    [
+      testCase "creates the pidfile and cleans up" $ do
+        startWithPidFile procSpec fileName jogOn $ \_pHandle -> do
+          fileShouldExist fileName
+          pid <- readFile fileName
+          null pid @?= False
+          all isNumber pid @?= True
+        fileShouldNotExist fileName
+    , testCase "calls the error callback when pidfile can't be created and re-raises" $ do
+        called <- newIORef False
+        let onPidError = const $ writeIORef called True
+        (res :: Either SomeException ()) <- try $ startWithPidFile procSpec badPidFile jogOn onPidError
+        readIORef called `shouldReturn` True
+        isLeft res @?= True
+    ]
+  ]
   where
     fileName = "temp.pid"
     badPidFile = "/bogus/path/to/pidfile"
diff --git a/test/Angel/UtilSpec.hs b/test/Angel/UtilSpec.hs
--- a/test/Angel/UtilSpec.hs
+++ b/test/Angel/UtilSpec.hs
@@ -8,27 +8,33 @@
 
 import SpecHelper
 
-spec :: Spec
-spec = do
-  describe "expandPath" $ do
-    it "generates the correct path for just a tilde" $ do
-      UserEntry { homeDirectory = home } <- getUserEntry
-      path <- expandPath "~/foo"
-      path `shouldBe` home ++ "/foo"
-    it "generates the correct path for tilde with a specific user" $ do
-      UserEntry { homeDirectory = home,
-                  userName      = user } <- getUserEntry
-      path <- expandPath $ "~" ++ user ++ "/foo"
-      path `shouldBe` home ++ "/foo"
-    it "leaves paths without tildes alone" $ do
-      path <- expandPath "/foo"
-      path `shouldBe` "/foo"
-  describe "split" $ do
-    prop "produces no null values" $ \(a :: Char) (xs :: [Char]) ->
-      none null $ split a xs
-    prop "produces no instances of the split element" $ \(a :: Char) (xs :: [Char]) ->
-      none (elem a) $ split a xs
-    it "splits" $
-      split ' ' "  foo  bar     baz  " `shouldBe` ["foo", "bar", "baz"]
+spec :: TestTree
+spec = testGroup "Angel.Util"
+  [
+    testGroup "expandPath"
+    [
+      testCase "generates the correct path for just a tilde" $ do
+        UserEntry { homeDirectory = home } <- getUserEntry
+        path <- expandPath "~/foo"
+        path @?= home ++ "/foo"
+    , testCase "generates the correct path for tilde with a specific user" $ do
+        UserEntry { homeDirectory = home,
+                    userName      = user } <- getUserEntry
+        path <- expandPath $ "~" ++ user ++ "/foo"
+        path @?= home ++ "/foo"
+    , testCase "leaves paths without tildes alone" $ do
+        path <- expandPath "/foo"
+        path @?= "/foo"
+    ]
+  , testGroup "split"
+    [
+      testProperty "produces no null values" $ \(a :: Char) (xs :: [Char]) ->
+        none null $ split a xs
+    , testProperty "produces no instances of the split element" $ \(a :: Char) (xs :: [Char]) ->
+        none (elem a) $ split a xs
+    , testCase "splits" $
+        split ' ' "  foo  bar     baz  " @?= ["foo", "bar", "baz"]
+    ]
+  ]
   where getUserEntry = getUserEntryForID =<< getEffectiveUserID
         none p = not . any p
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -1,1 +1,23 @@
-{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+module Main (main) where
+
+-------------------------------------------------------------------------------
+import qualified Angel.ConfigSpec
+import qualified Angel.JobSpec
+import qualified Angel.LogSpec
+import qualified Angel.PidFileSpec
+import qualified Angel.UtilSpec
+-------------------------------------------------------------------------------
+import SpecHelper
+-------------------------------------------------------------------------------
+
+
+
+main :: IO ()
+main = defaultMain $ testGroup "angel"
+  [
+    Angel.ConfigSpec.spec
+  , Angel.JobSpec.spec
+  , Angel.LogSpec.spec
+  , Angel.PidFileSpec.spec
+  , Angel.UtilSpec.spec
+  ]
diff --git a/test/SpecHelper.hs b/test/SpecHelper.hs
--- a/test/SpecHelper.hs
+++ b/test/SpecHelper.hs
@@ -1,4 +1,34 @@
-module SpecHelper (module X) where
+module SpecHelper
+    ( module X
+    , module SpecHelper
+    ) where
 
-import Test.Hspec as X
-import Test.Hspec.QuickCheck as X
+import Control.Exception
+import Test.Tasty as X
+import Test.Tasty.HUnit as X
+import Test.Tasty.QuickCheck as X
+
+
+
+-------------------------------------------------------------------------------
+shouldReturn :: (Show a, Eq a) => IO a -> a -> Assertion
+shouldReturn f v = do
+  v' <- f
+  v' @?= v
+
+
+-------------------------------------------------------------------------------
+shouldThrow :: Exception e => IO a -> (e -> Bool) -> Assertion
+shouldThrow f p = do
+  res <- try f
+  either (`shouldSatisfy` p) (const $ assertFailure "Did not throw an exception") res
+
+
+-------------------------------------------------------------------------------
+anyErrorCall :: ErrorCall -> Bool
+anyErrorCall = const True
+
+
+-------------------------------------------------------------------------------
+shouldSatisfy :: (Show a) => a -> (a -> Bool) -> Assertion
+shouldSatisfy a p = assertBool ("predicate failed on " ++ show a ) (p a)
