diff --git a/Makefile b/Makefile
new file mode 100644
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,16 @@
+run-test:	update-test
+	runhaskell Setup configure --user -fbuildExamples --enable-tests
+	runhaskell Setup build
+	runhaskell Setup haddock
+	runhaskell Setup test spreadsheet-test --show-details=streaming
+
+update-test:
+	doctest-extract-0.1 -i src/ -o test/ --executable-main=Main.hs \
+	  Data.Spreadsheet
+
+
+ghci:
+	ghci -Wall -XGeneralizedNewtypeDeriving -i:src src/Data/Spreadsheet.hs
+
+%.html:	%.md
+	pandoc --output $@ $<
diff --git a/example/Option.hs b/example/Option.hs
--- a/example/Option.hs
+++ b/example/Option.hs
@@ -28,8 +28,9 @@
 character name =
    OP.eitherReader $ \str ->
    case str of
-      [c] -> Right c
+      "TAB" -> Right '\t'
       "\\t" -> Right '\t'
+      [c] -> Right c
       _ -> Left $
          name ++ " must be one character, which " ++ show str ++ " is not"
 
diff --git a/spreadsheet.cabal b/spreadsheet.cabal
--- a/spreadsheet.cabal
+++ b/spreadsheet.cabal
@@ -1,6 +1,6 @@
 Cabal-Version:    2.2
 Name:             spreadsheet
-Version:          0.1.3.9
+Version:          0.1.3.10
 License:          BSD-3-Clause
 License-File:     LICENSE
 Author:           Henning Thielemann <haskell@henning-thielemann.de>
@@ -35,16 +35,18 @@
   * @cassava@: high-level CSV parser that treats rows as records,
     parses ByteStrings and is biased towards UTF-8 encoding.
 Tested-With:       GHC==7.4.2, GHC==7.8.4
+Tested-With:       GHC==8.6.5, GHC==9.6.3
 Build-Type:        Simple
 Extra-Source-Files:
   README.md
+  Makefile
 
 Source-Repository head
   Type:     darcs
   Location: http://code.haskell.org/~thielema/spreadsheet/
 
 Source-Repository this
-  Tag:      0.1.3.9
+  Tag:      0.1.3.10
   Type:     darcs
   Location: http://code.haskell.org/~thielema/spreadsheet/
 
@@ -72,8 +74,8 @@
   If flag(buildExamples)
     Build-Depends:
       spreadsheet,
-      optparse-applicative >=0.12 && <0.17,
-      shell-utility >=0.0 && <0.1,
+      optparse-applicative >=0.12 && <0.19,
+      shell-utility >=0.0 && <0.2,
       utility-ht,
       explicit-exception,
       base
@@ -91,8 +93,8 @@
   If flag(buildExamples)
     Build-Depends:
       spreadsheet,
-      optparse-applicative >=0.12 && <0.17,
-      containers >=0.4.2 && <0.7,
+      optparse-applicative >=0.12 && <0.19,
+      containers >=0.4.2 && <0.8,
       utility-ht,
       base
   Else
@@ -104,3 +106,20 @@
   Other-Modules:
     CSVExtract.Option
     Option
+
+Test-Suite spreadsheet-test
+  Type: exitcode-stdio-1.0
+  Build-Depends:
+    spreadsheet,
+    doctest-exitcode-stdio >=0.0 && <0.1,
+    doctest-lib >=0.1 && <0.2,
+    QuickCheck >=2.1 && <3,
+    explicit-exception,
+    base
+
+  GHC-Options:      -Wall
+  Hs-Source-Dirs:   test
+  Default-Language: Haskell98
+  Main-Is: Main.hs
+  Other-Modules:
+    Test.Data.Spreadsheet
diff --git a/src/Data/Spreadsheet.hs b/src/Data/Spreadsheet.hs
--- a/src/Data/Spreadsheet.hs
+++ b/src/Data/Spreadsheet.hs
@@ -21,7 +21,13 @@
 import qualified Control.Monad.Exception.Asynchronous as Async
 import qualified Data.Spreadsheet.CharSource as CharSource
 
+{- $setup
+>>> import qualified Data.Spreadsheet as Spreadsheet
+>>> import qualified Control.Monad.Exception.Asynchronous.Lazy as MEA
+>>> import qualified Test.QuickCheck as QC
+-}
 
