diff --git a/htools/Ganeti/HTools/QC.hs b/htools/Ganeti/HTools/QC.hs
index 393866cfd8c08df168b777601a36882071b593f4..8f16e9f52df134013995a06ad7dea55ee4a5a8f5 100644
--- a/htools/Ganeti/HTools/QC.hs
+++ b/htools/Ganeti/HTools/QC.hs
@@ -1,3 +1,5 @@
+{-# LANGUAGE TemplateHaskell #-}
+
 {-| Unittests for ganeti-htools.
 
 -}
@@ -71,8 +73,7 @@ import qualified Ganeti.HTools.Program.Hbal
 import qualified Ganeti.HTools.Program.Hscan
 import qualified Ganeti.HTools.Program.Hspace
 
-run :: Testable prop => prop -> Args -> IO Result
-run = flip quickCheckWithResult
+import Ganeti.HTools.QCHelper (testSuite)
 
 -- * Constants
 
@@ -364,16 +365,16 @@ prop_Utils_parseUnit (NonNegative n) =
     where _types = n::Int
 
 -- | Test list for the Utils module.
-testUtils =
-  [ run prop_Utils_commaJoinSplit
-  , run prop_Utils_commaSplitJoin
-  , run prop_Utils_fromObjWithDefault
-  , run prop_Utils_if'if
-  , run prop_Utils_select
-  , run prop_Utils_select_undefd
-  , run prop_Utils_select_undefv
-  , run prop_Utils_parseUnit
-  ]
+testSuite "Utils"
+              [ 'prop_Utils_commaJoinSplit
+              , 'prop_Utils_commaSplitJoin
+              , 'prop_Utils_fromObjWithDefault
+              , 'prop_Utils_if'if
+              , 'prop_Utils_select
+              , 'prop_Utils_select_undefd
+              , 'prop_Utils_select_undefv
+              , 'prop_Utils_parseUnit
+              ]
 
 -- ** PeerMap tests
 
@@ -413,13 +414,13 @@ prop_PeerMap_maxElem pmap =
           puniq = PeerMap.accumArray const pmap
 
 -- | List of tests for the PeerMap module.
-testPeerMap =
-    [ run prop_PeerMap_addIdempotent
-    , run prop_PeerMap_removeIdempotent
-    , run prop_PeerMap_maxElem
-    , run prop_PeerMap_addFind
-    , run prop_PeerMap_findMissing
-    ]
+testSuite "PeerMap"
+              [ 'prop_PeerMap_addIdempotent
+              , 'prop_PeerMap_removeIdempotent
+              , 'prop_PeerMap_maxElem
+              , 'prop_PeerMap_addFind
+              , 'prop_PeerMap_findMissing
+              ]
 
 -- ** Container tests
 
@@ -458,11 +459,11 @@ prop_Container_findByName node othername =
      Container.findByName nl' (Node.alias target) == Just target &&
      Container.findByName nl' othername == Nothing
 
-testContainer =
-    [ run prop_Container_addTwo
-    , run prop_Container_nameOf
-    , run prop_Container_findByName
-    ]
+testSuite "Container"
+              [ 'prop_Container_addTwo
+              , 'prop_Container_nameOf
+              , 'prop_Container_findByName
+              ]
 
 -- ** Instance tests
 
@@ -551,24 +552,24 @@ prop_Instance_setMovable inst m =
     Instance.movable inst' == m
     where inst' = Instance.setMovable inst m
 
-testInstance =
-    [ run prop_Instance_creat
-    , run prop_Instance_setIdx
-    , run prop_Instance_setName
-    , run prop_Instance_setAlias
-    , run prop_Instance_setPri
-    , run prop_Instance_setSec
-    , run prop_Instance_setBoth
-    , run prop_Instance_runStatus_True
-    , run prop_Instance_runStatus_False
-    , run prop_Instance_shrinkMG
-    , run prop_Instance_shrinkMF
-    , run prop_Instance_shrinkCG
-    , run prop_Instance_shrinkCF
-    , run prop_Instance_shrinkDG
-    , run prop_Instance_shrinkDF
-    , run prop_Instance_setMovable
-    ]
+testSuite "Instance"
+              [ 'prop_Instance_creat
+              , 'prop_Instance_setIdx
+              , 'prop_Instance_setName
+              , 'prop_Instance_setAlias
+              , 'prop_Instance_setPri
+              , 'prop_Instance_setSec
+              , 'prop_Instance_setBoth
+              , 'prop_Instance_runStatus_True
+              , 'prop_Instance_runStatus_False
+              , 'prop_Instance_shrinkMG
+              , 'prop_Instance_shrinkMF
+              , 'prop_Instance_shrinkCG
+              , 'prop_Instance_shrinkCF
+              , 'prop_Instance_shrinkDG
+              , 'prop_Instance_shrinkDF
+              , 'prop_Instance_setMovable
+              ]
 
 -- ** Text backend tests
 
@@ -663,13 +664,13 @@ prop_Text_NodeLSIdempotent node =
     -- override failN1 to what loadNode returns by default
     where n = node { Node.failN1 = True, Node.offline = False }
 
-testText =
-    [ run prop_Text_Load_Instance
-    , run prop_Text_Load_InstanceFail
-    , run prop_Text_Load_Node
-    , run prop_Text_Load_NodeFail
-    , run prop_Text_NodeLSIdempotent
-    ]
+testSuite "Text"
+              [ 'prop_Text_Load_Instance
+              , 'prop_Text_Load_InstanceFail
+              , 'prop_Text_Load_Node
+              , 'prop_Text_Load_NodeFail
+              , 'prop_Text_NodeLSIdempotent
+              ]
 
 -- ** Node tests
 
@@ -790,7 +791,6 @@ prop_Node_showField node =
   fst (Node.showHeader field) /= Types.unknownField &&
   Node.showField node field /= Types.unknownField
 
-
 prop_Node_computeGroups nodes =
   let ng = Node.computeGroups nodes
       onlyuuid = map fst ng
@@ -799,23 +799,22 @@ prop_Node_computeGroups nodes =
      length (nub onlyuuid) == length onlyuuid &&
      (null nodes || not (null ng))
 
-testNode =
-    [ run prop_Node_setAlias
-    , run prop_Node_setOffline
-    , run prop_Node_setMcpu
-    , run prop_Node_setXmem
-    , run prop_Node_addPriFM
-    , run prop_Node_addPriFD
-    , run prop_Node_addPriFC
-    , run prop_Node_addSec
-    , run prop_Node_rMem
-    , run prop_Node_setMdsk
-    , run prop_Node_tagMaps_idempotent
-    , run prop_Node_tagMaps_reject
-    , run prop_Node_showField
-    , run prop_Node_computeGroups
-    ]
-
+testSuite "Node"
+              [ 'prop_Node_setAlias
+              , 'prop_Node_setOffline
+              , 'prop_Node_setMcpu
+              , 'prop_Node_setXmem
+              , 'prop_Node_addPriFM
+              , 'prop_Node_addPriFD
+              , 'prop_Node_addPriFC
+              , 'prop_Node_addSec
+              , 'prop_Node_rMem
+              , 'prop_Node_setMdsk
+              , 'prop_Node_tagMaps_idempotent
+              , 'prop_Node_tagMaps_reject
+              , 'prop_Node_showField
+              , 'prop_Node_computeGroups
+              ]
 
 -- ** Cluster tests
 
@@ -957,16 +956,16 @@ prop_ClusterSplitCluster node inst =
      all (\(guuid, (nl'', _)) -> all ((== guuid) . Node.group)
                                  (Container.elems nl'')) gni
 
-testCluster =
-    [ run prop_Score_Zero
-    , run prop_CStats_sane
-    , run prop_ClusterAlloc_sane
-    , run prop_ClusterCanTieredAlloc
-    , run prop_ClusterAllocEvac
-    , run prop_ClusterAllocBalance
-    , run prop_ClusterCheckConsistency
-    , run prop_ClusterSplitCluster
-    ]
+testSuite "Cluster"
+              [ 'prop_Score_Zero
+              , 'prop_CStats_sane
+              , 'prop_ClusterAlloc_sane
+              , 'prop_ClusterCanTieredAlloc
+              , 'prop_ClusterAllocEvac
+              , 'prop_ClusterAllocBalance
+              , 'prop_ClusterCheckConsistency
+              , 'prop_ClusterSplitCluster
+              ]
 
 -- ** OpCodes tests
 
@@ -977,9 +976,8 @@ prop_OpCodes_serialization op =
     J.Ok op' -> op == op'
   where _types = op::OpCodes.OpCode
 
-testOpCodes =
-  [ run prop_OpCodes_serialization
-  ]
+testSuite "OpCodes"
+              [ 'prop_OpCodes_serialization ]
 
 -- ** Jobs tests
 
@@ -996,10 +994,10 @@ prop_JobStatus_serialization js =
     J.Ok js' -> js == js'
   where _types = js::Jobs.JobStatus
 
-testJobs =
-  [ run prop_OpStatus_serialization
-  , run prop_JobStatus_serialization
-  ]
+testSuite "Jobs"
+              [ 'prop_OpStatus_serialization
+              , 'prop_JobStatus_serialization
+              ]
 
 -- ** Loader tests
 
@@ -1044,14 +1042,14 @@ prop_Loader_compareNameComponent_prefix (NonEmpty s1) s2 =
   Loader.compareNameComponent (s1 ++ "." ++ s2) s1 ==
     Loader.LookupResult Loader.PartialMatch s1
 
-testLoader =
-  [ run prop_Loader_lookupNode
-  , run prop_Loader_lookupInstance
-  , run prop_Loader_assignIndices
-  , run prop_Loader_mergeData
-  , run prop_Loader_compareNameComponent_equal
-  , run prop_Loader_compareNameComponent_prefix
-  ]
+testSuite "Loader"
+              [ 'prop_Loader_lookupNode
+              , 'prop_Loader_lookupInstance
+              , 'prop_Loader_assignIndices
+              , 'prop_Loader_mergeData
+              , 'prop_Loader_compareNameComponent_equal
+              , 'prop_Loader_compareNameComponent_prefix
+              ]
 
 -- ** Types tests
 
@@ -1088,9 +1086,9 @@ prop_Types_eitherToResult ei =
     where r = Types.eitherToResult ei
           _types = ei::Either String Int
 
-testTypes =
-    [ run prop_Types_AllocPolicy_serialisation
-    , run prop_Types_DiskTemplate_serialisation
-    , run prop_Types_opToResult
-    , run prop_Types_eitherToResult
-    ]
+testSuite "Types"
+              [ 'prop_Types_AllocPolicy_serialisation
+              , 'prop_Types_DiskTemplate_serialisation
+              , 'prop_Types_opToResult
+              , 'prop_Types_eitherToResult
+              ]
diff --git a/htools/Ganeti/HTools/QCHelper.hs b/htools/Ganeti/HTools/QCHelper.hs
new file mode 100644
index 0000000000000000000000000000000000000000..cabf88c0def9284acaadadcffeff2d3310692cfb
--- /dev/null
+++ b/htools/Ganeti/HTools/QCHelper.hs
@@ -0,0 +1,47 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+{-| Unittest helpers for ganeti-htools
+
+-}
+
+{-
+
+Copyright (C) 2011 Google Inc.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+02110-1301, USA.
+
+-}
+
+module Ganeti.HTools.QCHelper
+    ( testSuite
+    ) where
+
+import Test.QuickCheck
+import Language.Haskell.TH
+
+run :: Testable prop => prop -> Args -> IO Result
+run = flip quickCheckWithResult
+
+testSuite :: String -> [Name] -> Q [Dec]
+testSuite tsname tdef = do
+  let fullname = mkName $ "test" ++ tsname
+  tests <- mapM (\n -> [| (run $(varE n), $(litE . StringL . nameBase $ n)) |])
+           tdef
+  sigtype <- [t| (String, [(Args -> IO Result, String)]) |]
+  return $ [ SigD fullname sigtype
+           , ValD (VarP fullname) (NormalB (TupE [LitE (StringL tsname),
+                                                  ListE tests])) []
+           ]
diff --git a/htools/test.hs b/htools/test.hs
index 34bd05a174e00a8d0078cf1725f1a79ac2c32d57..4c9ddf2d8d726afa05c7226548ad7f9b99cd9a6f 100644
--- a/htools/test.hs
+++ b/htools/test.hs
@@ -27,7 +27,7 @@ module Main(main) where
 
 import Data.IORef
 import Test.QuickCheck
-import System.Console.GetOpt
+import System.Console.GetOpt ()
 import System.IO
 import System.Exit
 import System (getArgs)
@@ -63,55 +63,61 @@ incIORef ir = atomicModifyIORef ir (\x -> (x + 1, ()))
 
 -- | Wrapper over a test runner with error counting.
 wrapTest :: IORef Int
-         -> (Args -> IO Result)
+         -> (Args -> IO Result, String)
          -> Args
-         -> IO (Result, Char)
-wrapTest ir test opts = do
+         -> IO (Result, Char, String)
+wrapTest ir (test, desc) opts = do
   r <- test opts
   c <- case r of
          Success {} -> return '.'
          GaveUp  {} -> return '?'
          Failure {} -> incIORef ir >> return '#'
          NoExpectedFailure {} -> incIORef ir >> return '*'
-  return (r, c)
+  return (r, c, desc)
+
+runTests :: String
+         -> Args
+         -> [Args -> IO (Result, Char, String)]
+         -> Int
+         -> IO [(Result, String)]
 
 runTests name opts tests max_count = do
   _ <- printf "%25s : " name
   hFlush stdout
   results <- mapM (\t -> do
-                     (r, c) <- t opts
+                     (r, c, desc) <- t opts
                      putChar c
                      hFlush stdout
-                     return r
+                     return (r, desc)
                   ) tests
-  let alldone = sum . map numTests $ results
+  let alldone = sum . map (numTests . fst) $ results
   _ <- printf "%*s(%d)\n" (max_count - length tests + 1) " " alldone
-  mapM_ (\(idx, r) ->
+  mapM_ (\(r, desc) ->
              case r of
                Failure { output = o, usedSeed = u, usedSize = size } ->
-                   printf "Test %d failed (seed was %s, test size %d): %s\n"
-                          idx (show u) size o
+                   printf "Test %s failed (seed was %s, test size %d): %s\n"
+                          desc (show u) size o
                GaveUp { numTests = passed } ->
-                   printf "Test %d incomplete: gave up with only %d\
+                   printf "Test %s incomplete: gave up with only %d\
                           \ passes after discarding %d tests\n"
-                          idx passed (maxDiscard opts)
+                          desc passed (maxDiscard opts)
                _ -> return ()
-        ) $ zip ([1..]::[Int]) results
+        ) results
   return results
 
-allTests :: [(String, Args, [Args -> IO Result])]
+allTests :: [(Args, (String, [(Args -> IO Result, String)]))]
 allTests =
-  [ ("Utils", fast, testUtils)
-  , ("PeerMap", fast, testPeerMap)
-  , ("Container", fast, testContainer)
-  , ("Instance", fast, testInstance)
-  , ("Node", fast, testNode)
-  , ("Text", fast, testText)
-  , ("OpCodes", fast, testOpCodes)
-  , ("Jobs", fast, testJobs)
-  , ("Loader", fast, testLoader)
-  , ("Types", fast, testTypes)
-  , ("Cluster", slow, testCluster)
+  [ (fast, testUtils)
+  , (fast, testPeerMap)
+  , (fast, testContainer)
+  , (fast, testInstance)
+  , (fast, testNode)
+  , (fast, testText)
+  , (fast, testOpCodes)
+  , (fast, testJobs)
+  , (fast, testLoader)
+  , (fast, testTypes)
+  , (slow, testCluster)
   ]
 
 transformTestOpts :: Args -> Options -> IO Args
@@ -135,9 +141,9 @@ main = do
   (opts, args) <- parseOpts cmd_args "test" options
   let tests = if null args
               then allTests
-              else filter (\(name, _, _) -> name `elem` args) allTests
-      max_count = maximum $ map (\(_, _, t) -> length t) tests
-  mapM_ (\(name, targs, tl) ->
+              else filter (\(_, (name, _)) -> name `elem` args) allTests
+      max_count = maximum $ map (\(_, (_, t)) -> length t) tests
+  mapM_ (\(targs, (name, tl)) ->
              transformTestOpts targs opts >>= \newargs ->
              runTests name newargs (wrap tl) max_count) tests
   terr <- readIORef errs