diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Revision history for libfuse3
 
+## 0.1.1.1 -- 2020-10-06
+
+* Minor improvements on the documentations
+* Add tests
+* Correct the version numbers in `configure.ac`
+
 ## 0.1.1.0 -- 2020-08-29
 
 * Improve the situation with signals
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@
 This package depends on the C library [libfuse][libfuse] and `pkg-config`. Please install them with your system package manager before building this package. For example, on Ubuntu:
 
 ```sh
-sudo apt-get update && sudo apt-get install libfuse3-dev pkg-config
+sudo apt-get update && sudo apt-get install libfuse3-dev fuse3 pkg-config
 ```
 
 **NOTE:** `libfuse3-dev` is not available until Ubuntu-20.04 (a.k.a. "focal").
@@ -44,7 +44,7 @@
 
 - A signal needs to be sent twice to stop the process and unmount the filesystem.
   - This means you have to run `kill -2 <pid>` twice or hit `Ctrl-C` twice (if running in foreground).
-  - On the other hand, `fusermount -u` can unmount the filesystem on the first attempt.
+  - On the other hand, `fusermount3 -u` can unmount the filesystem on the first attempt.
 - Not all Haskell-friendly bindings to the FUSE operations are implemented yet, including but not limited to:
   - `struct fuse_conn_info`. The availability of filesystem capabilities such as `FUSE_CAP_HANDLE_KILLPRIV` can't be checked.
   - Setting the fields of `struct fuse_file_info` from the certain fuse operations.
@@ -58,7 +58,7 @@
 - [HFuse][HFuse]: The bindings to libfuse-2.x
   - `libfuse3` is based on `HFuse` (with massive rewrites).
   - `libfuse3` has more complete API and exposes internal (and unstable) API to allow workarounds.