+
 {- |
 A spreadsheet is a list of lines,
 each line consists of cells,
@@ -90,6 +96,15 @@
 {- |
 @fromString qm sep text@ parses @text@ into a spreadsheet,
 using the quotation character @qm@ and the separator character @sep@.
+
+>>> Spreadsheet.fromString '"' '\t' "\"hello\"\t\"world\"\n\"end\"\n"
+Exceptional {exception = Nothing, result = [["hello","world"],["end"]]}
+>>> Spreadsheet.fromString '"' ',' "\"hello,world\",\"really\"\n\"end\"\n"
+Exceptional {exception = Nothing, result = [["hello,world","really"],["end"]]}
+>>> Spreadsheet.fromString '"' ';' "\"hello \"\"world\"\"\"\n\"really\"\n"
+Exceptional {exception = Nothing, result = [["hello \"world\""],["really"]]}
+>>> Spreadsheet.fromString '"' ',' "\"hello\nworld\"\n"
+Exceptional {exception = Nothing, result = [["hello\nworld"]]}
 -}
 fromString :: Char -> Char -> String -> Async.Exceptional Parser.UserMessage T
 fromString qm sep str =
@@ -110,6 +125,28 @@
    in  Async.Exceptional e (table, rest)
 
 
+{- |
+>>> Spreadsheet.toString '"' '\t' [["hello","world"],["end"]]
+"\"hello\"\t\"world\"\n\"end\"\n"
+>>> Spreadsheet.toString '"' ',' [["hello,world","really"],["end"]]
+"\"hello,world\",\"really\"\n\"end\"\n"
+>>> Spreadsheet.toString '"' ';' [["hello \"world\""],["really"]]
+"\"hello \"\"world\"\"\"\n\"really\"\n"
+>>> Spreadsheet.toString '"' ',' [["hello\nworld"]]
+"\"hello\nworld\"\n"
+>>> take 50 $ Spreadsheet.toString '"' ',' $ repeat ["hello","world"]
+"\"hello\",\"world\"\n\"hello\",\"world\"\n\"hello\",\"world\"\n\"h"
+>>> take 50 $ Spreadsheet.toString '"' ',' [cycle ["hello","world"]]
+"\"hello\",\"world\",\"hello\",\"world\",\"hello\",\"world\",\"h"
+
+prop> :{
+   QC.forAll (QC.elements ";,\t ") $ \sep tableNE ->
+   let table = map QC.getNonEmpty tableNE in
+   table ==
+   MEA.result (Spreadsheet.fromString '"' sep
+                  (Spreadsheet.toString '"' sep table))
+:}
+-}
 toString :: Char -> Char -> T -> String
 toString qm sep =
    unlines . map (concat . intersperse [sep] . map (quote qm))
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,10 @@
+-- Do not edit! Automatically created with doctest-extract.
+module Main where
+
+import qualified Test.Data.Spreadsheet
+
+import qualified Test.DocTest.Driver as DocTest
+
+main :: IO ()
+main = DocTest.run $ do
+    Test.Data.Spreadsheet.test
diff --git a/test/Test/Data/Spreadsheet.hs b/test/Test/Data/Spreadsheet.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Data/Spreadsheet.hs
@@ -0,0 +1,96 @@
+-- Do not edit! Automatically created with doctest-extract from src/Data/Spreadsheet.hs
+{-# LINE 24 "src/Data/Spreadsheet.hs" #-}
+
+module Test.Data.Spreadsheet where
+
+import Test.DocTest.Base
+import qualified Test.DocTest.Driver as DocTest
+
+{-# LINE 25 "src/Data/Spreadsheet.hs" #-}
+import     qualified Data.Spreadsheet as Spreadsheet
+import     qualified Control.Monad.Exception.Asynchronous.Lazy as MEA
+import     qualified Test.QuickCheck as QC
+
+test :: DocTest.T ()
+test = do
+ DocTest.printPrefix "Data.Spreadsheet:100: "
+{-# LINE 100 "src/Data/Spreadsheet.hs" #-}
+ DocTest.example(
+{-# LINE 100 "src/Data/Spreadsheet.hs" #-}
+    Spreadsheet.fromString '"' '\t' "\"hello\"\t\"world\"\n\"end\"\n"
+  )
+  [ExpectedLine [LineChunk "Exceptional {exception = Nothing, result = [[\"hello\",\"world\"],[\"end\"]]}"]]
+ DocTest.printPrefix "Data.Spreadsheet:102: "
+{-# LINE 102 "src/Data/Spreadsheet.hs" #-}
+ DocTest.example(
+{-# LINE 102 "src/Data/Spreadsheet.hs" #-}
+    Spreadsheet.fromString '"' ',' "\"hello,world\",\"really\"\n\"end\"\n"
+  )
+  [ExpectedLine [LineChunk "Exceptional {exception = Nothing, result = [[\"hello,world\",\"really\"],[\"end\"]]}"]]
+ DocTest.printPrefix "Data.Spreadsheet:104: "
+{-# LINE 104 "src/Data/Spreadsheet.hs" #-}
+ DocTest.example(
+{-# LINE 104 "src/Data/Spreadsheet.hs" #-}
+    Spreadsheet.fromString '"' ';' "\"hello \"\"world\"\"\"\n\"really\"\n"
+  )
+  [ExpectedLine [LineChunk "Exceptional {exception = Nothing, result = [[\"hello \\\"world\\\"\"],[\"really\"]]}"]]
+ DocTest.printPrefix "Data.Spreadsheet:106: "
+{-# LINE 106 "src/Data/Spreadsheet.hs" #-}
+ DocTest.example(
+{-# LINE 106 "src/Data/Spreadsheet.hs" #-}
+    Spreadsheet.fromString '"' ',' "\"hello\nworld\"\n"
+  )
+  [ExpectedLine [LineChunk "Exceptional {exception = Nothing, result = [[\"hello\\nworld\"]]}"]]
+ DocTest.printPrefix "Data.Spreadsheet:129: "
+{-# LINE 129 "src/Data/Spreadsheet.hs" #-}
+ DocTest.example(
+{-# LINE 129 "src/Data/Spreadsheet.hs" #-}
+    Spreadsheet.toString '"' '\t' [["hello","world"],["end"]]
+  )
+  [ExpectedLine [LineChunk "\"\\\"hello\\\"\\t\\\"world\\\"\\n\\\"end\\\"\\n\""]]
+ DocTest.printPrefix "Data.Spreadsheet:131: "
+{-# LINE 131 "src/Data/Spreadsheet.hs" #-}
+ DocTest.example(
+{-# LINE 131 "src/Data/Spreadsheet.hs" #-}
+    Spreadsheet.toString '"' ',' [["hello,world","really"],["end"]]
+  )
+  [ExpectedLine [LineChunk "\"\\\"hello,world\\\",\\\"really\\\"\\n\\\"end\\\"\\n\""]]
+ DocTest.printPrefix "Data.Spreadsheet:133: "
+{-# LINE 133 "src/Data/Spreadsheet.hs" #-}
+ DocTest.example(
+{-# LINE 133 "src/Data/Spreadsheet.hs" #-}
+    Spreadsheet.toString '"' ';' [["hello \"world\""],["really"]]
+  )
+  [ExpectedLine [LineChunk "\"\\\"hello \\\"\\\"world\\\"\\\"\\\"\\n\\\"really\\\"\\n\""]]
+ DocTest.printPrefix "Data.Spreadsheet:135: "
+{-# LINE 135 "src/Data/Spreadsheet.hs" #-}
+ DocTest.example(
+{-# LINE 135 "src/Data/Spreadsheet.hs" #-}
+    Spreadsheet.toString '"' ',' [["hello\nworld"]]
+  )
+  [ExpectedLine [LineChunk "\"\\\"hello\\nworld\\\"\\n\""]]
+ DocTest.printPrefix "Data.Spreadsheet:137: "
+{-# LINE 137 "src/Data/Spreadsheet.hs" #-}
+ DocTest.example(
+{-# LINE 137 "src/Data/Spreadsheet.hs" #-}
+    take 50 $ Spreadsheet.toString '"' ',' $ repeat ["hello","world"]
+  )
+  [ExpectedLine [LineChunk "\"\\\"hello\\\",\\\"world\\\"\\n\\\"hello\\\",\\\"world\\\"\\n\\\"hello\\\",\\\"world\\\"\\n\\\"h\""]]
+ DocTest.printPrefix "Data.Spreadsheet:139: "
+{-# LINE 139 "src/Data/Spreadsheet.hs" #-}
+ DocTest.example(
+{-# LINE 139 "src/Data/Spreadsheet.hs" #-}
+    take 50 $ Spreadsheet.toString '"' ',' [cycle ["hello","world"]]
+  )
+  [ExpectedLine [LineChunk "\"\\\"hello\\\",\\\"world\\\",\\\"hello\\\",\\\"world\\\",\\\"hello\\\",\\\"world\\\",\\\"h\""]]
+ DocTest.printPrefix "Data.Spreadsheet:142: "
+{-# LINE 142 "src/Data/Spreadsheet.hs" #-}
+ DocTest.property(
+{-# LINE 142 "src/Data/Spreadsheet.hs" #-}
+        
+   QC.forAll (QC.elements ";,\t ") $ \sep tableNE ->
+   let table = map QC.getNonEmpty tableNE in
+   table ==
+   MEA.result (Spreadsheet.fromString '"' sep
+                  (Spreadsheet.toString '"' sep table))
+  )
