diff --git a/scripts/cmake/test/MeshTest.cmake b/scripts/cmake/test/MeshTest.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..bc2e8d7d7bcc0e20855c3f043e16a83c13bc4065
--- /dev/null
+++ b/scripts/cmake/test/MeshTest.cmake
@@ -0,0 +1,179 @@
+#
+# MeshTest
+# -------
+#
+# MeshTest(
+#   NAME <name of the the test>
+#   PATH <working directory> # relative to SourceDir/Tests/Data
+#   EXECUTABLE <executable target> # optional, defaults to ogs
+#   EXECUTABLE_ARGS <arguments>
+#   WRAPPER <time|memcheck|callgrind|mpirun> # optional
+#   WRAPPER_ARGS <arguments> # optional
+#   REQUIREMENTS # optional simple boolean expression which has to be true to
+#                  enable the test, e.g.
+#                  OGS_USE_PETSC AND (OGS_USE_EIGEN OR OGS_USE_LIS)
+#   RUNTIME <in seconds> # optional for optimizing ctest duration
+#                          values should be taken from envinf1 serial job
+# )
+
+function (MeshTest)
+    if(NOT BUILD_TESTING)
+        return()
+    endif()
+    # parse arguments
+    set(options NONE)
+    set(oneValueArgs EXECUTABLE PATH NAME WRAPPER RUNTIME)
+    set(multiValueArgs EXECUTABLE_ARGS DATA DIFF_DATA WRAPPER_ARGS REQUIREMENTS)
+    cmake_parse_arguments(MeshTest "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
+
+
+    set(MeshTest_SOURCE_PATH "${Data_SOURCE_DIR}/${MeshTest_PATH}")
+    set(MeshTest_BINARY_PATH "${Data_BINARY_DIR}/${MeshTest_PATH}")
+    file(MAKE_DIRECTORY ${MeshTest_BINARY_PATH})
+    file(TO_NATIVE_PATH "${MeshTest_BINARY_PATH}" MeshTest_BINARY_PATH_NATIVE)
+    set(MeshTest_STDOUT_FILE_PATH "${MeshTest_BINARY_PATH}/${MeshTest_NAME}_stdout.log")
+
+    # set defaults
+    if (NOT DEFINED MeshTest_REQUIREMENTS)
+        set(MeshTest_REQUIREMENTS TRUE)
+    endif()
+    if (NOT DEFINED MeshTest_RUNTIME)
+        set(MeshTest_RUNTIME 1)
+    endif()
+
+    # --- Implement wrappers ---
+    # check requirements, disable if not met
+    if(${MeshTest_REQUIREMENTS})
+        # message(STATUS "Enabling test ${MeshTest_NAME}.")
+    else()
+        set(DISABLED_TESTS_LOG "${DISABLED_TESTS_LOG}\nRequirement ${MeshTest_REQUIREMENTS} not met! Disabling test ${MeshTest_NAME}." CACHE INTERNAL "")
+        return()
+    endif()
+
+    if(MeshTest_WRAPPER STREQUAL "time")
+        if(TIME_TOOL_PATH)
+            set(WRAPPER_COMMAND time)
+        else()
+            set(DISABLED_TESTS_LOG "${DISABLED_TESTS_LOG}\nDisabling time wrapper for ${MeshTest_NAME} as time exe was not found!" CACHE INTERNAL "")
+            set(MeshTest_WRAPPER_ARGS "")
+        endif()
+    elseif(MeshTest_WRAPPER STREQUAL "memcheck")
+        if(VALGRIND_TOOL_PATH)
+            set(WRAPPER_COMMAND "${VALGRIND_TOOL_PATH} --tool=memcheck --log-file=${MeshTest_SOURCE_PATH}/${MeshTest_NAME}_memcheck.log -v --leak-check=full --show-reachable=yes --track-origins=yes --malloc-fill=0xff --free-fill=0xff")
+            set(tester memcheck)
+        else()
+            set(DISABLED_TESTS_LOG "${DISABLED_TESTS_LOG}\nDisabling memcheck wrapper for ${MeshTest_NAME} as memcheck exe was not found!" CACHE INTERNAL "")
+            set(MeshTest_WRAPPER_ARGS "")
+        endif()
+    elseif(MeshTest_WRAPPER STREQUAL "callgrind")
+        if(VALGRIND_TOOL_PATH)
+            set(WRAPPER_COMMAND "${VALGRIND_TOOL_PATH} --tool=callgrind --branch-sim=yes --cache-sim=yes --dump-instr=yes --collect-jumps=yes")
+            unset(tester)
+        else()
+            set(DISABLED_TESTS_LOG "${DISABLED_TESTS_LOG}\nDisabling callgrind wrapper for ${MeshTest_NAME} as callgrind exe was not found!" CACHE INTERNAL "")
+            set(MeshTest_WRAPPER_ARGS "")
+        endif()
+    elseif(MeshTest_WRAPPER STREQUAL "mpirun")
+        if(MPIRUN_TOOL_PATH)
+            set(WRAPPER_COMMAND ${MPIRUN_TOOL_PATH})
+        else()
+            message(STATUS "ERROR: mpirun was not found but is required for ${MeshTest_NAME}!")
+            return()
+        endif()
+    endif()
+
+    # --- Implement testers ---
+    if(NOT MeshTest_DIFF_DATA)
+        message(FATAL_ERROR "MeshTest(): ${MeshTest_NAME} - no DIFF_DATA given!")
+    endif()
+
+    string(LENGTH "${MeshTest_DIFF_DATA}" DIFF_DATA_LENGTH)
+    if(${DIFF_DATA_LENGTH} GREATER 7500)
+        message(FATAL_ERROR "${MeshTest_NAME}: DIFF_DATA to long! Consider using regex-syntax: TODO")
+    endif()
+
+    set(SELECTED_DIFF_TOOL_PATH $<TARGET_FILE:vtkdiff>)
+
+    list(LENGTH MeshTest_DIFF_DATA DiffDataLength)
+    math(EXPR DiffDataLengthMod3 "${DiffDataLength} % 3")
+    if (${DiffDataLengthMod3} EQUAL 0)
+        math(EXPR DiffDataLastIndex "${DiffDataLength}-1")
+        foreach(DiffDataIndex RANGE 0 ${DiffDataLastIndex} 4)
+            list(GET MeshTest_DIFF_DATA "${DiffDataIndex}" REFERENCE_VTK_FILE)
+            math(EXPR DiffDataAuxIndex "${DiffDataIndex}+1")
+            list(GET MeshTest_DIFF_DATA "${DiffDataAuxIndex}" VTK_FILE)
+            math(EXPR DiffDataAuxIndex "${DiffDataIndex}+2")
+            list(GET MeshTest_DIFF_DATA "${DiffDataAuxIndex}" ABS_TOLERANCE)
+
+            list(APPEND TESTER_COMMAND "${SELECTED_DIFF_TOOL_PATH} -m \
+            ${MeshTest_SOURCE_PATH}/${REFERENCE_VTK_FILE} \
+            ${MeshTest_BINARY_PATH}/${VTK_FILE} \
+            --abs ${ABS_TOLERANCE}")
+        endforeach()
+    else ()
+        message(FATAL_ERROR "The number of diff data arguments must be a
+        multiple of three: expected.vtu output.vtu absolute_tolerance.")
+    endif()
+
+    ## -----------
+    if(TARGET ${MeshTest_EXECUTABLE})
+        set(MeshTest_EXECUTABLE_PARSED $<TARGET_FILE:${MeshTest_EXECUTABLE}>)
+    else()
+        set(MeshTest_EXECUTABLE_PARSED ${MeshTest_EXECUTABLE})
+    endif()
+
+    set(FILES_TO_DELETE "")
+    list(APPEND FILES_TO_DELETE "${MeshTest_STDOUT_FILE_PATH}")
+    foreach(ITEM ${MeshTest_DIFF_DATA})
+        if(ITEM MATCHES "^.*\.(vtu|vtk)$")
+            list(APPEND FILES_TO_DELETE "${ITEM}")
+        endif()
+    endforeach()
+
+    # Run the wrapper
+    if(DEFINED MeshTest_WRAPPER)
+        set(MeshTest_WRAPPER_STRING "-${MeshTest_WRAPPER}")
+    endif()
+    set(TEST_NAME "${MeshTest_EXECUTABLE}-${MeshTest_NAME}${MeshTest_WRAPPER_STRING}")
+    add_test(
+        NAME ${TEST_NAME}
+        COMMAND ${CMAKE_COMMAND}
+        -DEXECUTABLE=${MeshTest_EXECUTABLE_PARSED}
+        "-DEXECUTABLE_ARGS=${MeshTest_EXECUTABLE_ARGS}" # Quoted because passed as list
+        -Dcase_path=${MeshTest_SOURCE_PATH}             # see https://stackoverflow.com/a/33248574/80480
+        -DBINARY_PATH=${MeshTest_BINARY_PATH}
+        -DWRAPPER_COMMAND=${WRAPPER_COMMAND}
+        "-DWRAPPER_ARGS=${MeshTest_WRAPPER_ARGS}"
+        "-DFILES_TO_DELETE=${FILES_TO_DELETE}"
+        -DSTDOUT_FILE_PATH=${MeshTest_STDOUT_FILE_PATH}
+        -P ${PROJECT_SOURCE_DIR}/scripts/cmake/test/AddTestWrapper.cmake
+    )
+    set_tests_properties(${TEST_NAME} PROPERTIES COST ${MeshTest_RUNTIME})
+
+    if(TARGET ${MeshTest_EXECUTABLE})
+        add_dependencies(ctest ${MeshTest_EXECUTABLE})
+        add_dependencies(ctest-large ${MeshTest_EXECUTABLE})
+    endif()
+
+    # Run the tester
+    set(MeshTest_TESTER "vtkdiff")
+    set(TESTER_NAME "${TEST_NAME}-${MeshTest_TESTER}")
+    add_test(
+        NAME ${TESTER_NAME}
+        COMMAND ${CMAKE_COMMAND}
+        -Dcase_path=${MeshTest_SOURCE_PATH}
+        -DBINARY_PATH=${${MeshTest_BINARY_PATH}}
+        -DSELECTED_DIFF_TOOL_PATH=${SELECTED_DIFF_TOOL_PATH}
+        "-DTESTER_COMMAND=${TESTER_COMMAND}"
+        -DVTKJS_CONVERTER=${VTKJS_CONVERTER}
+        -DBINARY_PATH=${MeshTest_BINARY_PATH}
+        -DVTKJS_OUTPUT_PATH=${PROJECT_SOURCE_DIR}/web/static/vis/${MeshTest_PATH}
+        "-DVIS_FILES=${MeshTest_VIS}"
+        -DGLOB_MODE=${GLOB_MODE}
+        -P ${PROJECT_SOURCE_DIR}/scripts/cmake/test/AddTestTester.cmake
+        --debug-output
+        WORKING_DIRECTORY ${MeshTest_SOURCE_PATH}
+    )
+    set_tests_properties(${TESTER_NAME} PROPERTIES DEPENDS ${TEST_NAME})
+
+endfunction()
diff --git a/scripts/cmake/test/Test.cmake b/scripts/cmake/test/Test.cmake
index 6fe3986ddce11c1d5d0d24725ba03ccf11c29c0c..22ea870e81f83291460fa50dac486d557040c636 100644
--- a/scripts/cmake/test/Test.cmake
+++ b/scripts/cmake/test/Test.cmake
@@ -35,6 +35,7 @@ configure_file(
 )
 
 include(${CMAKE_CURRENT_SOURCE_DIR}/scripts/cmake/test/AddTest.cmake)
+include(${CMAKE_CURRENT_SOURCE_DIR}/scripts/cmake/test/MeshTest.cmake)
 include(${CMAKE_CURRENT_SOURCE_DIR}/scripts/cmake/test/OgsTest.cmake)
 
 if(DEFINED ENV{CTEST_NUM_THREADS})