diff --git a/ekg-rrd.cabal b/ekg-rrd.cabal
--- a/ekg-rrd.cabal
+++ b/ekg-rrd.cabal
@@ -1,5 +1,5 @@
 name:                ekg-rrd
-version: 0.2.1.65
+version: 0.2.1.69
 synopsis:            Passes ekg statistics to rrdtool
 description:         Simple API for passing ekg monitoring statistics to a round-robin database (RRD) using rrdtool.
 homepage:            https://bitbucket.org/davecturner/ekg-rrd
@@ -30,6 +30,7 @@
 test-suite test
   type: exitcode-stdio-1.0
   main-is: Main.hs
+  other-modules: Tests.System.Metrics.RRDTool.Tests
   hs-source-dirs: test
   default-language:   Haskell2010
   build-depends:      base
@@ -52,5 +53,5 @@
 source-repository this
   type: hg
   location: https://bitbucket.org/davecturner/ekg-rrd
-  tag: f75f521cc764bc2079f5c67a112345bf7a8a2b0d
+  tag: 5ef08e3f15641c976ffc5386790e4bafb4b71cd3
 
diff --git a/test/Tests/System/Metrics/RRDTool/Tests.hs b/test/Tests/System/Metrics/RRDTool/Tests.hs
new file mode 100644
--- /dev/null
+++ b/test/Tests/System/Metrics/RRDTool/Tests.hs
@@ -0,0 +1,260 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Tests.System.Metrics.RRDTool.Tests (tests) where
+
+import Test.Framework as TF
+import Test.HUnit
+import Test.Framework.Providers.HUnit
+
+import System.Metrics
+import System.Metrics.RRDTool.Internals
+import Data.Time
+import qualified Data.Text as T
+import qualified Data.HashMap.Strict as HM
+
+tests :: TF.Test
+tests = testGroup "System.Metrics.RRDTool"
+  [ defineDataSourceTests
+  , showMaybeValueTests
+  , updateRRDArgsTests
+  , gcSourcesTests
+  , defineRoundRobinArchiveTests
+  , createRRDArgsTests
+  ]
+
+defineDataSourceTests :: TF.Test
+defineDataSourceTests = testGroup "defineDataSource"
+  [ testCase "derive" $ "DS:data_source_name:DERIVE:900:U:U"
+    @=? defineDataSource (testDataSource { dsType = DsDerive, dsHeartBeat = 900 })
+  , testCase "gauge" $ "DS:data_source_name:GAUGE:900:U:U"
+    @=? defineDataSource (testDataSource { dsType = DsGauge, dsHeartBeat = 900 })
+  , testCase "min 0" $ "DS:data_source_name:DERIVE:900:0:U"
+    @=? defineDataSource (testDataSource { dsType = DsDerive, dsHeartBeat = 900, dsMin = Just 0 })
+  , testCase "min 10" $ "DS:data_source_name:DERIVE:900:10:U"
+    @=? defineDataSource (testDataSource { dsType = DsDerive, dsHeartBeat = 900, dsMin = Just 10 })
+  , testCase "min (-1)" $ "DS:data_source_name:DERIVE:900:18446744073709551615:U"
+    @=? defineDataSource (testDataSource { dsType = DsDerive, dsHeartBeat = 900, dsMin = Just (-1) })
+  , testCase "max 10" $ "DS:data_source_name:DERIVE:900:U:10"
+    @=? defineDataSource (testDataSource { dsType = DsDerive, dsHeartBeat = 900, dsMax = Just 10 })
+  , testCase "max maxBound" $ "DS:data_source_name:DERIVE:900:U:18446744073709551615"
+    @=? defineDataSource (testDataSource { dsType = DsDerive, dsHeartBeat = 900, dsMax = Just (-1) })
+  , testCase "name" $ "DS:other_data_source_name:DERIVE:900:U:U"
+    @=? defineDataSource (testDataSource { dsType = DsDerive, dsHeartBeat = 900, dsName = "other_data_source_name" })
+  ]
+  where
+    testDataSource = undefinedDataSource
+      { dsName = "data_source_name"
+      , dsMin = Nothing
+      , dsMax = Nothing
+      }
+
+undefinedDataSource :: DataSource
+undefinedDataSource = DataSource
+  { dsName      = error "dsName undefined"
+  , dsMetric    = error "dsMetric undefined"
+  , dsType      = error "dsType undefined"
+  , dsHeartBeat = error "dsHeartBeat undefined"
+  , dsMin       = error "dsMin undefined"
+  , dsMax       = error "dsMax undefined"
+  }
+
+showMaybeValueTests :: TF.Test
+showMaybeValueTests = testGroup "showMaybeValue"
+  [ testCase "U" $ "U" @=? showMaybeValue Nothing
+  , testCase "0" $ "0" @=? showMaybeValue (Just 0)
+  , testCase "10" $ "10" @=? showMaybeValue (Just 10)
+  , testCase "maxBound"   $ "9223372036854775807" @=? showMaybeValue (Just maxBound)
+  , testCase "maxBound+1" $ "9223372036854775808" @=? showMaybeValue (Just $ maxBound + 1)
+  , testCase "-1" $ "18446744073709551615" @=? showMaybeValue (Just (-1))
+  ]
+
+updateRRDArgsTests :: TF.Test
+updateRRDArgsTests = testGroup "updateRRDArgs"
+  [ testCase "No update" $ updateRRDArgs rrd HM.empty (testTime 0)
+    @?= Nothing
+  , testCase "Unknown DS" $ updateRRDArgs rrd
+    (HM.singleton "data-source-4" $ Counter 0) (testTime 0)
+    @?= Nothing
+
+  , testCase "Counter update" $ updateRRDArgs rrd
+    (HM.singleton "data-source-1" $ Counter 0) (testTime 0)
+    @?= Just ["update","test.rrd","-t","data-source-1","1413331200:0"]
+  , testCase "Gauge update" $ updateRRDArgs rrd
+    (HM.singleton "data-source-1" $ Gauge 0) (testTime 0)
+    @?= Just ["update","test.rrd","-t","data-source-1","1413331200:0"]
+
+  , testCase "Multiple updates" $ updateRRDArgs rrd
+    (HM.fromList [("data-source-1",Counter 0),("data-source-2",Gauge 1)])
+    (testTime 0)
+    @?= Just ["update","test.rrd","-t","data-source-1:data-source-2"
+             ,"1413331200:0:1"]
+
+  , testCase "maxbound" $ updateRRDArgs rrd
+    (HM.singleton "data-source-1" $ Gauge maxBound) (testTime 0)
+    @?= Just ["update","test.rrd","-t","data-source-1","1413331200:9223372036854775807"]
+  , testCase "minbound" $ updateRRDArgs rrd
+    (HM.singleton "data-source-1" $ Gauge minBound) (testTime 0)
+    @?= Just ["update","test.rrd","-t","data-source-1","1413331200:9223372036854775808"]
+  , testCase "value of -1" $ updateRRDArgs rrd
+    (HM.singleton "data-source-1" $ Gauge (-1)) (testTime 0)
+    @?= Just ["update","test.rrd","-t","data-source-1","1413331200:18446744073709551615"]
+
+  , testCase "Label update is undefined" $ updateRRDArgs rrd
+    (HM.singleton "data-source-1" $ System.Metrics.Label "label") (testTime 0)
+    @?= Just ["update","test.rrd","-t","data-source-1","1413331200:U"]
+  ]
+  where
+  rrd = RoundRobinDatabase
+    { rrdToolPath = error "rrdToolPath undefined"
+    , rrdFilePath = "test.rrd"
+    , rrdArchives = error "rrdArchives undefined"
+    , rrdSources  = HM.fromList
+      [("data-source-1", undefinedDataSource {dsName="data-source-1"})
+      ,("data-source-2", undefinedDataSource {dsName="data-source-2"})
+      ,("data-source-3", undefinedDataSource {dsName="data-source-3"})
+      ]
+    , rrdStore = error "rrdStore undefined"
+    , rrdStep = error "rrdStep undefined"
+    }
+
+testTime :: NominalDiffTime -> UTCTime
+testTime = flip addUTCTime $ UTCTime (fromGregorian 2014 10 15) 0
+
+gcSourcesTests :: TF.Test
+gcSourcesTests = testGroup "gcSource"
+  [ testCase "source names" $ HM.fromList
+    (map (\ds -> (dsMetric ds, ())) (gcSources $ error "undefined heartbeat"))
+    @?= HM.fromList (map (\ds -> (T.pack ds, ())) $ words $
+      "rts.gc.num_gcs rts.gc.mutator_wall_ms rts.gc.gc_cpu_ms "
+      ++ "rts.gc.bytes_allocated rts.gc.num_bytes_usage_samples "
+      ++ "rts.gc.current_bytes_slop rts.gc.max_bytes_slop "
+      ++ "rts.gc.cumulative_bytes_used rts.gc.gc_wall_ms "
+      ++ "rts.gc.mutator_cpu_ms rts.gc.peak_megabytes_allocated "
+      ++ "rts.gc.max_bytes_used rts.gc.bytes_copied rts.gc.wall_ms "
+      ++ "rts.gc.current_bytes_used rts.gc.cpu_ms")
+
+  , testCounter "rts.gc.num_gcs" "num_gcs"
+  , testCounter "rts.gc.bytes_allocated" "bytes_allocated"
+  , testCounter "rts.gc.num_gcs" "num_gcs"
+  , testCounter "rts.gc.num_bytes_usage_samples" "num_bytes_usage_sam"
+  , testCounter "rts.gc.cumulative_bytes_used" "cumulative_bytes_us"
+  , testCounter "rts.gc.bytes_copied" "bytes_copied"
+  , testCounter "rts.gc.mutator_cpu_ms" "mutator_cpu_ms"
+  , testCounter "rts.gc.mutator_wall_ms" "mutator_wall_ms"
+  , testCounter "rts.gc.gc_cpu_ms" "gc_cpu_ms"
+  , testCounter "rts.gc.gc_wall_ms" "gc_wall_ms"
+  , testCounter "rts.gc.cpu_ms" "cpu_ms"
+  , testCounter "rts.gc.wall_ms" "wall_ms"
+
+  , testGauge "rts.gc.max_bytes_used" "max_bytes_used"
+  , testGauge "rts.gc.current_bytes_used" "current_bytes_used"
+  , testGauge "rts.gc.current_bytes_slop" "current_bytes_slop"
+  , testGauge "rts.gc.max_bytes_slop" "max_bytes_slop"
+  , testGauge "rts.gc.peak_megabytes_allocated" "peak_megabytes_allo"
+  ]
+
+  where
+
+  gcSourcesMap = HM.fromList [(dsMetric ds, ds) | ds <- gcSources 123]
+
+  testCounter = testGcSource DsDerive
+  testGauge   = testGcSource DsGauge
+
+  testGcSource sourceType metric source = testCase (T.unpack metric) $
+    HM.lookup metric gcSourcesMap @?= Just (DataSource
+      { dsName      = source
+      , dsMetric    = metric
+      , dsType      = sourceType
+      , dsHeartBeat = 123
+      , dsMin       = Just 0
+      , dsMax       = Nothing
+      })
+
+defineRoundRobinArchiveTests :: TF.Test
+defineRoundRobinArchiveTests = testGroup "defineRoundRobinArchive"
+  [ testCase "last" $ "RRA:LAST:0.5:1:8064"
+    @=? defineRoundRobinArchive (RoundRobinArchive CFLast 0.5 1 8064)
+  , testCase "average" $ "RRA:AVERAGE:0.75:2:8065"
+    @=? defineRoundRobinArchive (RoundRobinArchive CFAverage 0.75 2 8065)
+  , testCase "min" $ "RRA:MIN:0.25:3:8066"
+    @=? defineRoundRobinArchive (RoundRobinArchive CFMin 0.25 3 8066)
+  , testCase "max" $ "RRA:MAX:0.0:4:8067"
+    @=? defineRoundRobinArchive (RoundRobinArchive CFMax 0.0 4 8067)
+  ]
+
+createRRDArgsTests :: TF.Test
+createRRDArgsTests = testGroup "createRRDArgs"
+  [ testCase "create"
+  $ ["create","test.rrd","--no-overwrite","-s","234"
+    ,"DS:data-source-1:GAUGE:567:0:U","DS:data-source-2:DERIVE:568:2:5"
+    ,"RRA:LAST:0.5:1:8064","RRA:AVERAGE:0.5:6:105120"]
+  @=? createRRDArgs (RoundRobinDatabase
+    { rrdFilePath = "test.rrd"
+    , rrdToolPath = error "rrdToolPath undefined"
+    , rrdStore = error "rrdStore undefined"
+    , rrdStep = 234
+    , rrdSources = HM.fromList
+        [ ("1", undefinedDataSource
+          { dsName = "data-source-1"
+          , dsType = DsGauge
+          , dsHeartBeat = 567
+          , dsMin = Just 0
+          , dsMax = Nothing
+          })
+        , ("2", undefinedDataSource
+          { dsName = "data-source-2"
+          , dsType = DsDerive
+          , dsHeartBeat = 568
+          , dsMin = Just 2
+          , dsMax = Just 5
+          })
+        ]
+    , rrdArchives =
+        [ RoundRobinArchive
+            { rraCf = CFLast
+            , rraXff = 0.5
+            , rraPdpCount = 1
+            , rraRecordCount = 8064
+            }
+        , RoundRobinArchive
+            { rraCf = CFAverage
+            , rraXff = 0.5
+            , rraPdpCount = 6
+            , rraRecordCount = 105120
+            }
+        ]
+    })
+  , testCase "create with other RRAs"
+  $ ["create","test.rrd","--no-overwrite","-s","234"
+    ,"DS:data-source-1:GAUGE:567:0:U"
+    ,"RRA:MIN:0.5:4:8064","RRA:MAX:0.75:12:13000"]
+  @=? createRRDArgs (RoundRobinDatabase
+    { rrdFilePath = "test.rrd"
+    , rrdToolPath = error "rrdToolPath undefined"
+    , rrdStore = error "rrdStore undefined"
+    , rrdStep = 234
+    , rrdSources = HM.fromList
+        [ ("1", undefinedDataSource
+          { dsName = "data-source-1"
+          , dsType = DsGauge
+          , dsHeartBeat = 567
+          , dsMin = Just 0
+          , dsMax = Nothing
+          })
+        ]
+    , rrdArchives =
+        [ RoundRobinArchive
+            { rraCf = CFMin
+            , rraXff = 0.5
+            , rraPdpCount = 4
+            , rraRecordCount = 8064
+            }
+        , RoundRobinArchive
+            { rraCf = CFMax
+            , rraXff = 0.75
+            , rraPdpCount = 12
+            , rraRecordCount = 13000
+            }
+        ]
+    })
+  ]
