diff --git a/src/scheme/FaceIntegrator.hpp b/src/scheme/FaceIntegrator.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..30ee39077ea12d898b4bdee00f12970be79e8753
--- /dev/null
+++ b/src/scheme/FaceIntegrator.hpp
@@ -0,0 +1,322 @@
+#ifndef FACE_INTEGRATOR_HPP
+#define FACE_INTEGRATOR_HPP
+
+#include <analysis/IQuadratureDescriptor.hpp>
+#include <analysis/QuadratureManager.hpp>
+#include <geometry/LineTransformation.hpp>
+#include <geometry/SquareTransformation.hpp>
+#include <geometry/TriangleTransformation.hpp>
+#include <mesh/Connectivity.hpp>
+#include <mesh/ItemId.hpp>
+#include <utils/Array.hpp>
+
+#include <functional>
+
+class FaceIntegrator
+{
+ private:
+  template <size_t Dimension>
+  class AllFaces
+  {
+   private:
+    const Connectivity<Dimension>& m_connectivity;
+
+   public:
+    using index_type = FaceId;
+
+    PUGS_INLINE
+    FaceId
+    faceId(const FaceId face_id) const
+    {
+      return face_id;
+    }
+
+    PUGS_INLINE
+    size_t
+    size() const
+    {
+      return m_connectivity.numberOfFaces();
+    }
+
+    PUGS_INLINE
+    AllFaces(const Connectivity<Dimension>& connectivity) : m_connectivity{connectivity} {}
+  };
+
+  template <template <typename T> typename ArrayT>
+  class FaceList
+  {
+   private:
+    const ArrayT<FaceId>& m_face_list;
+
+   public:
+    using index_type = typename ArrayT<FaceId>::index_type;
+
+    PUGS_INLINE
+    FaceId
+    faceId(const index_type index) const
+    {
+      return m_face_list[index];
+    }
+
+    PUGS_INLINE
+    size_t
+    size() const
+    {
+      return m_face_list.size();
+    }
+
+    PUGS_INLINE
+    FaceList(const ArrayT<FaceId>& face_list) : m_face_list{face_list} {}
+  };
+
+  template <typename MeshType, typename OutputArrayT, typename ListTypeT, typename OutputType, typename InputType>
+  static PUGS_INLINE void
+  _tensorialIntegrateTo(std::function<OutputType(InputType)> function,
+                        const IQuadratureDescriptor& quadrature_descriptor,
+                        const MeshType& mesh,
+                        const ListTypeT& face_list,
+                        OutputArrayT& value)
+  {
+    constexpr size_t Dimension = MeshType::Dimension;
+    static_assert(Dimension > 1);
+
+    static_assert(std::is_same_v<TinyVector<Dimension>, std::decay_t<InputType>>, "invalid input data type");
+    static_assert(std::is_same_v<std::remove_const_t<typename OutputArrayT::data_type>, OutputType>,
+                  "invalid output data type");
+
+    using execution_space = typename Kokkos::DefaultExecutionSpace::execution_space;
+    Kokkos::Experimental::UniqueToken<execution_space, Kokkos::Experimental::UniqueTokenScope::Global> tokens;
+
+    if constexpr (std::is_arithmetic_v<OutputType>) {
+      value.fill(0);
+    } else if constexpr (is_tiny_vector_v<OutputType> or is_tiny_matrix_v<OutputType>) {
+      value.fill(zero);
+    } else {
+      static_assert(std::is_same_v<OutputType, double>, "unexpected output type");
+    }
+
+    const auto& connectivity = mesh.connectivity();
+
+    const auto face_to_node_matrix = connectivity.faceToNodeMatrix();
+    const auto xr                  = mesh.xr();
+
+    if constexpr (Dimension == 2) {
+      const auto qf = QuadratureManager::instance().getLineFormula(quadrature_descriptor);
+
+      parallel_for(face_list.size(), [=, &qf, &face_list](typename ListTypeT::index_type index) {
+        const FaceId face_id = face_list.faceId(index);
+
+        const auto face_to_node = face_to_node_matrix[face_id];
+
+        Assert(face_to_node.size() == 2);
+        const LineTransformation<2> T(xr[face_to_node[0]], xr[face_to_node[1]]);
+
+        for (size_t i_point = 0; i_point < qf.numberOfPoints(); ++i_point) {
+          const auto xi = qf.point(i_point);
+          value[index] += qf.weight(i_point) * T.velocityNorm() * function(T(xi));
+        }
+      });
+
+    } else if constexpr (Dimension == 3) {
+      const auto qf = QuadratureManager::instance().getSquareFormula(quadrature_descriptor);
+
+      auto invalid_face = std::make_pair(false, FaceId{});
+
+      parallel_for(face_list.size(), [=, &invalid_face, &qf, &face_list](typename ListTypeT::index_type index) {
+        const FaceId face_id = face_list.faceId(index);
+
+        const auto face_to_node = face_to_node_matrix[face_id];
+
+        switch (face_to_node.size()) {
+        case 3: {
+          SquareTransformation<3> T(xr[face_to_node[0]], xr[face_to_node[1]], xr[face_to_node[2]], xr[face_to_node[2]]);
+          for (size_t i_point = 0; i_point < qf.numberOfPoints(); ++i_point) {
+            const auto xi = qf.point(i_point);
+            value[index] += qf.weight(i_point) * T.areaVariationNorm(xi) * function(T(xi));
+          }
+          break;
+        }
+        case 4: {
+          SquareTransformation<3> T(xr[face_to_node[0]], xr[face_to_node[1]], xr[face_to_node[2]], xr[face_to_node[3]]);
+          for (size_t i_point = 0; i_point < qf.numberOfPoints(); ++i_point) {
+            const auto xi = qf.point(i_point);
+            value[index] += qf.weight(i_point) * T.areaVariationNorm(xi) * function(T(xi));
+          }
+          break;
+        }
+        default: {
+          invalid_face = std::make_pair(true, face_id);
+        }
+        }
+      });
+
+      // LCOV_EXCL_START
+      if (invalid_face.first) {
+        std::ostringstream os;
+        os << "invalid face type for integration: " << face_to_node_matrix[invalid_face.second].size() << " points";
+        throw UnexpectedError(os.str());
+      }
+      // LCOV_EXCL_STOP
+
+    } else {
+      throw UnexpectedError("invalid dimension for face integration");
+    }
+  }
+
+  template <typename MeshType, typename OutputArrayT, typename ListTypeT, typename OutputType, typename InputType>
+  static PUGS_INLINE void
+  _directIntegrateTo(std::function<OutputType(InputType)> function,
+                     const IQuadratureDescriptor& quadrature_descriptor,
+                     const MeshType& mesh,
+                     const ListTypeT& face_list,
+                     OutputArrayT& value)
+  {
+    Assert(not quadrature_descriptor.isTensorial());
+
+    constexpr size_t Dimension = MeshType::Dimension;
+    static_assert(Dimension > 1);
+
+    static_assert(std::is_same_v<TinyVector<Dimension>, std::decay_t<InputType>>, "invalid input data type");
+    static_assert(std::is_same_v<std::remove_const_t<typename OutputArrayT::data_type>, OutputType>,
+                  "invalid output data type");
+
+    if constexpr (std::is_arithmetic_v<OutputType>) {
+      value.fill(0);
+    } else if constexpr (is_tiny_vector_v<OutputType> or is_tiny_matrix_v<OutputType>) {
+      value.fill(zero);
+    } else {
+      static_assert(std::is_same_v<OutputType, double>, "unexpected output type");
+    }
+
+    const auto& connectivity = mesh.connectivity();
+
+    const auto face_to_node_matrix = connectivity.faceToNodeMatrix();
+    const auto xr                  = mesh.xr();
+
+    if constexpr (Dimension == 2) {
+      parallel_for(face_list.size(),
+                   [=, &quadrature_descriptor, &face_list](const typename ListTypeT::index_type& index) {
+                     const FaceId face_id = face_list.faceId(index);
+
+                     const auto face_to_node = face_to_node_matrix[face_id];
+
+                     Assert(face_to_node.size() == 2);
+                     const LineTransformation<2> T(xr[face_to_node[0]], xr[face_to_node[1]]);
+                     const auto qf = QuadratureManager::instance().getLineFormula(quadrature_descriptor);
+
+                     for (size_t i_point = 0; i_point < qf.numberOfPoints(); ++i_point) {
+                       const auto xi = qf.point(i_point);
+                       value[index] += qf.weight(i_point) * T.velocityNorm() * function(T(xi));
+                     }
+                   });
+
+    } else if constexpr (Dimension == 3) {
+      auto invalid_face = std::make_pair(false, FaceId{});
+
+      parallel_for(face_list.size(),
+                   [=, &invalid_face, &quadrature_descriptor, &face_list](const typename ListTypeT::index_type& index) {
+                     const FaceId face_id = face_list.faceId(index);
+
+                     const auto face_to_node = face_to_node_matrix[face_id];
+
+                     switch (face_to_node.size()) {
+                     case 3: {
+                       const TriangleTransformation<3> T(xr[face_to_node[0]], xr[face_to_node[1]], xr[face_to_node[2]]);
+                       const auto qf = QuadratureManager::instance().getTriangleFormula(quadrature_descriptor);
+
+                       for (size_t i_point = 0; i_point < qf.numberOfPoints(); ++i_point) {
+                         const auto xi = qf.point(i_point);
+                         value[index] += qf.weight(i_point) * T.areaVariationNorm() * function(T(xi));
+                       }
+                       break;
+                     }
+                     case 4: {
+                       const SquareTransformation<3> T(xr[face_to_node[0]], xr[face_to_node[1]], xr[face_to_node[2]],
+                                                       xr[face_to_node[3]]);
+                       const auto qf = QuadratureManager::instance().getSquareFormula(quadrature_descriptor);
+
+                       for (size_t i_point = 0; i_point < qf.numberOfPoints(); ++i_point) {
+                         const auto xi = qf.point(i_point);
+                         value[index] += qf.weight(i_point) * T.areaVariationNorm(xi) * function(T(xi));
+                       }
+                       break;
+                     }
+                       // LCOV_EXCL_START
+                     default: {
+                       invalid_face = std::make_pair(true, face_id);
+                       break;
+                     }
+                       // LCOV_EXCL_STOP
+                     }
+                   });
+
+      // LCOV_EXCL_START
+      if (invalid_face.first) {
+        std::ostringstream os;
+        os << "invalid face type for integration: " << face_to_node_matrix[invalid_face.second].size() << " points";
+        throw UnexpectedError(os.str());
+      }
+      // LCOV_EXCL_STOP
+    }
+  }
+
+ public:
+  template <typename MeshType, typename OutputArrayT, typename OutputType, typename InputType>
+  static PUGS_INLINE void
+  integrateTo(const std::function<OutputType(InputType)>& function,
+              const IQuadratureDescriptor& quadrature_descriptor,
+              const MeshType& mesh,
+              OutputArrayT& value)
+  {
+    constexpr size_t Dimension = MeshType::Dimension;
+
+    if (quadrature_descriptor.isTensorial()) {
+      _tensorialIntegrateTo<MeshType, OutputArrayT>(function, quadrature_descriptor, mesh,
+                                                    AllFaces<Dimension>{mesh.connectivity()}, value);
+    } else {
+      _directIntegrateTo<MeshType, OutputArrayT>(function, quadrature_descriptor, mesh,
+                                                 AllFaces<Dimension>{mesh.connectivity()}, value);
+    }
+  }
+
+  template <typename MeshType, typename OutputArrayT, typename FunctionType>
+  static PUGS_INLINE void
+  integrateTo(const FunctionType& function,
+              const IQuadratureDescriptor& quadrature_descriptor,
+              const MeshType& mesh,
+              OutputArrayT& value)
+  {
+    integrateTo(std::function(function), quadrature_descriptor, mesh, value);
+  }
+
+  template <typename MeshType, typename OutputType, typename InputType, template <typename DataType> typename ArrayT>
+  static PUGS_INLINE ArrayT<OutputType>
+  integrate(const std::function<OutputType(InputType)>& function,
+            const IQuadratureDescriptor& quadrature_descriptor,
+            const MeshType& mesh,
+            const ArrayT<FaceId>& face_list)
+  {
+    ArrayT<OutputType> value(size(face_list));
+    if (quadrature_descriptor.isTensorial()) {
+      _tensorialIntegrateTo<MeshType, ArrayT<OutputType>>(function, quadrature_descriptor, mesh, FaceList{face_list},
+                                                          value);
+    } else {
+      _directIntegrateTo<MeshType, ArrayT<OutputType>>(function, quadrature_descriptor, mesh, FaceList{face_list},
+                                                       value);
+    }
+
+    return value;
+  }
+
+  template <typename MeshType, typename FunctionType, template <typename DataType> typename ArrayT>
+  static PUGS_INLINE auto
+  integrate(const FunctionType& function,
+            const IQuadratureDescriptor& quadrature_descriptor,
+            const MeshType& mesh,
+            const ArrayT<FaceId>& face_list)
+  {
+    return integrate(std::function(function), quadrature_descriptor, mesh, face_list);
+  }
+};
+
+#endif   // FACE_INTEGRATOR_HPP
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 8d7595e7ca2e9809829f9636e6d52de4c343910b..b10741748ad167b6d0f806ece0571cdbdb00eeab 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -76,6 +76,7 @@ add_executable (unit_tests
   test_EscapedString.cpp
   test_Exceptions.cpp
   test_ExecutionPolicy.cpp
+  test_FaceIntegrator.cpp
   test_FakeProcessor.cpp
   test_ForProcessor.cpp
   test_FunctionArgumentConverter.cpp
diff --git a/tests/test_FaceIntegrator.cpp b/tests/test_FaceIntegrator.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..9dee9c032b48db4ad51c362b5f47681613039fb5
--- /dev/null
+++ b/tests/test_FaceIntegrator.cpp
@@ -0,0 +1,506 @@
+#include <catch2/catch_approx.hpp>
+#include <catch2/catch_test_macros.hpp>
+
+#include <analysis/GaussLegendreQuadratureDescriptor.hpp>
+#include <analysis/GaussLobattoQuadratureDescriptor.hpp>
+#include <analysis/GaussQuadratureDescriptor.hpp>
+#include <mesh/DiamondDualMeshManager.hpp>
+#include <mesh/ItemValue.hpp>
+#include <mesh/Mesh.hpp>
+#include <scheme/FaceIntegrator.hpp>
+
+#include <MeshDataBaseForTests.hpp>
+
+// clazy:excludeall=non-pod-global-static
+
+TEST_CASE("FaceIntegrator", "[scheme]")
+{
+  SECTION("2D")
+  {
+    using R2 = TinyVector<2>;
+
+    const auto mesh = MeshDataBaseForTests::get().hybrid2DMesh();
+
+    auto f = [](const R2& X) -> double {
+      const double x = X[0];
+      const double y = X[1];
+      return x * x + 2 * x * y + 3 * y * y + 2;
+    };
+
+    Array<const double> int_f_per_face = [=] {
+      Array<double> int_f(mesh->numberOfFaces());
+      auto face_to_node_matrix = mesh->connectivity().faceToNodeMatrix();
+
+      parallel_for(
+        mesh->numberOfFaces(), PUGS_LAMBDA(const FaceId face_id) {
+          auto face_node_list = face_to_node_matrix[face_id];
+          auto xr             = mesh->xr();
+          double integral     = 0;
+          LineTransformation<2> T(xr[face_node_list[0]], xr[face_node_list[1]]);
+          auto qf = QuadratureManager::instance().getLineFormula(GaussQuadratureDescriptor(2));
+
+          for (size_t i = 0; i < qf.numberOfPoints(); ++i) {
+            const auto& xi = qf.point(i);
+            integral += qf.weight(i) * T.velocityNorm() * f(T(xi));
+          }
+
+          int_f[face_id] = integral;
+        });
+
+      return int_f;
+    }();
+
+    SECTION("direct formula")
+    {
+      SECTION("all faces")
+      {
+        SECTION("FaceValue")
+        {
+          FaceValue<double> values(mesh->connectivity());
+          FaceIntegrator::integrateTo([=](const R2 x) { return f(x); }, GaussQuadratureDescriptor(2), *mesh, values);
+
+          double error = 0;
+          for (FaceId face_id = 0; face_id < mesh->numberOfFaces(); ++face_id) {
+            error += std::abs(int_f_per_face[face_id] - values[face_id]);
+          }
+
+          REQUIRE(error == Catch::Approx(0).margin(1E-10));
+        }
+
+        SECTION("Array")
+        {
+          Array<double> values(mesh->numberOfFaces());
+
+          FaceIntegrator::integrateTo(f, GaussQuadratureDescriptor(2), *mesh, values);
+
+          double error = 0;
+          for (FaceId face_id = 0; face_id < mesh->numberOfFaces(); ++face_id) {
+            error += std::abs(int_f_per_face[face_id] - values[face_id]);
+          }
+
+          REQUIRE(error == Catch::Approx(0).margin(1E-10));
+        }
+
+        SECTION("SmallArray")
+        {
+          SmallArray<double> values(mesh->numberOfFaces());
+          FaceIntegrator::integrateTo(f, GaussQuadratureDescriptor(2), *mesh, values);
+
+          double error = 0;
+          for (FaceId face_id = 0; face_id < mesh->numberOfFaces(); ++face_id) {
+            error += std::abs(int_f_per_face[face_id] - values[face_id]);
+          }
+
+          REQUIRE(error == Catch::Approx(0).margin(1E-10));
+        }
+      }
+
+      SECTION("face list")
+      {
+        SECTION("Array")
+        {
+          Array<FaceId> face_list{mesh->numberOfFaces() / 2 + mesh->numberOfFaces() % 2};
+
+          {
+            size_t k = 0;
+            for (FaceId face_id = 0; face_id < mesh->numberOfFaces(); ++(++face_id), ++k) {
+              face_list[k] = face_id;
+            }
+
+            REQUIRE(k == face_list.size());
+          }
+
+          Array<double> values = FaceIntegrator::integrate(f, GaussQuadratureDescriptor(2), *mesh, face_list);
+
+          double error = 0;
+          for (size_t i = 0; i < face_list.size(); ++i) {
+            error += std::abs(int_f_per_face[face_list[i]] - values[i]);
+          }
+
+          REQUIRE(error == Catch::Approx(0).margin(1E-10));
+        }
+
+        SECTION("SmallArray")
+        {
+          SmallArray<FaceId> face_list{mesh->numberOfFaces() / 2 + mesh->numberOfFaces() % 2};
+
+          {
+            size_t k = 0;
+            for (FaceId face_id = 0; face_id < mesh->numberOfFaces(); ++(++face_id), ++k) {
+              face_list[k] = face_id;
+            }
+
+            REQUIRE(k == face_list.size());
+          }
+
+          SmallArray<double> values = FaceIntegrator::integrate(f, GaussQuadratureDescriptor(2), *mesh, face_list);
+
+          double error = 0;
+          for (size_t i = 0; i < face_list.size(); ++i) {
+            error += std::abs(int_f_per_face[face_list[i]] - values[i]);
+          }
+
+          REQUIRE(error == Catch::Approx(0).margin(1E-10));
+        }
+      }
+    }
+
+    SECTION("tensorial formula")
+    {
+      SECTION("all faces")
+      {
+        SECTION("FaceValue")
+        {
+          FaceValue<double> values(mesh->connectivity());
+          FaceIntegrator::integrateTo([=](const R2 x) { return f(x); }, GaussLobattoQuadratureDescriptor(2), *mesh,
+                                      values);
+
+          double error = 0;
+          for (FaceId face_id = 0; face_id < mesh->numberOfFaces(); ++face_id) {
+            error += std::abs(int_f_per_face[face_id] - values[face_id]);
+          }
+
+          REQUIRE(error == Catch::Approx(0).margin(1E-10));
+        }
+
+        SECTION("Array")
+        {
+          Array<double> values(mesh->numberOfFaces());
+
+          FaceIntegrator::integrateTo(f, GaussLobattoQuadratureDescriptor(2), *mesh, values);
+
+          double error = 0;
+          for (FaceId face_id = 0; face_id < mesh->numberOfFaces(); ++face_id) {
+            error += std::abs(int_f_per_face[face_id] - values[face_id]);
+          }
+
+          REQUIRE(error == Catch::Approx(0).margin(1E-10));
+        }
+
+        SECTION("SmallArray")
+        {
+          SmallArray<double> values(mesh->numberOfFaces());
+          FaceIntegrator::integrateTo(f, GaussLobattoQuadratureDescriptor(2), *mesh, values);
+
+          double error = 0;
+          for (FaceId face_id = 0; face_id < mesh->numberOfFaces(); ++face_id) {
+            error += std::abs(int_f_per_face[face_id] - values[face_id]);
+          }
+
+          REQUIRE(error == Catch::Approx(0).margin(1E-10));
+        }
+      }
+
+      SECTION("face list")
+      {
+        SECTION("Array")
+        {
+          Array<FaceId> face_list{mesh->numberOfFaces() / 2 + mesh->numberOfFaces() % 2};
+
+          {
+            size_t k = 0;
+            for (FaceId face_id = 0; face_id < mesh->numberOfFaces(); ++(++face_id), ++k) {
+              face_list[k] = face_id;
+            }
+
+            REQUIRE(k == face_list.size());
+          }
+
+          Array<double> values = FaceIntegrator::integrate(f, GaussLobattoQuadratureDescriptor(2), *mesh, face_list);
+
+          double error = 0;
+          for (size_t i = 0; i < face_list.size(); ++i) {
+            error += std::abs(int_f_per_face[face_list[i]] - values[i]);
+          }
+
+          REQUIRE(error == Catch::Approx(0).margin(1E-10));
+        }
+
+        SECTION("SmallArray")
+        {
+          SmallArray<FaceId> face_list{mesh->numberOfFaces() / 2 + mesh->numberOfFaces() % 2};
+
+          {
+            size_t k = 0;
+            for (FaceId face_id = 0; face_id < mesh->numberOfFaces(); ++(++face_id), ++k) {
+              face_list[k] = face_id;
+            }
+
+            REQUIRE(k == face_list.size());
+          }
+
+          SmallArray<double> values =
+            FaceIntegrator::integrate(f, GaussLobattoQuadratureDescriptor(2), *mesh, face_list);
+
+          double error = 0;
+          for (size_t i = 0; i < face_list.size(); ++i) {
+            error += std::abs(int_f_per_face[face_list[i]] - values[i]);
+          }
+
+          REQUIRE(error == Catch::Approx(0).margin(1E-10));
+        }
+      }
+    }
+  }
+
+  SECTION("3D")
+  {
+    using R3 = TinyVector<3>;
+
+    auto hybrid_mesh = MeshDataBaseForTests::get().hybrid3DMesh();
+
+    auto f = [](const R3& X) -> double {
+      const double x = X[0];
+      const double y = X[1];
+      const double z = X[2];
+      return x * x + 2 * x * y + 3 * y * y + 2 * z * z - z + 1;
+    };
+
+    std::vector<std::pair<std::string, decltype(hybrid_mesh)>> mesh_list;
+    mesh_list.push_back(std::make_pair("hybrid mesh", hybrid_mesh));
+    mesh_list.push_back(
+      std::make_pair("diamond mesh", DiamondDualMeshManager::instance().getDiamondDualMesh(hybrid_mesh)));
+
+    for (auto mesh_info : mesh_list) {
+      auto mesh_name = mesh_info.first;
+      auto mesh      = mesh_info.second;
+
+      Array<const double> int_f_per_face = [=] {
+        Array<double> int_f(mesh->numberOfFaces());
+        auto face_to_node_matrix = mesh->connectivity().faceToNodeMatrix();
+
+        parallel_for(
+          mesh->numberOfFaces(), PUGS_LAMBDA(const FaceId face_id) {
+            auto face_node_list = face_to_node_matrix[face_id];
+            auto xr             = mesh->xr();
+            double integral     = 0;
+
+            switch (face_node_list.size()) {
+            case 3: {
+              TriangleTransformation<3> T(xr[face_node_list[0]], xr[face_node_list[1]], xr[face_node_list[2]]);
+              auto qf = QuadratureManager::instance().getTriangleFormula(GaussQuadratureDescriptor(2));
+              for (size_t i = 0; i < qf.numberOfPoints(); ++i) {
+                const auto& xi = qf.point(i);
+                integral += qf.weight(i) * T.areaVariationNorm() * f(T(xi));
+              }
+              break;
+            }
+            case 4: {
+              SquareTransformation<3> T(xr[face_node_list[0]], xr[face_node_list[1]], xr[face_node_list[2]],
+                                        xr[face_node_list[3]]);
+              auto qf = QuadratureManager::instance().getSquareFormula(GaussQuadratureDescriptor(2));
+              for (size_t i = 0; i < qf.numberOfPoints(); ++i) {
+                const auto& xi = qf.point(i);
+                integral += qf.weight(i) * T.areaVariationNorm(xi) * f(T(xi));
+              }
+              break;
+            }
+            default: {
+              throw UnexpectedError("invalid face (node number must be 3 or 4)");
+            }
+            }
+            int_f[face_id] = integral;
+          });
+
+        return int_f;
+      }();
+
+      SECTION(mesh_name)
+      {
+        SECTION("direct formula")
+        {
+          SECTION("all faces")
+          {
+            SECTION("FaceValue")
+            {
+              FaceValue<double> values(mesh->connectivity());
+              FaceIntegrator::integrateTo([=](const R3 x) { return f(x); }, GaussQuadratureDescriptor(4), *mesh,
+                                          values);
+
+              double error = 0;
+              for (FaceId face_id = 0; face_id < mesh->numberOfFaces(); ++face_id) {
+                error += std::abs(int_f_per_face[face_id] - values[face_id]);
+              }
+
+              REQUIRE(error == Catch::Approx(0).margin(1E-10));
+            }
+
+            SECTION("Array")
+            {
+              Array<double> values(mesh->numberOfFaces());
+
+              FaceIntegrator::integrateTo(f, GaussQuadratureDescriptor(4), *mesh, values);
+
+              double error = 0;
+              for (FaceId face_id = 0; face_id < mesh->numberOfFaces(); ++face_id) {
+                error += std::abs(int_f_per_face[face_id] - values[face_id]);
+              }
+
+              REQUIRE(error == Catch::Approx(0).margin(1E-10));
+            }
+
+            SECTION("SmallArray")
+            {
+              SmallArray<double> values(mesh->numberOfFaces());
+              FaceIntegrator::integrateTo(f, GaussQuadratureDescriptor(4), *mesh, values);
+
+              double error = 0;
+              for (FaceId face_id = 0; face_id < mesh->numberOfFaces(); ++face_id) {
+                error += std::abs(int_f_per_face[face_id] - values[face_id]);
+              }
+
+              REQUIRE(error == Catch::Approx(0).margin(1E-10));
+            }
+          }
+
+          SECTION("face list")
+          {
+            SECTION("Array")
+            {
+              Array<FaceId> face_list{mesh->numberOfFaces() / 2 + mesh->numberOfFaces() % 2};
+
+              {
+                size_t k = 0;
+                for (FaceId face_id = 0; face_id < mesh->numberOfFaces(); ++(++face_id), ++k) {
+                  face_list[k] = face_id;
+                }
+
+                REQUIRE(k == face_list.size());
+              }
+
+              Array<double> values = FaceIntegrator::integrate(f, GaussQuadratureDescriptor(4), *mesh, face_list);
+
+              double error = 0;
+              for (size_t i = 0; i < face_list.size(); ++i) {
+                error += std::abs(int_f_per_face[face_list[i]] - values[i]);
+              }
+
+              REQUIRE(error == Catch::Approx(0).margin(1E-10));
+            }
+
+            SECTION("SmallArray")
+            {
+              SmallArray<FaceId> face_list{mesh->numberOfFaces() / 2 + mesh->numberOfFaces() % 2};
+
+              {
+                size_t k = 0;
+                for (FaceId face_id = 0; face_id < mesh->numberOfFaces(); ++(++face_id), ++k) {
+                  face_list[k] = face_id;
+                }
+
+                REQUIRE(k == face_list.size());
+              }
+
+              SmallArray<double> values = FaceIntegrator::integrate(f, GaussQuadratureDescriptor(4), *mesh, face_list);
+
+              double error = 0;
+              for (size_t i = 0; i < face_list.size(); ++i) {
+                error += std::abs(int_f_per_face[face_list[i]] - values[i]);
+              }
+
+              REQUIRE(error == Catch::Approx(0).margin(1E-10));
+            }
+          }
+        }
+
+        SECTION("tensorial formula")
+        {
+          SECTION("all faces")
+          {
+            SECTION("FaceValue")
+            {
+              FaceValue<double> values(mesh->connectivity());
+              FaceIntegrator::integrateTo([=](const R3 x) { return f(x); }, GaussLegendreQuadratureDescriptor(10),
+                                          *mesh, values);
+
+              double error = 0;
+              for (FaceId face_id = 0; face_id < mesh->numberOfFaces(); ++face_id) {
+                error += std::abs(int_f_per_face[face_id] - values[face_id]);
+              }
+
+              REQUIRE(error == Catch::Approx(0).margin(1E-10));
+            }
+
+            SECTION("Array")
+            {
+              Array<double> values(mesh->numberOfFaces());
+
+              FaceIntegrator::integrateTo(f, GaussLobattoQuadratureDescriptor(4), *mesh, values);
+
+              double error = 0;
+              for (FaceId face_id = 0; face_id < mesh->numberOfFaces(); ++face_id) {
+                error += std::abs(int_f_per_face[face_id] - values[face_id]);
+              }
+
+              REQUIRE(error == Catch::Approx(0).margin(1E-10));
+            }
+
+            SECTION("SmallArray")
+            {
+              SmallArray<double> values(mesh->numberOfFaces());
+              FaceIntegrator::integrateTo(f, GaussLobattoQuadratureDescriptor(4), *mesh, values);
+
+              double error = 0;
+              for (FaceId face_id = 0; face_id < mesh->numberOfFaces(); ++face_id) {
+                error += std::abs(int_f_per_face[face_id] - values[face_id]);
+              }
+
+              REQUIRE(error == Catch::Approx(0).margin(1E-10));
+            }
+          }
+
+          SECTION("face list")
+          {
+            SECTION("Array")
+            {
+              Array<FaceId> face_list{mesh->numberOfFaces() / 2 + mesh->numberOfFaces() % 2};
+
+              {
+                size_t k = 0;
+                for (FaceId face_id = 0; face_id < mesh->numberOfFaces(); ++(++face_id), ++k) {
+                  face_list[k] = face_id;
+                }
+
+                REQUIRE(k == face_list.size());
+              }
+
+              Array<double> values =
+                FaceIntegrator::integrate(f, GaussLobattoQuadratureDescriptor(4), *mesh, face_list);
+
+              double error = 0;
+              for (size_t i = 0; i < face_list.size(); ++i) {
+                error += std::abs(int_f_per_face[face_list[i]] - values[i]);
+              }
+
+              REQUIRE(error == Catch::Approx(0).margin(1E-10));
+            }
+
+            SECTION("SmallArray")
+            {
+              SmallArray<FaceId> face_list{mesh->numberOfFaces() / 2 + mesh->numberOfFaces() % 2};
+
+              {
+                size_t k = 0;
+                for (FaceId face_id = 0; face_id < mesh->numberOfFaces(); ++(++face_id), ++k) {
+                  face_list[k] = face_id;
+                }
+
+                REQUIRE(k == face_list.size());
+              }
+
+              SmallArray<double> values =
+                FaceIntegrator::integrate(f, GaussLobattoQuadratureDescriptor(4), *mesh, face_list);
+
+              double error = 0;
+              for (size_t i = 0; i < face_list.size(); ++i) {
+                error += std::abs(int_f_per_face[face_list[i]] - values[i]);
+              }
+
+              REQUIRE(error == Catch::Approx(0).margin(1E-10));
+            }
+          }
+        }
+      }
+    }
+  }
+}