diff --git a/MathLib/Integration/GaussLegendreTet.cpp b/MathLib/Integration/GaussLegendreTet.cpp
index b2a7fdeaccd43928c840f52cccfbf5d0b3507b42..1faa686b15a83eed368b71e2ee6ebfb0563ece6c 100644
--- a/MathLib/Integration/GaussLegendreTet.cpp
+++ b/MathLib/Integration/GaussLegendreTet.cpp
@@ -11,10 +11,11 @@
 
 namespace MathLib
 {
-
-template <> const std::array<std::array<double, 3>, GaussLegendreTet<1>::NPoints>
-GaussLegendreTet<1>::X = {{{{1./3., 1./3., 1./3.}}}};
-template <> double const GaussLegendreTet<1>::W[1] = {1.0};
+template <>
+const std::array<std::array<double, 3>, GaussLegendreTet<1>::NPoints>
+    GaussLegendreTet<1>::X = {{{{1. / 4., 1. / 4., 1. / 4.}}}};
+template <>
+double const GaussLegendreTet<1>::W[1] = {1. / 6.};
 
 const std::array<std::array<double, 3>, GaussLegendreTet<2>::NPoints>
 GaussLegendreTet<2>::X = {{ {{1./4., 1./4., 1./4.}},
@@ -24,37 +25,41 @@ GaussLegendreTet<2>::X = {{ {{1./4., 1./4., 1./4.}},
                             {{1./6., 1./6., 1./2.}} }};
 double const GaussLegendreTet<2>::W[5] = {-2./15., 0.075, 0.075, 0.075, 0.075};
 
+std::array<std::array<double, 3>, GaussLegendreTet<3>::NPoints> initGLTet3X()
+{
+    // Cf. Gellert, M., Harbord, R., 1991. Moderate degree cubature formulas for
+    // 3-D tetrahedral finite-element approximations. Communications in Applied
+    // Numerical Methods 7, 487–495. doi:10.1002/cnm.1630070609
+    const double a = 0.0673422422100983;
+    const double b = 0.3108859192633005;
+    const double c = 0.7217942490673264;
+    const double d = 0.0927352503108912;
+    const double e = 0.4544962958743506;
+    const double f = 0.045503704125649;
+
+    return {{{a, b, b},
+             {b, a, b},
+             {b, b, a},
+             {b, b, b},
+             {c, d, d},
+             {d, c, d},
+             {d, d, c},
+             {d, d, d},
+             {e, e, f},
+             {e, f, e},
+             {e, f, f},
+             {f, e, e},
+             {f, e, f},
+             {f, f, e}}};
+}
+
 const std::array<std::array<double, 3>, GaussLegendreTet<3>::NPoints>
-GaussLegendreTet<3>::X = {{ {{1./2., 1./2., 1./2.}},
-                            {{0.09197107805272303, 0.09197107805272303, 0.09197107805272303}},
-                            {{0.72408676584183096, 0.09197107805272303, 0.09197107805272303}},
-                            {{0.09197107805272303, 0.72408676584183096, 0.09197107805272303}},
-                            {{0.09197107805272303, 0.09197107805272303, 0.72408676584183096}},
-                            {{0.44364916731037080, 0.05635083268962915, 0.05635083268962915}},
-                            {{0.05635083268962915, 0.44364916731037080, 0.05635083268962915}},
-                            {{0.05635083268962915, 0.05635083268962915, 0.44364916731037080}},
-                            {{0.05635083268962915, 0.44364916731037080, 0.44364916731037080}},
-                            {{0.44364916731037080, 0.05635083268962915, 0.44364916731037080}},
-                            {{0.44364916731037080, 0.44364916731037080, 0.05635083268962915}},
-                            {{0.31979362782962989, 0.31979362782962989, 0.31979362782962989}},
-                            {{0.04061911651111023, 0.31979362782962989, 0.31979362782962989}},
-                            {{0.31979362782962989, 0.04061911651111023, 0.31979362782962989}},
-                            {{0.31979362782962989, 0.31979362782962989, 0.04061911651111023}} }};
-double const GaussLegendreTet<3>::W[15] = {0.019753086419753086,
-                                           0.011989513963169772,
-                                           0.011989513963169772,
-                                           0.011989513963169772,
-                                           0.011989513963169772,
-                                           0.008818342151675485,
-                                           0.008818342151675485,
-                                           0.008818342151675485,
-                                           0.008818342151675485,
-                                           0.008818342151675485,
-                                           0.008818342151675485,
-                                           0.011511367871045397,
-                                           0.011511367871045397,
-                                           0.011511367871045397,
-                                           0.011511367871045397
-                                           };
+    GaussLegendreTet<3>::X = initGLTet3X();
+
+static const double p = 0.1126879257180162 / 6.;
+static const double q = 0.0734930431163619 / 6.;
+static const double r = 0.0425460207770812 / 6.;
 
+double const GaussLegendreTet<3>::W[GaussLegendreTet<3>::NPoints] = {
+    p, p, p, p, q, q, q, q, r, r, r, r, r, r};
 }
diff --git a/MathLib/Integration/GaussLegendreTet.h b/MathLib/Integration/GaussLegendreTet.h
index 4a6ac7e6c039bc04e4496ace6c46b0e2b142aac2..110635849f5bfdc34aedc4d5e56c83c374310272 100644
--- a/MathLib/Integration/GaussLegendreTet.h
+++ b/MathLib/Integration/GaussLegendreTet.h
@@ -38,7 +38,7 @@ struct GaussLegendreTet<2> {
 template <>
 struct GaussLegendreTet<3> {
     static MATHLIB_EXPORT const unsigned Order = 3;
-    static MATHLIB_EXPORT const unsigned NPoints = 15;
+    static MATHLIB_EXPORT const unsigned NPoints = 14;
     static MATHLIB_EXPORT const std::array<std::array<double, 3>, NPoints> X;
     static MATHLIB_EXPORT const double W[NPoints];
 };
diff --git a/Tests/Data b/Tests/Data
index 611c3253b6254fe1ad1e53cb72b8b124ff11c16e..bb88a0209350e88e857f8e95b8845ab82ddd78c0 160000
--- a/Tests/Data
+++ b/Tests/Data
@@ -1 +1 @@
-Subproject commit 611c3253b6254fe1ad1e53cb72b8b124ff11c16e
+Subproject commit bb88a0209350e88e857f8e95b8845ab82ddd78c0
diff --git a/Tests/MathLib/TestGaussLegendreIntegration.cpp b/Tests/MathLib/TestGaussLegendreIntegration.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..abab841932bb84609bacb7937b91312856ea0506
--- /dev/null
+++ b/Tests/MathLib/TestGaussLegendreIntegration.cpp
@@ -0,0 +1,613 @@
+/**
+ * \copyright
+ * Copyright (c) 2012-2017, OpenGeoSys Community (http://www.opengeosys.org)
+ *            Distributed under a Modified BSD License.
+ *              See accompanying file LICENSE.txt or
+ *              http://www.opengeosys.org/LICENSE.txt
+ */
+
+#include <gtest/gtest.h>
+
+#include <cmath>
+#include <limits>
+
+#include "BaseLib/BuildInfo.h"
+
+#include "MathLib/Integration/GaussLegendreTet.h"
+
+#include "MeshLib/Elements/Element.h"
+#include "MeshLib/IO/VtkIO/VtuInterface.h"
+#include "MeshLib/MeshSubsets.h"
+
+#include "NumLib/DOF/LocalToGlobalIndexMap.h"
+#include "NumLib/Fem/ShapeMatrixPolicy.h"
+
+#include "ProcessLib/Utils/CreateLocalAssemblers.h"
+#include "ProcessLib/Utils/InitShapeMatrices.h"
+
+#include "Tests/VectorUtils.h"
+
+namespace GaussLegendreTest
+{
+template <typename Shp>
+static std::array<double, 3> interpolateNodeCoordinates(
+    MeshLib::Element const& e, Shp const& N)
+{
+    std::array<double, 3> res;
+
+    auto const* const nodes = e.getNodes();
+    auto node_coords = N;
+
+    for (std::size_t d = 0; d < 3; ++d)
+    {
+        for (unsigned ip = 0; ip < N.size(); ++ip)
+        {
+            node_coords[ip] = (*nodes[ip])[d];
+        }
+
+        res[d] = N.dot(node_coords);
+    }
+
+    return res;
+}
+
+class LocalAssemblerDataInterface
+{
+public:
+    using Function = std::function<double(std::array<double, 3> const&)>;
+
+    virtual double integrate(Function const& f,
+                             unsigned const integration_order) const = 0;
+
+    virtual ~LocalAssemblerDataInterface() = default;
+};
+
+template <typename ShapeFunction, typename IntegrationMethod,
+          unsigned GlobalDim>
+class LocalAssemblerData : public LocalAssemblerDataInterface
+{
+    using ShapeMatricesType = ShapeMatrixPolicyType<ShapeFunction, GlobalDim>;
+    using ShapeMatrices = typename ShapeMatricesType::ShapeMatrices;
+
+public:
+    LocalAssemblerData(MeshLib::Element const& e,
+                       std::size_t const /*local_matrix_size*/,
+                       bool is_axially_symmetric,
+                       unsigned const /*integration_order*/)
+        : _e(e)
+    {
+        if (is_axially_symmetric)
+            OGS_FATAL("Only testing Cartesian meshes!");
+    }
+
+    double integrate(const Function& f,
+                     unsigned const integration_order) const override
+    {
+        double integral = 0;
+
+        auto const sms =
+            ProcessLib::initShapeMatrices<ShapeFunction, ShapeMatricesType,
+                                          IntegrationMethod, GlobalDim>(
+                _e, false /*is_axially_symmetric*/,
+                IntegrationMethod{integration_order});
+        IntegrationMethod integration_method{integration_order};
+
+        for (unsigned ip = 0; ip < sms.size(); ++ip)
+        {
+            // We need to assert that detJ is constant in each element in order
+            // to be sure that in every element that we are integrating over the
+            // effective polynomial degree is the one we expect.
+            EXPECT_NEAR(sms[0].detJ, sms[ip].detJ,
+                        std::numeric_limits<double>::epsilon());
+            auto const& N = sms[ip].N;
+            auto const coords = interpolateNodeCoordinates(_e, N);
+            auto const function_value = f(coords);
+            integral += function_value * sms[ip].detJ *
+                        integration_method.getWeightedPoint(ip).getWeight();
+        }
+
+        return integral;
+    }
+
+private:
+    MeshLib::Element const& _e;
+};
+
+class IntegrationTestProcess
+{
+public:
+    using LocalAssembler = LocalAssemblerDataInterface;
+
+    IntegrationTestProcess(MeshLib::Mesh const& mesh,
+                           unsigned const integration_order)
+        : _integration_order(integration_order),
+          _mesh_subset_all_nodes(mesh, &mesh.getNodes())
+    {
+        std::vector<MeshLib::MeshSubsets> all_mesh_subsets;
+        all_mesh_subsets.emplace_back(&_mesh_subset_all_nodes);
+
+        _dof_table = std::make_unique<NumLib::LocalToGlobalIndexMap>(
+            std::move(all_mesh_subsets), NumLib::ComponentOrder::BY_COMPONENT);
+
+        // createAssemblers(mesh);
+        ProcessLib::createLocalAssemblers<LocalAssemblerData>(
+            mesh.getDimension(), mesh.getElements(), *_dof_table, 1,
+            _local_assemblers, mesh.isAxiallySymmetric(), _integration_order);
+    }
+
+    double integrate(LocalAssembler::Function const& f) const
+    {
+        double integral = 0;
+        for (auto const& la : _local_assemblers)
+        {
+            integral += la->integrate(f, _integration_order);
+        }
+
+        return integral;
+    }
+
+private:
+    unsigned const _integration_order;
+
+    MeshLib::MeshSubset _mesh_subset_all_nodes;
+    std::unique_ptr<NumLib::LocalToGlobalIndexMap> _dof_table;
+
+    std::vector<std::unique_ptr<LocalAssembler>> _local_assemblers;
+};
+
+struct FBase
+{
+private:
+    static std::vector<double> initCoeffs(std::size_t const num_coeffs)
+    {
+        std::vector<double> cs(num_coeffs);
+        fillVectorRandomly(cs);
+        return cs;
+    }
+
+public:
+    FBase(std::size_t const num_coeffs) : coeffs(initCoeffs(num_coeffs)) {}
+
+    virtual double operator()(
+        std::array<double, 3> const& /*coords*/) const = 0;
+    virtual double getAnalyticalIntegralOverUnitCube() const = 0;
+
+    LocalAssemblerDataInterface::Function getClosure() const
+    {
+        return [this](std::array<double, 3> const& coords) {
+            return this->operator()(coords);
+        };
+    }
+
+    virtual ~FBase() = default;
+
+    std::vector<double> const coeffs;
+};
+
+struct FConst final : FBase
+{
+    FConst() : FBase(1) {}
+
+    double operator()(std::array<double, 3> const&) const override
+    {
+        return coeffs[0];
+    }
+
+    double getAnalyticalIntegralOverUnitCube() const override
+    {
+        return coeffs[0];
+    }
+};
+
+struct FLin final : FBase
+{
+    FLin() : FBase(4) {}
+
+    double operator()(std::array<double, 3> const& coords) const override
+    {
+        auto const x = coords[0];
+        auto const y = coords[1];
+        auto const z = coords[2];
+        return coeffs[0] + coeffs[1] * x + coeffs[2] * y + coeffs[3] * z;
+    }
+
+    double getAnalyticalIntegralOverUnitCube() const override
+    {
+        return coeffs[0];
+    }
+};
+
+struct FQuad final : FBase
+{
+    FQuad() : FBase(10) {}
+
+    double operator()(std::array<double, 3> const& coords) const override
+    {
+        auto const x = coords[0];
+        auto const y = coords[1];
+        auto const z = coords[2];
+        return coeffs[0] + coeffs[1] * x + coeffs[2] * y + coeffs[3] * z +
+               coeffs[4] * x * y + coeffs[5] * y * z + coeffs[6] * z * x +
+               coeffs[7] * x * x + coeffs[8] * y * y + coeffs[9] * z * z;
+    }
+
+    double getAnalyticalIntegralOverUnitCube() const override
+    {
+        double const a = -.5;
+        double const b = .5;
+
+        double const a3 = a * a * a;
+        double const b3 = b * b * b;
+
+        return coeffs[0] + (coeffs[7] + coeffs[8] + coeffs[9]) * (b3 - a3) / 3.;
+    }
+};
+
+struct F3DSeparablePolynomial final : FBase
+{
+    F3DSeparablePolynomial(unsigned polynomial_degree)
+        : FBase(3 * polynomial_degree + 3), _degree(polynomial_degree)
+    {
+    }
+
+    // f(x, y, z) = g(x) * h(y) * i(z)
+    double operator()(std::array<double, 3> const& coords) const override
+    {
+        double res = 1.0;
+        for (unsigned d : {0, 1, 2})
+        {
+            auto const x = coords[d];
+
+            double poly = 0.0;
+            for (unsigned n = 0; n <= _degree; ++n)
+            {
+                poly += coeffs[n + d * (_degree + 1)] * std::pow(x, n);
+            }
+
+            res *= poly;
+        }
+
+        return res;
+    }
+
+    // [ F(x, y, z) ]_a^b = [ G(x) ]_a^b * [ H(y) ]_a^b * [ I(z) ]_a^b
+    double getAnalyticalIntegralOverUnitCube() const override
+    {
+        double const a = -.5;
+        double const b = .5;
+
+        double res = 1.0;
+        for (unsigned d : {0, 1, 2})
+        {
+            double poly = 0.0;
+            for (unsigned n = 0; n <= _degree; ++n)
+            {
+                poly += coeffs[n + d * (_degree + 1)] *
+                        (std::pow(b, n + 1) - std::pow(a, n + 1)) / (n + 1);
+            }
+
+            res *= poly;
+        }
+
+        return res;
+    }
+
+private:
+    unsigned const _degree;
+};
+
+unsigned binomial_coefficient(unsigned n, unsigned k)
+{
+    EXPECT_GE(n, k);
+    unsigned res = 1;
+
+    for (unsigned i = n; i > k; --i)
+    {
+        res *= i;
+    }
+
+    for (unsigned i = n - k; i > 0; --i)
+    {
+        res /= i;
+    }
+
+    return res;
+}
+
+/* This function is a polynomial where for each monomial a_ijk x^i y^j z^k
+ * holds: i + j + k <= n, where n is the overall polynomial degree
+ */
+struct F3DNonseparablePolynomial final : FBase
+{
+    // The number of coefficients/monomials are obtained as follows: Compute the
+    // number of combinations with repititions when drawing
+    // polynomial_degree times from the set { x, y, z, 1 }
+    F3DNonseparablePolynomial(unsigned polynomial_degree)
+        : FBase(binomial_coefficient(4 + polynomial_degree - 1, 4 - 1)),
+          _degree(polynomial_degree)
+    {
+    }
+
+    double operator()(std::array<double, 3> const& coords) const override
+    {
+        auto const x = coords[0];
+        auto const y = coords[1];
+        auto const z = coords[2];
+
+        double res = 0.0;
+        unsigned index = 0;
+
+        for (unsigned x_deg = 0; x_deg <= _degree; ++x_deg)
+        {
+            for (unsigned y_deg = 0; x_deg + y_deg <= _degree; ++y_deg)
+            {
+                for (unsigned z_deg = 0; x_deg + y_deg + z_deg <= _degree;
+                     ++z_deg)
+                {
+                    EXPECT_GT(coeffs.size(), index);
+
+                    res += coeffs[index] * std::pow(x, x_deg) *
+                           std::pow(y, y_deg) * std::pow(z, z_deg);
+
+                    ++index;
+                }
+            }
+        }
+
+        EXPECT_EQ(coeffs.size(), index);
+
+        return res;
+    }
+
+    double getAnalyticalIntegralOverUnitCube() const override
+    {
+        double const a = -.5;
+        double const b = .5;
+
+        double res = 0.0;
+        unsigned index = 0;
+
+        for (unsigned x_deg = 0; x_deg <= _degree; ++x_deg)
+        {
+            for (unsigned y_deg = 0; x_deg + y_deg <= _degree; ++y_deg)
+            {
+                for (unsigned z_deg = 0; x_deg + y_deg + z_deg <= _degree;
+                     ++z_deg)
+                {
+                    EXPECT_GT(coeffs.size(), index);
+
+                    res += coeffs[index] *
+                           (std::pow(b, x_deg + 1) - std::pow(a, x_deg + 1)) /
+                           (x_deg + 1) *
+                           (std::pow(b, y_deg + 1) - std::pow(a, y_deg + 1)) /
+                           (y_deg + 1) *
+                           (std::pow(b, z_deg + 1) - std::pow(a, z_deg + 1)) /
+                           (z_deg + 1);
+
+                    ++index;
+                }
+            }
+        }
+
+        EXPECT_EQ(coeffs.size(), index);
+
+        return res;
+    }
+
+private:
+    unsigned const _degree;
+};
+
+std::unique_ptr<FBase> getF(unsigned polynomial_order)
+{
+    std::vector<double> coeffs;
+
+    switch (polynomial_order)
+    {
+        case 0:
+            return std::make_unique<FConst>();
+        case 1:
+            return std::make_unique<FLin>();
+        case 2:
+            return std::make_unique<FQuad>();
+    }
+
+    OGS_FATAL("unsupported polynomial order: %d.", polynomial_order);
+}
+
+}  // namespace GaussLegendreTest
+
+/* *****************************************************************************
+ *
+ * The idea behind the tests in this file is to integrate polynomials of
+ * different degree over the unit cube.
+ *
+ * Gauss-Legendre integration should be able to exactly integrate those up to a
+ * certian degree.
+ *
+ * The coefficients of the tested polynomials are chosen randomly.
+ *
+ **************************************************************************** */
+
+static double const eps = 10 * std::numeric_limits<double>::epsilon();
+
+/* The tests in this file fundamentally rely on a mesh being read from a vtu
+ * file, and a DOF table being computed for that mesh. Since our PETSc build
+ * only works with node partitioned meshes, it cannot digest the read meshes.
+ */
+#ifndef USE_PETSC
+#define OGS_DONT_TEST_THIS_IF_PETSC(group, test_case) TEST(group, test_case)
+#else
+#define OGS_DONT_TEST_THIS_IF_PETSC(group, test_case) \
+    TEST(group, DISABLED_##test_case)
+#endif
+
+OGS_DONT_TEST_THIS_IF_PETSC(MathLib, IntegrationGaussLegendreTet)
+{
+    std::unique_ptr<MeshLib::Mesh> mesh_tet(
+        MeshLib::IO::VtuInterface::readVTUFile(BaseLib::BuildInfo::data_path +
+                                               "/MathLib/unit_cube_tet.vtu"));
+
+    for (unsigned integration_order : {1, 2, 3})
+    {
+        DBUG("\n==== integration order: %u.\n", integration_order);
+        GaussLegendreTest::IntegrationTestProcess pcs_tet(*mesh_tet,
+                                                          integration_order);
+
+        for (unsigned polynomial_order : {0, 1, 2})
+        {
+            if (polynomial_order > 2 * integration_order - 1)
+                break;
+
+            DBUG("  == polynomial order: %u.", polynomial_order);
+            auto f = GaussLegendreTest::getF(polynomial_order);
+
+            auto const integral_tet = pcs_tet.integrate(f->getClosure());
+            EXPECT_NEAR(f->getAnalyticalIntegralOverUnitCube(), integral_tet,
+                        eps);
+        }
+    }
+}
+
+OGS_DONT_TEST_THIS_IF_PETSC(MathLib, IntegrationGaussLegendreHex)
+{
+    std::unique_ptr<MeshLib::Mesh> mesh_hex(
+        MeshLib::IO::VtuInterface::readVTUFile(BaseLib::BuildInfo::data_path +
+                                               "/MathLib/unit_cube_hex.vtu"));
+
+    for (unsigned integration_order : {1, 2, 3})
+    {
+        DBUG("\n==== integration order: %u.\n", integration_order);
+        GaussLegendreTest::IntegrationTestProcess pcs_hex(*mesh_hex,
+                                                          integration_order);
+
+        for (unsigned polynomial_order : {0, 1, 2})
+        {
+            if (polynomial_order > 2 * integration_order - 1)
+                break;
+
+            DBUG("  == polynomial order: %u.", polynomial_order);
+            auto f = GaussLegendreTest::getF(polynomial_order);
+
+            auto const integral_hex = pcs_hex.integrate(f->getClosure());
+            EXPECT_NEAR(f->getAnalyticalIntegralOverUnitCube(), integral_hex,
+                        eps);
+        }
+    }
+}
+
+// This test is disabled, because the polynomials involved are too complicated
+// to be exactly integrated over tetrahedra using Gauss-Legendre quadrature
+OGS_DONT_TEST_THIS_IF_PETSC(
+    MathLib, DISABLED_IntegrationGaussLegendreTetSeparablePolynomial)
+{
+    std::unique_ptr<MeshLib::Mesh> mesh_tet(
+        MeshLib::IO::VtuInterface::readVTUFile(BaseLib::BuildInfo::data_path +
+                                               "/MathLib/unit_cube_tet.vtu"));
+
+    for (unsigned integration_order : {1, 2, 3})
+    {
+        DBUG("\n==== integration order: %u.\n", integration_order);
+        GaussLegendreTest::IntegrationTestProcess pcs_tet(*mesh_tet,
+                                                          integration_order);
+
+        for (unsigned polynomial_order = 0;
+             // Gauss-Legendre integration is exact up to this order!
+             polynomial_order < 2 * integration_order;
+             ++polynomial_order)
+        {
+            DBUG("  == polynomial order: %u.", polynomial_order);
+            GaussLegendreTest::F3DSeparablePolynomial f(polynomial_order);
+
+            auto const integral_tet = pcs_tet.integrate(f.getClosure());
+            EXPECT_NEAR(f.getAnalyticalIntegralOverUnitCube(), integral_tet,
+                        eps);
+        }
+    }
+}
+
+OGS_DONT_TEST_THIS_IF_PETSC(MathLib,
+                            IntegrationGaussLegendreHexSeparablePolynomial)
+{
+    std::unique_ptr<MeshLib::Mesh> mesh_hex(
+        MeshLib::IO::VtuInterface::readVTUFile(BaseLib::BuildInfo::data_path +
+                                               "/MathLib/unit_cube_hex.vtu"));
+
+    for (unsigned integration_order : {1, 2, 3, 4})
+    {
+        DBUG("\n==== integration order: %u.\n", integration_order);
+        GaussLegendreTest::IntegrationTestProcess pcs_hex(*mesh_hex,
+                                                          integration_order);
+
+        for (unsigned polynomial_order = 0;
+             // Gauss-Legendre integration is exact up to this order!
+             polynomial_order < 2 * integration_order;
+             ++polynomial_order)
+        {
+            DBUG("  == polynomial order: %u.", polynomial_order);
+            GaussLegendreTest::F3DSeparablePolynomial f(polynomial_order);
+
+            auto const integral_hex = pcs_hex.integrate(f.getClosure());
+            EXPECT_NEAR(f.getAnalyticalIntegralOverUnitCube(), integral_hex,
+                        eps);
+        }
+    }
+}
+
+OGS_DONT_TEST_THIS_IF_PETSC(MathLib,
+                            IntegrationGaussLegendreTetNonSeparablePolynomial)
+{
+    std::unique_ptr<MeshLib::Mesh> mesh_tet(
+        MeshLib::IO::VtuInterface::readVTUFile(BaseLib::BuildInfo::data_path +
+                                               "/MathLib/unit_cube_tet.vtu"));
+
+    for (unsigned integration_order : {1, 2, 3})
+    {
+        DBUG("\n==== integration order: %u.\n", integration_order);
+        GaussLegendreTest::IntegrationTestProcess pcs_tet(*mesh_tet,
+                                                          integration_order);
+
+        for (unsigned polynomial_order = 0;
+             // Gauss-Legendre integration is exact up to this order!
+             polynomial_order < 2 * integration_order;
+             ++polynomial_order)
+        {
+            DBUG("  == polynomial order: %u.", polynomial_order);
+            GaussLegendreTest::F3DNonseparablePolynomial f(polynomial_order);
+
+            auto const integral_tet = pcs_tet.integrate(f.getClosure());
+            EXPECT_NEAR(f.getAnalyticalIntegralOverUnitCube(), integral_tet,
+                        eps);
+        }
+    }
+}
+
+OGS_DONT_TEST_THIS_IF_PETSC(MathLib,
+                            IntegrationGaussLegendreHexNonSeparablePolynomial)
+{
+    std::unique_ptr<MeshLib::Mesh> mesh_hex(
+        MeshLib::IO::VtuInterface::readVTUFile(BaseLib::BuildInfo::data_path +
+                                               "/MathLib/unit_cube_hex.vtu"));
+
+    for (unsigned integration_order : {1, 2, 3, 4})
+    {
+        DBUG("\n==== integration order: %u.\n", integration_order);
+        GaussLegendreTest::IntegrationTestProcess pcs_hex(*mesh_hex,
+                                                          integration_order);
+
+        for (unsigned polynomial_order = 0;
+             // Gauss-Legendre integration is exact up to this order!
+             polynomial_order < 2 * integration_order;
+             ++polynomial_order)
+        {
+            DBUG("  == polynomial order: %u.", polynomial_order);
+            GaussLegendreTest::F3DNonseparablePolynomial f(polynomial_order);
+
+            auto const integral_hex = pcs_hex.integrate(f.getClosure());
+            EXPECT_NEAR(f.getAnalyticalIntegralOverUnitCube(), integral_hex,
+                        eps);
+        }
+    }
+}
+
+#undef OGS_DONT_TEST_THIS_IF_PETSC
diff --git a/Tests/NumLib/TestExtrapolation.cpp b/Tests/NumLib/TestExtrapolation.cpp
index 075556d43ce5aec93cdad88f370e124ccfd2264e..10a82220df115efb9d7bf886d8357dca789626be 100644
--- a/Tests/NumLib/TestExtrapolation.cpp
+++ b/Tests/NumLib/TestExtrapolation.cpp
@@ -11,8 +11,8 @@
 
 #include "MathLib/LinAlg/LinAlg.h"
 
-#include "MeshLib/MeshGenerators/MeshGenerator.h"
 #include "MeshLib/IO/writeMeshToFile.h"
+#include "MeshLib/MeshGenerators/MeshGenerator.h"
 
 #include "NumLib/DOF/DOFTableUtil.h"
 #include "NumLib/DOF/MatrixProviderUser.h"
@@ -24,13 +24,13 @@
 #include "NumLib/Function/Interpolation.h"
 #include "NumLib/NumericsConfig.h"
 
-#include "ProcessLib/Utils/LocalDataInitializer.h"
 #include "ProcessLib/Utils/CreateLocalAssemblers.h"
 #include "ProcessLib/Utils/InitShapeMatrices.h"
+#include "ProcessLib/Utils/LocalDataInitializer.h"
 
 #include "Tests/VectorUtils.h"
 
-namespace
+namespace ExtrapolationTest
 {
 template <typename ShapeMatrices>
 void interpolateNodalValuesToIntegrationPoints(
@@ -39,15 +39,13 @@ void interpolateNodalValuesToIntegrationPoints(
         shape_matrices,
     std::vector<double>& interpolated_values)
 {
-    for (unsigned ip=0; ip<shape_matrices.size(); ++ip)
+    for (unsigned ip = 0; ip < shape_matrices.size(); ++ip)
     {
         NumLib::shapeFunctionInterpolate(
             local_nodal_values, shape_matrices[ip].N, interpolated_values[ip]);
     }
 }
 
-} // anonymous namespace
-
 class LocalAssemblerDataInterface : public NumLib::ExtrapolatableElement
 {
 public:
@@ -96,8 +94,8 @@ public:
     {
     }
 
-    Eigen::Map<const Eigen::RowVectorXd>
-    getShapeMatrix(const unsigned integration_point) const override
+    Eigen::Map<const Eigen::RowVectorXd> getShapeMatrix(
+        const unsigned integration_point) const override
     {
         auto const& N = _shape_matrices[integration_point].N;
 
@@ -129,7 +127,7 @@ public:
     void interpolateNodalValuesToIntegrationPoints(
         std::vector<double> const& local_nodal_values) override
     {
-        ::interpolateNodalValuesToIntegrationPoints(
+        ExtrapolationTest::interpolateNodalValuesToIntegrationPoints(
             local_nodal_values, _shape_matrices, _int_pt_values);
     }
 
@@ -139,7 +137,7 @@ private:
     std::vector<double> _int_pt_values;
 };
 
-class TestProcess
+class ExtrapolationTestProcess
 {
 public:
     using LocalAssembler = LocalAssemblerDataInterface;
@@ -148,9 +146,10 @@ public:
     using ExtrapolatorImplementation =
         NumLib::LocalLinearLeastSquaresExtrapolator;
 
-    TestProcess(MeshLib::Mesh const& mesh, unsigned const integration_order)
-        : _integration_order(integration_order)
-        , _mesh_subset_all_nodes(mesh, &mesh.getNodes())
+    ExtrapolationTestProcess(MeshLib::Mesh const& mesh,
+                             unsigned const integration_order)
+        : _integration_order(integration_order),
+          _mesh_subset_all_nodes(mesh, &mesh.getNodes())
     {
         std::vector<MeshLib::MeshSubsets> all_mesh_subsets;
         all_mesh_subsets.emplace_back(&_mesh_subset_all_nodes);
@@ -211,13 +210,14 @@ private:
     std::unique_ptr<ExtrapolatorInterface> _extrapolator;
 };
 
-void extrapolate(TestProcess const& pcs, IntegrationPointValuesMethod method,
+void extrapolate(ExtrapolationTestProcess const& pcs,
+                 IntegrationPointValuesMethod method,
                  GlobalVector const& expected_extrapolated_global_nodal_values,
                  std::size_t const nnodes, std::size_t const nelements)
 {
     namespace LinAlg = MathLib::LinAlg;
 
-    auto const tolerance_dx  = 30.0 * std::numeric_limits<double>::epsilon();
+    auto const tolerance_dx = 30.0 * std::numeric_limits<double>::epsilon();
     auto const tolerance_res = 15.0 * std::numeric_limits<double>::epsilon();
 
     const double t = 0.0;
@@ -227,7 +227,7 @@ void extrapolate(TestProcess const& pcs, IntegrationPointValuesMethod method,
     auto const& x_extra = *result.first;
     auto const& residual = *result.second;
 
-    ASSERT_EQ(nnodes,    x_extra.size());
+    ASSERT_EQ(nnodes, x_extra.size());
     ASSERT_EQ(nelements, residual.size());
 
     auto const res_norm = LinAlg::normMax(residual);
@@ -235,14 +235,16 @@ void extrapolate(TestProcess const& pcs, IntegrationPointValuesMethod method,
     EXPECT_GT(tolerance_res, res_norm);
 
     auto delta_x = MathLib::MatrixVectorTraits<GlobalVector>::newInstance(
-                expected_extrapolated_global_nodal_values);
-    LinAlg::axpy(*delta_x, -1.0, x_extra); // delta_x = x_expected - x_extra
+        expected_extrapolated_global_nodal_values);
+    LinAlg::axpy(*delta_x, -1.0, x_extra);  // delta_x = x_expected - x_extra
 
     auto const dx_norm = LinAlg::normMax(*delta_x);
     DBUG("maximum norm of delta x:  %g", dx_norm);
     EXPECT_GT(tolerance_dx, dx_norm);
 }
 
+}  // anonymous namespace
+
 #ifndef USE_PETSC
 TEST(NumLib, Extrapolation)
 #else
@@ -264,18 +266,20 @@ TEST(NumLib, DISABLED_Extrapolation)
 
     // generate mesh
     std::unique_ptr<MeshLib::Mesh> mesh(
-                MeshLib::MeshGenerator::generateRegularHexMesh(
-                    mesh_length, mesh_elements_in_each_direction));
+        MeshLib::MeshGenerator::generateRegularHexMesh(
+            mesh_length, mesh_elements_in_each_direction));
 
     for (unsigned integration_order : {2, 3, 4})
     {
         namespace LinAlg = MathLib::LinAlg;
 
-        auto const nnodes    = mesh->getNumberOfNodes();
+        auto const nnodes = mesh->getNumberOfNodes();
         auto const nelements = mesh->getNumberOfElements();
-        DBUG("number of nodes: %lu, number of elements: %lu", nnodes, nelements);
+        DBUG("number of nodes: %lu, number of elements: %lu", nnodes,
+             nelements);
 
-        TestProcess pcs(*mesh, integration_order);
+        ExtrapolationTest::ExtrapolationTestProcess pcs(*mesh,
+                                                        integration_order);
 
         // generate random nodal values
         MathLib::MatrixSpecifications spec{nnodes, nnodes, nullptr, nullptr};
@@ -287,16 +291,20 @@ TEST(NumLib, DISABLED_Extrapolation)
 
         // test extrapolation of a quantity that is stored in the local
         // assembler
-        extrapolate(pcs, &LocalAssemblerDataInterface::getStoredQuantity, *x,
-                    nnodes, nelements);
+        ExtrapolationTest::extrapolate(
+            pcs,
+            &ExtrapolationTest::LocalAssemblerDataInterface::getStoredQuantity,
+            *x, nnodes, nelements);
 
         // expect 2*x as extraplation result for derived quantity
         auto two_x = MathLib::MatrixVectorTraits<GlobalVector>::newInstance(*x);
-        LinAlg::axpy(*two_x, 1.0, *x); // two_x = x + x
+        LinAlg::axpy(*two_x, 1.0, *x);  // two_x = x + x
 
         // test extrapolation of a quantity that is derived from some
         // integration point values
-        extrapolate(pcs, &LocalAssemblerDataInterface::getDerivedQuantity,
-                    *two_x, nnodes, nelements);
+        ExtrapolationTest::extrapolate(
+            pcs,
+            &ExtrapolationTest::LocalAssemblerDataInterface::getDerivedQuantity,
+            *two_x, nnodes, nelements);
     }
 }
diff --git a/Tests/VectorUtils.h b/Tests/VectorUtils.h
index d8c9adc0527080dcb4f9b403e28ad85be350bbb1..673f2083565ea4de47e6dfb6494ffcf6fde40744 100644
--- a/Tests/VectorUtils.h
+++ b/Tests/VectorUtils.h
@@ -28,3 +28,15 @@ void fillVectorRandomly(Vector& x)
     finalizeVectorAssembly(x);
 #endif
 }
+
+inline void fillVectorRandomly(std::vector<double>& x)
+{
+    std::random_device rd;
+    std::mt19937 random_number_generator(rd());
+    std::uniform_real_distribution<double> rnd;
+
+    for (auto& value : x)
+    {
+        value = rnd(random_number_generator);
+    }
+}