diff --git a/hsini.cabal b/hsini.cabal
--- a/hsini.cabal
+++ b/hsini.cabal
@@ -1,5 +1,5 @@
 name          : hsini
-version       : 0.3
+version       : 0.3.1
 license       : BSD3
 license-file  : LICENSE
 author        : Magnus Therning
@@ -27,6 +27,7 @@
     type: exitcode-stdio-1.0
     hs-source-dirs: tst, src
     main-is: Main.hs
+    other-modules: Ini, ReaderI
     default-language : Haskell2010
     build-depends:
         base,
diff --git a/tst/Ini.hs b/tst/Ini.hs
new file mode 100644
--- /dev/null
+++ b/tst/Ini.hs
@@ -0,0 +1,71 @@
+{-# OPTIONS_GHC -XTemplateHaskell #-}
+-- Copyright : 2011 Magnus Therning
+-- License   : BSD3
+module Ini where
+
+-- {{{1 imports
+import Data.List
+import Data.Maybe
+import Test.Framework
+import Test.Framework.TH
+import Test.Framework.Providers.QuickCheck2
+import Test.QuickCheck
+
+import Data.Ini
+import Data.Ini.Types
+
+-- {{{1 section properties
+-- adding and then deleting a section is a no-op (if the section doesn't exist
+-- already)
+prop_secAddDel sn cfglst = delSection sn (setOption sn "foo" "bar" cfg2) == cfg2
+    where
+        cfg = cfgFromList cfglst
+        cfg2 = delSection sn cfg -- must make sure the section doesn't exist before adding
+
+-- after adding a section the config has such a section
+prop_secAddHas sn cfglst = hasSection sn (setOption sn "foo" "bar" cfg)
+    where cfg = cfgFromList cfglst
+
+-- after adding a section it's possible to get it
+prop_secAddGet sn cfglst = isJust $ getSection sn (setOption sn "foo" "bar" cfg)
+    where cfg = cfgFromList cfglst
+
+-- after deleting a section it's gone
+prop_secDelGet sn cfglst = isNothing $ getSection sn $ delSection sn cfg2
+    where
+        cfg = cfgFromList cfglst
+        cfg2 = setOption sn "foo" "bar" cfg
+
+-- {{{1 option properties
+-- setting and then deleting an option is a no-op (if the option doesn't exist
+-- already)
+prop_optSetDel sn on ov cfglst = delOption sn on (setOption sn on ov cfg) == cfg2
+    where
+        cfg = cfgFromList cfglst
+        cfg2 = delOption sn on cfg
+
+-- after setting an option it's there
+prop_optSetHas sn on ov cfglst = hasOption sn on (setOption sn on ov cfg)
+    where cfg = cfgFromList cfglst
+
+-- after setting an option it's possible to get it
+prop_optSetGet sn on ov cfglst = isJust $ getOption sn on $ setOption sn on ov cfg
+    where cfg = cfgFromList cfglst
+
+-- after deleting a section it's gone
+prop_optDelGet sn on ov cfglst = isNothing $ getOption sn on $ delOption sn on cfg2
+    where
+        cfg = cfgFromList cfglst
+        cfg2 = setOption sn on ov cfg
+
+-- getting all items
+prop_optAllItems cfglst = (length _cfglst > 0) ==> lstItems == (allItems sn cfg)
+    where
+        cfg = cfgFromList cfglst
+        _cfglst = cfgToList cfg
+        -- sn = head . sort $ map fst _cfglst
+        sn = head $ map fst _cfglst
+        lstItems = fromJust $ lookup sn _cfglst
+
+-- {{{1 allTests
+allTests = $(testGroupGenerator)
diff --git a/tst/ReaderI.hs b/tst/ReaderI.hs
new file mode 100644
--- /dev/null
+++ b/tst/ReaderI.hs
@@ -0,0 +1,147 @@
+{-# OPTIONS_GHC -XTemplateHaskell #-}
+-- Copyright : 2011 Magnus Therning
+-- License   : BSD3
+module ReaderI where
+
+import Test.Framework
+import Test.Framework.Providers.HUnit
+import Test.Framework.TH
+import Test.HUnit.Base
+import Text.ParserCombinators.Parsec as P
+
+import Data.Ini.Reader.Internals
+
+-- Convenience function that translates a parser result to something that's
+-- easier to check.
+p2E p s t = let
+        res = P.parse p s t
+    in case res of
+        Left _ -> Left "bad"
+        Right e -> Right e
+
+-- {{{1 secParser
+case_secParserAllowedChars1 = let
+        expected = Right $ SectionL "foo"
+        actual = p2E secParser "sec" "[foo]\n"
+    in expected @=? actual
+
+case_secParserAllowedChars2 = let
+        expected = Right $ SectionL "FooBar"
+        actual = p2E secParser "sec" "[FooBar]\n"
+    in expected @=? actual
+
+case_secParserAllowedChars3 = let
+        expected = Right $ SectionL "@Foo/Bar-"
+        actual = p2E secParser "sec" "[@Foo/Bar-]\n"
+    in expected @=? actual
+
+case_secParserAllowedChars4 = let
+        expected = Right $ SectionL "foo123"
+        actual = p2E secParser "sec" "[foo123]\n"
+    in expected @=? actual
+
+case_secParserAllowedChars5 = let
+        expected = Right $ SectionL "_foo"
+        actual = p2E secParser "sec" "[_foo]\n"
+    in expected @=? actual
+
+case_secParserDisallowedChars1 = let
+        expected = Left "bad"
+        actual = p2E secParser "sec" "[foo.bar]\n"
+    in expected @=? actual
+
+case_secParserDropSpace = let
+        expected = Right $ SectionL "foo"
+        actual = p2E secParser "sec" "[ \tfoo\t ]\n"
+    in expected @=? actual
+
+case_secParserDropTrailing = let
+        expected = Right $ SectionL "foo"
+        actual = p2E secParser "sec" "[foo]  \t foobar\n"
+    in expected @=? actual
+
+-- {{{1 optLineParser
+case_optLineParserAllowedChars1 = let
+        expected = Right $ OptionL "foo" "bar"
+        actual = p2E optLineParser "optLine" "foo=bar\n"
+    in expected @=? actual
+
+case_optLineParserAllowedChars2 = let
+        expected = Right $ OptionL "Foo" "bAr"
+        actual = p2E optLineParser "optLine" "Foo=bAr\n"
+    in expected @=? actual
+
+case_optLineParserAllowedChars3 = let
+        expected = Right $ OptionL "foo@/foo-" "bar"
+        actual = p2E optLineParser "optLine" "foo@/foo-=bar\n"
+    in expected @=? actual
+
+case_optLineParserAllowedChars4 = let
+        expected = Right $ OptionL "foo123" "bar"
+        actual = p2E optLineParser "optLine" "foo123=bar\n"
+    in expected @=? actual
+
+case_optLineParserAllowedChars5 = let
+        expected = Right $ OptionL "_foo" "bar"
+        actual = p2E optLineParser "optLine" "_foo=bar\n"
+    in expected @=? actual
+
+case_optLineParserDisallowedChars1 = let
+        expected = Left "bad"
+        actual = p2E optLineParser "optLine" "foo.bar=baz\n"
+    in expected @=? actual
+
+case_optLineParserDropSpace = let
+        expected = Right $ OptionL "foo" "bar"
+        actual = p2E optLineParser "optLine" "foo\t \t=\t \t bar\n"
+    in expected @=? actual
+
+case_optLineParserKeepSpace = let
+        expected = Right $ OptionL "foo" "bar \t \t"
+        actual = p2E optLineParser "optLine" "foo\t \t=\t \t bar \t \t\n"
+    in expected @=? actual
+
+-- {{{1 optContParser
+case_optContParserSpace = let
+        expected = Right $ OptionContL "foo"
+        actual = p2E optContParser "optCont" " foo\n"
+    in expected @=? actual
+
+case_optContParserTab = let
+        expected = Right $OptionContL "foo"
+        actual = p2E optContParser "optCont" "\tfoo\n"
+    in expected @=? actual
+
+case_optContParserKeepTrailing = let
+        expected = Right $ OptionContL "foo  \t\t"
+        actual = p2E optContParser "optCont" "\tfoo  \t\t\n"
+    in expected @=? actual
+
+-- {{{1 noiseParser
+case_noiseParserEmptyLine = let
+        expected = Right CommentL
+        actual = p2E noiseParser "noise" "\n"
+    in expected @=? actual
+
+case_noiseParserComment1 = let
+        expected = Right CommentL
+        actual = p2E noiseParser "noise" "# a comment\n"
+    in expected @=? actual
+
+case_noiseParserComment2 = let
+        expected = Right CommentL
+        actual = p2E noiseParser "noise" "; another comment\n"
+    in expected @=? actual
+
+case_noiseParserNonEmpty = let
+        expected = Left "bad"
+        actual = p2E noiseParser "noise" " \n"
+    in expected @=? actual
+
+-- {{{1 iniParser
+-- TBD
+
+-- {{{1 buildConfig
+-- TBD
+
+allTests = $(testGroupGenerator)
