diff --git a/dune/vtk/filewriter.hh b/dune/vtk/filewriter.hh
index 5a8472bc33f7ee9dc7d9de82857ef6a2b80643dc..ecddbd2b0f6f72dcebb8f2f249af0e278e1ed5c0 100644
--- a/dune/vtk/filewriter.hh
+++ b/dune/vtk/filewriter.hh
@@ -14,7 +14,7 @@ namespace Dune
     virtual ~FileWriter () = default;
 
     /// Write to file given by `filename` and (optionally) store additional data in `dataDir`
-    virtual void write (std::string const& filename, Std::optional<std::string> dataDir = {}) const = 0;
+    virtual std::string write (std::string const& filename, Std::optional<std::string> dataDir = {}) const = 0;
   };
 
 } // end namespace Dune
diff --git a/dune/vtk/pvdwriter.hh b/dune/vtk/pvdwriter.hh
index 3faec46272554844efa38672007df182a3616b8e..38fef8f8055847aec5b8cf98642b8fb11b1337c6 100644
--- a/dune/vtk/pvdwriter.hh
+++ b/dune/vtk/pvdwriter.hh
@@ -52,7 +52,7 @@ namespace Dune
      * \param dir (Ignored) Timestep files are already written and their filenames are
      *            stored in \ref timesteps_.
      **/
-    virtual void write (std::string const& fn, Std::optional<std::string> dir = {}) const override;
+    virtual std::string write (std::string const& fn, Std::optional<std::string> dir = {}) const override;
 
     /// Attach point data to the writer, \see VtkFunction for possible arguments
     template <class Function, class... Args>
diff --git a/dune/vtk/pvdwriter.impl.hh b/dune/vtk/pvdwriter.impl.hh
index 26ff028ea050bbefc17b30b39420f1daed41c30d..347eaeb53c520e98962e80e8c0729367a19ec46e 100644
--- a/dune/vtk/pvdwriter.impl.hh
+++ b/dune/vtk/pvdwriter.impl.hh
@@ -49,7 +49,7 @@ void PvdWriter<W>
 
 
 template <class W>
-void PvdWriter<W>
+std::string PvdWriter<W>
   ::write (std::string const& fn, Std::optional<std::string> /*dir*/) const
 {
   auto p = filesystem::path(fn);
@@ -57,9 +57,12 @@ void PvdWriter<W>
   p.remove_filename();
   p /= name.string();
 
+  std::string outputFilename;
+
   int commRank = vtkWriter_.comm().rank();
   if (commRank == 0) {
-    std::ofstream out(p.string() + ".pvd", std::ios_base::ate | std::ios::binary);
+    outputFilename = p.string() + ".pvd";
+    std::ofstream out(outputFilename, std::ios_base::ate | std::ios::binary);
     assert(out.is_open());
 
     out.imbue(std::locale::classic());
@@ -69,6 +72,8 @@ void PvdWriter<W>
 
     writeFile(out);
   }
+
+  return outputFilename;
 }
 
 
diff --git a/dune/vtk/vtktimeserieswriter.hh b/dune/vtk/vtktimeserieswriter.hh
index 83a3d4cf539a6d2ead06f6ed0d12f8675c3c61aa..21bcd6c20399d14e1e07bf11fb5ee437420e429f 100644
--- a/dune/vtk/vtktimeserieswriter.hh
+++ b/dune/vtk/vtktimeserieswriter.hh
@@ -76,8 +76,10 @@ namespace Dune
      *
      * \param fn   Filename of the Timeseries file. May contain a directory and any file extension.
      * \param dir  The optional parameter specifies the directory of the partition files.
+     * 
+     * \returns File name of the actual written file
      **/
-    virtual void write (std::string const& fn, Std::optional<std::string> dir = {}) const override;
+    virtual std::string write (std::string const& fn, Std::optional<std::string> dir = {}) const override;
 
     /// Attach point data to the writer, \see VtkFunction for possible arguments
     template <class Function, class... Args>