-- [fuse-rs][fuse-rs]: The Rust implementation of FUSE. Unlike this package, `fuse-rs` implements the FUSE protocol itself (i.e. replaces `libfuse`). See [its README](https://github.com/zargony/fuse-rs) for overview.
+- [fuse-rs][fuse-rs]: The Rust implementation of FUSE. Unlike this package, `fuse-rs` implements the FUSE protocol itself, mostly reimplementing `libfuse` in Rust. See [its README](https://github.com/zargony/fuse-rs) for overview.
 
 [libfuse]: https://github.com/libfuse/libfuse
 [HFuse]: https://github.com/m15k/hfuse
diff --git a/configure b/configure
--- a/configure
+++ b/configure
@@ -1,6 +1,6 @@
 #! /bin/sh
 # Guess values for system-dependent variables and create Makefiles.
-# Generated by GNU Autoconf 2.69 for Haskell libfuse3 package 0.1.0.0.
+# Generated by GNU Autoconf 2.69 for Haskell libfuse3 package 0.1.1.1.
 #
 #
 # Copyright (C) 1992-1996, 1998-2012 Free Software Foundation, Inc.
@@ -576,8 +576,8 @@
 # Identity of this package.
 PACKAGE_NAME='Haskell libfuse3 package'
 PACKAGE_TARNAME='haskell-libfuse3-package'
-PACKAGE_VERSION='0.1.0.0'
-PACKAGE_STRING='Haskell libfuse3 package 0.1.0.0'
+PACKAGE_VERSION='0.1.1.1'
+PACKAGE_STRING='Haskell libfuse3 package 0.1.1.1'
 PACKAGE_BUGREPORT=''
 PACKAGE_URL=''
 
@@ -1192,7 +1192,7 @@
   # Omit some internal or obsolete options to make the list less imposing.
   # This message is too long to be a string in the A/UX 3.1 sh.
   cat <<_ACEOF
-\`configure' configures Haskell libfuse3 package 0.1.0.0 to adapt to many kinds of systems.
+\`configure' configures Haskell libfuse3 package 0.1.1.1 to adapt to many kinds of systems.
 
 Usage: $0 [OPTION]... [VAR=VALUE]...
 
@@ -1254,7 +1254,7 @@
 
 if test -n "$ac_init_help"; then
   case $ac_init_help in
-     short | recursive ) echo "Configuration of Haskell libfuse3 package 0.1.0.0:";;
+     short | recursive ) echo "Configuration of Haskell libfuse3 package 0.1.1.1:";;
    esac
   cat <<\_ACEOF
 
@@ -1341,7 +1341,7 @@
 test -n "$ac_init_help" && exit $ac_status
 if $ac_init_version; then
   cat <<\_ACEOF
-Haskell libfuse3 package configure 0.1.0.0
+Haskell libfuse3 package configure 0.1.1.1
 generated by GNU Autoconf 2.69
 
 Copyright (C) 2012 Free Software Foundation, Inc.
@@ -1453,7 +1453,7 @@
 This file contains any messages produced by compilers while
 running configure, to aid debugging if configure makes a mistake.
 
-It was created by Haskell libfuse3 package $as_me 0.1.0.0, which was
+It was created by Haskell libfuse3 package $as_me 0.1.1.1, which was
 generated by GNU Autoconf 2.69.  Invocation command line was
 
   $ $0 $@
@@ -3340,7 +3340,7 @@
 # report actual input values of CONFIG_FILES etc. instead of their
 # values after options handling.
 ac_log="
-This file was extended by Haskell libfuse3 package $as_me 0.1.0.0, which was
+This file was extended by Haskell libfuse3 package $as_me 0.1.1.1, which was
 generated by GNU Autoconf 2.69.  Invocation command line was
 
   CONFIG_FILES    = $CONFIG_FILES
@@ -3402,7 +3402,7 @@
 cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1
 ac_cs_config="`$as_echo "$ac_configure_args" | sed 's/^ //; s/[\\""\`\$]/\\\\&/g'`"
 ac_cs_version="\\
-Haskell libfuse3 package config.status 0.1.0.0
+Haskell libfuse3 package config.status 0.1.1.1
 configured by $0, generated by GNU Autoconf 2.69,
   with options \\"\$ac_cs_config\\"
 
diff --git a/configure.ac b/configure.ac
--- a/configure.ac
+++ b/configure.ac
@@ -1,4 +1,4 @@
-AC_INIT([Haskell libfuse3 package], [0.1.1.0])
+AC_INIT([Haskell libfuse3 package], [0.1.1.1])
 
 # Safety check: Ensure that we are in the correct source directory.
 AC_CONFIG_SRCDIR([libfuse3.cabal])
diff --git a/libfuse3.cabal b/libfuse3.cabal
--- a/libfuse3.cabal
+++ b/libfuse3.cabal
@@ -3,7 +3,7 @@
 -- For further documentation, see http://haskell.org/cabal/users-guide/
 
 name:                libfuse3
-version:             0.1.1.0
+version:             0.1.1.1
 synopsis:            A Haskell binding for libfuse-3.x
 description:         Bindings for libfuse, the FUSE userspace reference implementation, of version 3.x. Compatible with Linux.
 -- bug-reports:
@@ -52,6 +52,23 @@
   include-dirs:        include
   default-language:    Haskell2010
   ghc-options:         -Wall -fdefer-typed-holes
+
+test-suite integtest
+  type:                exitcode-stdio-1.0
+  main-is:             Main.hs
+  build-depends:       libfuse3, base
+                     , bytestring
+                     , directory
+                     , filepath
+                     , hspec
+                     , process
+                     , temporary
+                     , unix
+  -- other-modules:
+  -- other-extensions:
+  hs-source-dirs:      test/integtest
+  default-language:    Haskell2010
+  ghc-options:         -Wall -fdefer-typed-holes -threaded
 
 executable null
   if flag(examples)
diff --git a/src/System/LibFuse3/Internal.hsc b/src/System/LibFuse3/Internal.hsc
--- a/src/System/LibFuse3/Internal.hsc
+++ b/src/System/LibFuse3/Internal.hsc
@@ -928,7 +928,7 @@
     -- is unmounted.
     -- Adding the RTS option @--install-signal-handlers=no@ does not fix the issue.
     --
-    -- On the other hand, @fusermount -u@ successfully unmounts the filesystem on the first
+    -- On the other hand, @fusermount3 -u@ successfully unmounts the filesystem on the first
     -- attempt.
     withSignalHandlers (C.fuse_session_exit session) $ do
       retVal <- C.fuse_loop_mt_31 pFuse cloneFd
@@ -954,22 +954,6 @@
 -- This is all that has to be called from the @main@ function. On top of
 -- the `FuseOperations` record with filesystem implementation, you must give
 -- an exception handler converting Haskell exceptions to `Errno`.
---
--- This function does the following:
---
---   * parses command line options
---
---   * passes all options after @--@ to the fusermount program
---
---   * mounts the filesystem by calling @fusermount@
---
---   * installs signal handlers
---
---   * registers an exit handler to unmount the filesystem on program exit
---
---   * registers the operations
---
---   * calls FUSE event loop
 fuseMain :: Exception e => FuseOperations fh dh -> (e -> IO Errno) -> IO ()
 fuseMain ops handler = do
   -- this used to be implemented using libfuse's fuse_main. Doing this will fork()
diff --git a/test/integtest/Main.hs b/test/integtest/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/integtest/Main.hs
@@ -0,0 +1,110 @@
+-- Please note that @cabal v2-test@ handles the test output very badly for some reason;
+-- the output is duplicated and interleaved.
+--
+-- Use @cabal v2-run -- integtest@ for more reliable output.
+module Main where
+
+import Control.Exception (SomeException, finally)
+import Control.Monad (when)
+import Data.Bits ((.|.))
+import Data.Maybe (isNothing)
+import Foreign.C (eIO, eNOENT)
+import System.Directory (findExecutable, listDirectory)
+import System.Environment (withArgs)
+import System.Exit (exitFailure)
+import System.FilePath ((</>))
+import System.IO (hPrint, hPutStrLn, stderr)
+import System.IO.Temp (withSystemTempDirectory)
+import System.LibFuse3
+import System.Posix.Files (directoryMode, regularFileMode)
+import System.Posix.Process (getProcessStatus, forkProcess)
+import System.Process (callProcess)
+import Test.Hspec (Spec, SpecWith, around, describe, hspec, it, shouldBe)
+
+import qualified Data.ByteString as B
+
+-- | Runs a spec on a FUSE filesystem mounted on a temporary directory.
+withFileSystem :: FuseOperations fh dh -> SpecWith FilePath -> Spec
+withFileSystem ops = around $ \theSpec ->
+  withSystemTempDirectory "libfuse3test" $ \mountPoint -> do
+    let unmount = do
+          hPutStrLn stderr $ "unmounting : " <> mountPoint
+          callProcess "fusermount3" ["-u", mountPoint]
+    withArgs [mountPoint] $ do
+      hPutStrLn stderr $ "mounting on: " <> mountPoint
+      pid <- forkProcess $ fuseMain ops (\e -> hPrint stderr (e :: SomeException) >> pure eIO)
+      flip finally unmount $ do
+        -- wait for fuseMain to daemonize
+        _ <- getProcessStatus True False pid
+        theSpec mountPoint
+
+specs :: Spec
+specs = do
+  -- A basic test of fuseGetattr; we get what we give
+  describe "fuseGetattr" $
+    let stat = defaultFileStat
+          { fileMode = directoryMode .|. 0o755
+          , linkCount = 1
+          }
+        ops = defaultFuseOperations
+          { fuseGetattr = Just $ \path _ -> case path of
+              "/" -> pure $ Right stat
+              _ -> pure $ Left eNOENT
+          }
+    in withFileSystem ops $ it "getFileStat reads the stat as is" $ \mountPoint -> do
+         -- assumes that `getFileStat` works correctly
+         stat' <- getFileStat mountPoint
+         -- ignore differences in fileID
+         -- TODO test use_ino?
+         stat' { fileID = fileID stat } `shouldBe` stat
+
+  -- If `fuseOpen` is not defined, make sure that `fuseRead` doesn't throw unless
+  -- the file handle is evaluated.
+  describe "fuseRead without fuseOpen" $
+    let content = B.singleton 0
+        ops = defaultFuseOperations
+          { fuseGetattr = Just $ \path mfh ->
+              case mfh of
+                Just _ -> pure $ Left eIO -- this should never happen
+                Nothing
+                  | path == "/file" -> pure $ Right $ defaultFileStat
+                      { fileMode = regularFileMode .|. 0o644
+                      , fileSize = 1
+                      }
+                  | otherwise -> pure $ Left eNOENT
+          , fuseRead = Just $ \path _fh len off ->
+              -- check that evaluating the args other than the file handle is harmless
+              path `seq` len `seq` off `seq` (pure $ Right content)
+          }
+    in withFileSystem ops $ it "fileRead reads without a crash" $ \mountPoint -> do
+         readContent <- B.readFile $ mountPoint </> "file"
+         readContent `shouldBe` content
+
+  -- If `fuseOpendir` is not defined, make sure that `fuseReaddir` doesn't throw unless
+  -- the directory handle is evaluated.
+  describe "fuseReaddir without fuseOpendir" $
+    let ops = defaultFuseOperations
+          { fuseGetattr = Just $ \path _ ->
+              case () of
+                _ | path == "/dir/file" -> pure $ Right $ defaultFileStat
+                      { fileMode = regularFileMode .|. 0o644
+                      , fileSize = 1
+                      }
+                  | path == "/dir" -> pure $ Right $ defaultFileStat
+                      { fileMode = directoryMode .|. 0o755
+                      }
+                  | otherwise -> pure $ Left eNOENT
+          , fuseReaddir = Just $ \path _dh ->
+              path `seq` (pure $ Right $ [(".", Nothing), ("..", Nothing), ("file", Nothing)])
+          }
+    in withFileSystem ops $ it "fileReaddir reads without a crash" $ \mountPoint -> do
+         entries <- listDirectory $ mountPoint </> "dir"
+         entries `shouldBe` ["file"]
+
+main :: IO ()
+main = do
+  mfusermount3 <- findExecutable "fusermount3"
+  when (isNothing mfusermount3) $ do
+    hPutStrLn stderr "Command 'fusermount3' not found. Please install it first. It usually comes with a package named like 'fuse3'."
+    exitFailure
+  hspec specs