diff --git a/dune/vtk/vtktimeserieswriter.impl.hh b/dune/vtk/vtktimeserieswriter.impl.hh
index 52abdec86b63be238d209aa7a0482f903e75790d..96cacb967f83d1fd1043b8d8ef552e0ce6e5fa61 100644
--- a/dune/vtk/vtktimeserieswriter.impl.hh
+++ b/dune/vtk/vtktimeserieswriter.impl.hh
@@ -74,7 +74,7 @@ void VtkTimeseriesWriter<W>
 
 
 template <class W>
-void VtkTimeseriesWriter<W>
+std::string VtkTimeseriesWriter<W>
   ::write (std::string const& fn, Std::optional<std::string> dir) const
 {
   assert( initialized_ );
@@ -96,9 +96,11 @@ void VtkTimeseriesWriter<W>
   if (commSize > 1)
     serial_fn += "_p" + std::to_string(commRank);
 
+  std::string outputFilename;
+
   { // write serial file
-    std::ofstream serial_out(serial_fn + "." + vtkWriter_.getFileExtension(),
-                             std::ios_base::ate | std::ios::binary);
+    outputFilename = serial_fn + "." + vtkWriter_.getFileExtension();
+    std::ofstream serial_out(outputFilename, std::ios_base::ate | std::ios::binary);
     assert(serial_out.is_open());
 
     serial_out.imbue(std::locale::classic());
@@ -111,8 +113,8 @@ void VtkTimeseriesWriter<W>
 
   if (commSize > 1 && commRank == 0) {
     // write parallel file
-    std::ofstream parallel_out(parallel_fn + ".p" + vtkWriter_.getFileExtension(),
-                               std::ios_base::ate | std::ios::binary);
+    outputFilename = parallel_fn + ".p" + vtkWriter_.getFileExtension();
+    std::ofstream parallel_out(outputFilename, std::ios_base::ate | std::ios::binary);
     assert(parallel_out.is_open());
 
     parallel_out.imbue(std::locale::classic());
@@ -122,6 +124,8 @@ void VtkTimeseriesWriter<W>
 
     vtkWriter_.writeTimeseriesParallelFile(parallel_out, rel_fn, commSize, timesteps_);
   }
+
+  return outputFilename;
 }
 
 } // end namespace Dune
diff --git a/dune/vtk/vtkwriterinterface.hh b/dune/vtk/vtkwriterinterface.hh
index 33121d2525c4bdf548fb24dcf8f95273cc72b326..97117ce228db631dee5414c596aad56e6446161c 100644
--- a/dune/vtk/vtkwriterinterface.hh
+++ b/dune/vtk/vtkwriterinterface.hh
@@ -67,12 +67,7 @@ namespace Dune
       , format_(format)
       , datatype_(datatype)
     {
-#if !HAVE_VTK_ZLIB
-      if (format_ == Vtk::COMPRESSED) {
-        std::cout << "Dune is compiled without compression. Falling back to BINARY VTK output!\n";
-        format_ = Vtk::BINARY;
-      }
-#endif
+      setFormat(format);
     }
 
 
@@ -80,8 +75,10 @@ namespace Dune
     /**
      * \param fn   Filename of the VTK file. May contain a directory and any file extension.
      * \param dir  The optional parameter specifies the directory of the partition files for parallel writes.
+     * 
+     * \returns File name that is actually written.
      **/
-    virtual void write (std::string const& fn, Std::optional<std::string> dir = {}) const override;
+    virtual std::string write (std::string const& fn, Std::optional<std::string> dir = {}) const override;
 
     /// \brief Attach point data to the writer
     /**
@@ -112,6 +109,26 @@ namespace Dune
       return *this;
     }
 
+
+    // Sets the VTK file format
+    void setFormat (Vtk::FormatTypes format)
+    {
+      format_ = format;
+      
+#if !HAVE_VTK_ZLIB
+      if (format_ == Vtk::COMPRESSED) {
+        std::cout << "Dune is compiled without compression. Falling back to BINARY VTK output!\n";
+        format_ = Vtk::BINARY;
+      }
+#endif
+    }
+
+    // Sets the global datatype used for coordinates and other global float values
+    void setDatatype (Vtk::DataTypes datatype)
+    {
+      datatype_ = datatype;
+    }
+
   private:
     /// Write a serial VTK file in Unstructured format
     virtual void writeSerialFile (std::ofstream& out) const = 0;
diff --git a/dune/vtk/vtkwriterinterface.impl.hh b/dune/vtk/vtkwriterinterface.impl.hh
index 3e2fe2f3de7656b1b4a3b42d2660ede7438da25b..f0e7715b9d5f98998ad0c89a66763140945ac3ae 100644
--- a/dune/vtk/vtkwriterinterface.impl.hh
+++ b/dune/vtk/vtkwriterinterface.impl.hh
@@ -23,7 +23,7 @@
 namespace Dune {
 
 template <class GV, class DC>
-void VtkWriterInterface<GV,DC>
+std::string VtkWriterInterface<GV,DC>
   ::write (std::string const& fn, Std::optional<std::string> dir) const
 {
   dataCollector_->update();
@@ -43,8 +43,11 @@ void VtkWriterInterface<GV,DC>
   if (comm().size() > 1)
     serial_fn += "_p" + std::to_string(comm().rank());
 
+  std::string outputFilename;
+  
   { // write serial file
-    std::ofstream serial_out(serial_fn + "." + fileExtension(), std::ios_base::ate | std::ios::binary);
+    outputFilename = serial_fn + "." + fileExtension();
+    std::ofstream serial_out(outputFilename, std::ios_base::ate | std::ios::binary);
     assert(serial_out.is_open());
 
     serial_out.imbue(std::locale::classic());
@@ -56,8 +59,9 @@ void VtkWriterInterface<GV,DC>
   }
 
   if (comm().size() > 1 && comm().rank() == 0) {
-    // write parallel file
-    std::ofstream parallel_out(parallel_fn + ".p" + fileExtension(), std::ios_base::ate | std::ios::binary);
+    // write parallel filee
+    outputFilename = parallel_fn + ".p" + fileExtension();
+    std::ofstream parallel_out(outputFilename, std::ios_base::ate | std::ios::binary);
     assert(parallel_out.is_open());
 
     parallel_out.imbue(std::locale::classic());
@@ -67,6 +71,8 @@ void VtkWriterInterface<GV,DC>
 
     writeParallelFile(parallel_out, rel_fn, comm().size());
   }
+
+  return outputFilename;
 }
 
 
diff --git a/dune/vtk/writers/CMakeLists.txt b/dune/vtk/writers/CMakeLists.txt
index 779237467679ed664f32981dfa76537d2d2cb4fb..f32eb18971cfa16f619bd627ce355d5eb0935d99 100644
--- a/dune/vtk/writers/CMakeLists.txt
+++ b/dune/vtk/writers/CMakeLists.txt
@@ -9,3 +9,5 @@ install(FILES
   vtkunstructuredgridwriter.hh
   vtkunstructuredgridwriter.impl.hh
   DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/dune/vtkwriter/writers)
+
+add_subdirectory(test)
\ No newline at end of file
diff --git a/dune/vtk/writers/test/CMakeLists.txt b/dune/vtk/writers/test/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..5197f1d7423166aac3e5a16fde2c0b36ac7eb5c9
--- /dev/null
+++ b/dune/vtk/writers/test/CMakeLists.txt
@@ -0,0 +1,2 @@
+dune_add_test(SOURCES test-vtkwriter.cc
+  LINK_LIBRARIES dunevtk)
\ No newline at end of file
diff --git a/dune/vtk/writers/test/checkvtkfile.hh b/dune/vtk/writers/test/checkvtkfile.hh
new file mode 100644
index 0000000000000000000000000000000000000000..413adb278cc3a7140165006df5847e7d565c1278
--- /dev/null
+++ b/dune/vtk/writers/test/checkvtkfile.hh
@@ -0,0 +1,181 @@
+#ifndef DUNE_VTK_TEST_CHECKVTKFILE_HH
+#define DUNE_VTK_TEST_CHECKVTKFILE_HH
+
+#include <cstddef>
+#include <cstdlib>
+#include <iostream>
+#include <iomanip>
+#include <ios>
+#include <iostream>
+#include <ostream>
+#include <set>
+#include <sstream>
+#include <string>
+
+#include <sys/wait.h>
+
+#include <dune/common/exceptions.hh>
+#include <dune/common/test/testsuite.hh>
+
+namespace Dune {
+namespace Impl {
+
+// quote so the result can be used inside '...' in python
+// quotes not included in the result
+inline std::string pyq(const std::string &s)
+{
+  std::ostringstream result;
+  for(std::size_t i = 0; i < s.size(); ++i)
+  {
+    char c = s[i];
+    switch(c) {
+    case '\'': result << "\\'";  break;
+    case '\\': result << "\\\\"; break;
+    case '\n': result << "\\n";  break;
+    default:
+      if(c < 32 || c >= 127)
+        result << "\\x" << std::hex << std::setfill('0') << std::setw(2)
+               << static_cast<int>(c);
+      else
+        result << c;
+    }
+  }
+  return result.str();
+}
+
+// quote so the result can be used inside '...' in the bourne shell
+// quotes not included in the result
+inline std::string shq(const std::string &s)
+{
+  std::ostringstream result;
+  bool pend = false;
+  for(std::size_t i = 0; i < s.size(); ++i)
+  {
+    char c = s[i];
+    switch(c) {
+    case '\0': DUNE_THROW(Dune::NotImplemented,
+                          "Can't pass \\0 through the shell");
+    case '\'': result << (pend ? "" : "'") << "\\'"; pend = true;  break;
+    default:   result << (pend ? "'" : "") << c;     pend = false; break;
+    }
+  }
+  if(pend) result << "'";
+  return result.str();
+}
+
+inline int runShell(const std::string &code)
+{
+  int result = std::system(code.c_str());
+  // Avoid returning anything that is a multiple of 256, unless the return
+  // value was 0.  This way the return value can be directly used as an
+  // argument to exit(), which usually interprets its argument modulo 256.
+  if(WIFEXITED(result))
+    return WEXITSTATUS(result);
+  if(WIFSIGNALED(result))
+    return WTERMSIG(result) + 256;
+  else
+    return 513;
+}
+
+inline int runPython(const std::string &code)
+{
+  return runShell("python -c '"+shq(code)+"'");
+}
+
+inline bool is_suffix(const std::string &s, const std::string &suffix)
+{
+  return s.size() >= suffix.size() &&
+    s.compare(s.size() - suffix.size(), suffix.size(), suffix) == 0;
+}
+
+inline bool havePythonVTK()
+{
+  static const bool result = [] {
+    // This check is invoked only once, even in a multithreading environment,
+    // since it is invoked in the initializer of a static variable.
+    if(runPython("from vtk import *") == 0)
+      return true;
+    std::cerr << "warning: python or python vtk module not available.  This "
+              << "will result in skipped tests, since we cannot check that "
+              << "vtk can read the files we wrote." << std::endl;
+    return false;
+  } ();
+
+  return result;
+}
+
+inline std::string pythonVTKReader(const std::string& filename)
+{
+  if     (is_suffix(filename, ".vtu"))  return "vtkXMLUnstructuredGridReader";
+  else if(is_suffix(filename, ".pvtu")) return "vtkXMLPUnstructuredGridReader";
+  else if(is_suffix(filename, ".vts"))  return "vtkXMLStructuredGridReader";
+  else if(is_suffix(filename, ".pvts")) return "vtkXMLPStructuredGridReader";
+  else if(is_suffix(filename, ".vtr"))  return "vtkXMLRectilinearGridReader";
+  else if(is_suffix(filename, ".pvtr")) return "vtkXMLPRectilinearGridReader";
+  else if(is_suffix(filename, ".vti"))  return "vtkXMLImageDataReader";
+  else if(is_suffix(filename, ".pvti")) return "vtkXMLPImageDataReader";
+  else DUNE_THROW(Dune::NotImplemented,
+                  "Unknown vtk file extension: " << filename);
+}
+
+} /* namespace Impl */
+
+class VTKChecker
+{
+public:
+  void push(const std::string& file)
+  {
+    auto res = files_.insert(file);
+    if (not res.second) {
+      testSuite_.check(false, "VTKChecker")
+        << "'" << file << "' was added multiple times";
+    }
+  }
+
+  int check()
+  {
+    if (not Impl::havePythonVTK()) {
+      return 77;
+    }
+    else if (not files_.empty()) {
+      const int result = Impl::runPython(generatePythonCode());
+      testSuite_.check(result == 0);
+    }
+    return testSuite_.exit();
+  }
+
+  const TestSuite& testSuite() const
+  {
+    return testSuite_;
+  }
+
+private:
+  std::string generatePythonCode() const
+  {
+    std::stringstream code;
+
+    code << "from vtk import *\n"
+          << "import sys\n"
+          << "passed = True\n";
+
+    for (const auto& file : files_) {
+      code << "reader = " << Impl::pythonVTKReader(file) << "()\n"
+            << "reader.SetFileName('" << Impl::pyq(file) << "')\n"
+            << "reader.Update()\n"
+            << "if (not (reader.GetOutput().GetNumberOfCells() > 0)):\n"
+            << "    print('ERROR in {}'.format('" << Impl::pyq(file) << "'))\n"
+            << "    passed = False\n";
+    }
+
+    code << "sys.exit(0 if passed else 1)\n";
+
+    return code.str();
+  }
+
+  std::set< std::string > files_;
+  TestSuite testSuite_;
+};
+
+} /* namespace Dune */
+
+#endif // DUNE_VTK_TEST_CHECKVTKFILE_HH
diff --git a/dune/vtk/writers/test/test-vtkwriter.cc b/dune/vtk/writers/test/test-vtkwriter.cc
new file mode 100644
index 0000000000000000000000000000000000000000..127e46cf9ae10c0e109d96dd7c75fc82e6c8a675
--- /dev/null
+++ b/dune/vtk/writers/test/test-vtkwriter.cc
@@ -0,0 +1,137 @@
+// -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+// vi: set et ts=4 sw=2 sts=2:
+
+#if HAVE_CONFIG_H
+#include "config.h" // autoconf defines, needed by the dune headers
+#endif
+
+#include <algorithm>
+#include <iostream>
+#include <ostream>
+#include <sstream>
+#include <string>
+#include <vector>
+
+#include <dune/common/fvector.hh>
+#include <dune/common/parallel/mpihelper.hh>
+#include <dune/functions/gridfunctions/analyticgridviewfunction.hh>
+#include <dune/grid/yaspgrid.hh>
+#if HAVE_UG
+  #include <dune/grid/uggrid.hh>
+#endif
+#include <dune/vtk/vtkwriter.hh>
+
+#include "checkvtkfile.hh"
+
+// accumulate exit status
+void acc (int &accresult, int result)
+{
+  if (accresult == 0 || (accresult == 77 && result != 0))
+    accresult = result;
+}
+
+struct Acc
+{
+  int operator() (int v1, int v2) const
+  {
+    acc(v1, v2);
+    return v1;
+  }
+};
+
+template <class GridView>
+int testGridView (std::string prefix, Dune::VTKChecker& vtkChecker, GridView const& gridView)
+{
+  enum { dim = GridView :: dimension };
+
+  Dune::VtkWriter<GridView> vtk(gridView);
+
+  auto f1 = Dune::Functions::makeAnalyticGridViewFunction([](const auto& x) { return std::sin(x.two_norm()); },gridView);
+  vtk.addCellData(f1, "scalar-valued lambda");
+
+  auto f2 = Dune::Functions::makeAnalyticGridViewFunction([](const auto& x) { return x; },gridView);
+  vtk.addPointData(f2, "vector-valued lambda");
+
+  int result = 0;
+
+  // ASCII files
+  {
+    vtk.setFormat(Dune::Vtk::ASCII);
+    std::string name = vtk.write(prefix + "_ascii");
+    if (gridView.comm().rank() == 0) vtkChecker.push(name);
+  }
+
+  // BINARY files
+  {
+    vtk.setFormat(Dune::Vtk::BINARY);
+    std::string name = vtk.write(prefix + "_binary");
+    if (gridView.comm().rank() == 0) vtkChecker.push(name);
+  }
+
+  // COMPRESSED files
+  {
+    vtk.setFormat(Dune::Vtk::COMPRESSED);
+    std::string name = vtk.write(prefix + "_compressed");
+    if (gridView.comm().rank() == 0) vtkChecker.push(name);
+  }
+
+  return result;
+}
+
+template <class Grid>
+int testGrid (std::string prefix, Dune::VTKChecker& vtkChecker, Grid& grid)
+{
+  if (grid.comm().rank() == 0)
+    std::cout << "vtkCheck(" << prefix << ")" << std::endl;
+
+  grid.globalRefine(1);
+
+  int result = 0;
+
+  acc(result, testGridView( prefix + "_leaf",   vtkChecker, grid.leafGridView() ));
+  acc(result, testGridView( prefix + "_level0", vtkChecker, grid.levelGridView(0) ));
+  acc(result, testGridView( prefix + "_level1", vtkChecker, grid.levelGridView(grid.maxLevel()) ));
+
+  return result;
+}
+
+int main (int argc, char** argv)
+{
+  using namespace Dune;
+  const MPIHelper& mpiHelper = MPIHelper::instance(argc, argv);
+
+  if (mpiHelper.rank() == 0)
+    std::cout << "vtktest: MPI_Comm_size == " << mpiHelper.size() << std::endl;
+
+  int result = 0; // pass by default
+
+  VTKChecker vtkChecker;
+
+  YaspGrid<1> yaspGrid1({1.0}, {8});
+  YaspGrid<2> yaspGrid2({1.0,2.0}, {8,4});
+  YaspGrid<3> yaspGrid3({1.0,2.0,3.0}, {8,4,4});
+
+  acc(result, testGrid("yaspgrid_1d", vtkChecker, yaspGrid1));
+  acc(result, testGrid("yaspgrid_2d", vtkChecker, yaspGrid2));
+  acc(result, testGrid("yaspgrid_3d", vtkChecker, yaspGrid3));
+
+#if HAVE_UG
+  auto ugGrid2a = StructuredGridFactory<UGGrid<2>>::createSimplexGrid({0.0,0.0}, {1.0,2.0}, {2u,4u});
+  auto ugGrid3a = StructuredGridFactory<UGGrid<3>>::createSimplexGrid({0.0,0.0,0.0}, {1.0,2.0,3.0}, {2u,4u,6u});
+
+  acc(result, testGrid("uggrid_simplex_2d", vtkChecker, *ugGrid2a));
+  acc(result, testGrid("uggrid_simplex_3d", vtkChecker, *ugGrid3a));
+
+  auto ugGrid2b = StructuredGridFactory<UGGrid<2>>::createCubeGrid({0.0,0.0}, {1.0,2.0}, {2u,4u});
+  auto ugGrid3b = StructuredGridFactory<UGGrid<3>>::createCubeGrid({0.0,0.0,0.0}, {1.0,2.0,3.0}, {2u,4u,6u});
+
+  acc(result, testGrid("uggrid_cube_2d", vtkChecker, *ugGrid2b));
+  acc(result, testGrid("uggrid_cube_3d", vtkChecker, *ugGrid3b));
+#endif
+
+  acc(result, vtkChecker.check());
+
+  mpiHelper.getCollectiveCommunication().allreduce<Acc>(&result, 1);
+
+  return result;
+}