diff --git a/Applications/ApplicationsLib/ProjectData.cpp b/Applications/ApplicationsLib/ProjectData.cpp
index 5b2209b42d001d700b83881f9c930a9da0172c57..6606f35b60063720f65194515ecf3f1c392726e0 100644
--- a/Applications/ApplicationsLib/ProjectData.cpp
+++ b/Applications/ApplicationsLib/ProjectData.cpp
@@ -471,33 +471,11 @@ void ProjectData::parseMedia(
         auto material_id_string =
             //! \ogs_file_attr{prj__media__medium__id}
             medium_config.getConfigAttribute<std::string>("id", "0");
-        material_id_string.erase(
-            remove_if(begin(material_id_string), end(material_id_string),
-                      [](unsigned char const c) { return std::isspace(c); }),
-            end(material_id_string));
-        auto const material_ids_strings =
-            BaseLib::splitString(material_id_string, ',');
-
-        // Convert strings to ints;
-        std::vector<int> material_ids;
-        std::transform(
-            begin(material_ids_strings), end(material_ids_strings),
-            std::back_inserter(material_ids), [](std::string const& m_id) {
-                if (auto const it = std::find_if_not(
-                        begin(m_id), end(m_id),
-                        [](unsigned char const c) { return std::isdigit(c); });
-                    it != end(m_id))
-                {
-                    OGS_FATAL(
-                        "Could not parse material ID's from '{:s}'. Please "
-                        "separate multiple material ID's by comma only. "
-                        "Invalid character: '%c'",
-                        m_id, *it);
-                }
-                return std::stoi(m_id);
-            });
 
-        for (auto const& id : material_ids)
+        auto const material_ids_of_this_medium =
+            splitMaterialIdString(material_id_string);
+
+        for (auto const& id : material_ids_of_this_medium)
         {
             if (_media.find(id) != end(_media))
             {
@@ -508,15 +486,21 @@ void ProjectData::parseMedia(
                     id);
             }
 
-            _media[id] =
-                (id == material_ids[0])
-                    ? MaterialPropertyLib::createMedium(
-                          _mesh_vec[0]->getDimension(), medium_config,
-                          _parameters,
-                          _local_coordinate_system ? &*_local_coordinate_system
-                                                   : nullptr,
-                          _curves)
-                    : _media[material_ids[0]];
+            if (id == material_ids_of_this_medium[0])
+            {
+                _media[id] = MaterialPropertyLib::createMedium(
+                    _mesh_vec[0]->getDimension(), medium_config, _parameters,
+                    _local_coordinate_system ? &*_local_coordinate_system
+                                             : nullptr,
+                    _curves);
+            }
+            else
+            {
+                // This medium has multiple material IDs assigned and this is
+                // not the first material ID. Therefore we can reuse the medium
+                // we created before.
+                _media[id] = _media[material_ids_of_this_medium[0]];
+            }
         }
     }
 
@@ -1213,3 +1197,54 @@ void ProjectData::parseCurves(std::optional<BaseLib::ConfigTree> const& config)
             "The curve name is not unique.");
     }
 }
+
+std::vector<int> splitMaterialIdString(std::string const& material_id_string)
+{
+    auto const material_ids_strings =
+        BaseLib::splitString(material_id_string, ',');
+
+    std::vector<int> material_ids;
+    for (auto& mid_str : material_ids_strings)
+    {
+        std::size_t num_chars_processed = 0;
+        int material_id;
+        try
+        {
+            material_id = std::stoi(mid_str, &num_chars_processed);
+        }
+        catch (std::invalid_argument&)
+        {
+            OGS_FATAL(
+                "Could not parse material ID from '{}' to a valid "
+                "integer.",
+                mid_str);
+        }
+        catch (std::out_of_range&)
+        {
+            OGS_FATAL(
+                "Could not parse material ID from '{}'. The integer value "
+                "of the given string exceeds the permitted range.",
+                mid_str);
+        }
+
+        if (num_chars_processed != mid_str.size())
+        {
+            // Not the whole string has been parsed. Check the rest.
+            if (auto const it = std::find_if_not(
+                    begin(mid_str) + num_chars_processed, end(mid_str),
+                    [](unsigned char const c) { return std::isspace(c); });
+                it != end(mid_str))
+            {
+                OGS_FATAL(
+                    "Could not parse material ID from '{}'. Please "
+                    "separate multiple material IDs by comma only. "
+                    "Invalid character: '{}' at position {}.",
+                    mid_str, *it, distance(begin(mid_str), it));
+            }
+        }
+
+        material_ids.push_back(material_id);
+    };
+
+    return material_ids;
+}
diff --git a/Applications/ApplicationsLib/ProjectData.h b/Applications/ApplicationsLib/ProjectData.h
index 47b57e4922457a6373bbfd9255ee5053cbd9291b..cd81ac4fc0f6eac90e18e06a2b75629460c7f339 100644
--- a/Applications/ApplicationsLib/ProjectData.h
+++ b/Applications/ApplicationsLib/ProjectData.h
@@ -141,3 +141,8 @@ private:
              std::unique_ptr<MathLib::PiecewiseLinearInterpolation>>
         _curves;
 };
+
+/// Parses a comma separated list of integers.
+/// Such lists occur in the medium definition in the OGS prj file.
+/// Error messages in this function refer to this specific purpose.
+std::vector<int> splitMaterialIdString(std::string const& material_id_string);
diff --git a/Tests/ApplicationsLib/TestProjectData.cpp b/Tests/ApplicationsLib/TestProjectData.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..5985cbd31fe00c0f5e45b9838e0225ce74093f5a
--- /dev/null
+++ b/Tests/ApplicationsLib/TestProjectData.cpp
@@ -0,0 +1,95 @@
+/**
+ * \file
+ * \copyright
+ * Copyright (c) 2012-2021, OpenGeoSys Community (http://www.opengeosys.org)
+ *            Distributed under a Modified BSD License.
+ *              See accompanying file LICENSE.txt or
+ *              http://www.opengeosys.org/project/license
+ *
+ */
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include <algorithm>
+
+#include "Applications/ApplicationsLib/ProjectData.h"
+
+TEST(ApplicationsLib_ProjectData_SplitIntegerList, EmptyList)
+{
+    ASSERT_TRUE(splitMaterialIdString("").empty());
+}
+
+TEST(ApplicationsLib_ProjectData_SplitIntegerList, SingleInt)
+{
+    using namespace testing;
+
+    EXPECT_THAT(splitMaterialIdString("5"), ContainerEq(std::vector<int>{5}));
+
+    // leading whitespace
+    EXPECT_THAT(splitMaterialIdString("  16"),
+                ContainerEq(std::vector<int>{16}));
+
+    // trailing whitespace
+    EXPECT_THAT(splitMaterialIdString("23    "),
+                ContainerEq(std::vector<int>{23}));
+
+    // negative numbers are OK
+    EXPECT_THAT(splitMaterialIdString("-20"),
+                ContainerEq(std::vector<int>{-20}));
+}
+
+TEST(ApplicationsLib_ProjectData_SplitIntegerList, SingleIntFail)
+{
+    // wrong character prefix/suffix
+    EXPECT_THROW(splitMaterialIdString("x"), std::runtime_error);
+    EXPECT_THROW(splitMaterialIdString(".5"), std::runtime_error);
+    EXPECT_THROW(splitMaterialIdString("5?"), std::runtime_error);
+    EXPECT_THROW(splitMaterialIdString("7 !"), std::runtime_error);
+    EXPECT_THROW(splitMaterialIdString("8   u"), std::runtime_error);
+
+    // hexadecimal numbers are not accepted
+    EXPECT_THROW(splitMaterialIdString("0xfa"), std::runtime_error);
+
+    // another integer is not accepted
+    EXPECT_THROW(splitMaterialIdString("1 2"), std::runtime_error);
+
+    // range exceeeded
+    EXPECT_THROW(
+        splitMaterialIdString("1234567890123456789012345678901234567890"),
+        std::runtime_error);
+}
+
+TEST(ApplicationsLib_ProjectData_SplitIntegerList, IntList)
+{
+    using namespace testing;
+
+    EXPECT_THAT(splitMaterialIdString("5,6,7"),
+                ContainerEq(std::vector<int>{5, 6, 7}));
+
+    // whitespace around comma
+    EXPECT_THAT(splitMaterialIdString("9  ,10,  11,12   ,   13"),
+                ContainerEq(std::vector<int>{9, 10, 11, 12, 13}));
+
+    // trailing comma is ignored
+    EXPECT_THAT(splitMaterialIdString("20, 22, 24,"),
+                ContainerEq(std::vector<int>{20, 22, 24}));
+}
+
+TEST(ApplicationsLib_ProjectData_SplitIntegerList, IntListFail)
+{
+    // only delimiter
+    EXPECT_THROW(splitMaterialIdString(","), std::runtime_error);
+
+    // empty element
+    EXPECT_THROW(splitMaterialIdString("5,,6"), std::runtime_error);
+
+    // leading comma
+    EXPECT_THROW(splitMaterialIdString(",40"), std::runtime_error);
+
+    // missing comma
+    EXPECT_THROW(splitMaterialIdString("12   20"), std::runtime_error);
+
+    // wrong number in the list
+    EXPECT_THROW(splitMaterialIdString("1,2,x,5"), std::runtime_error);
+}
diff --git a/Tests/BaseLib/TestStringTools.cpp b/Tests/BaseLib/TestStringTools.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..7a524ab96c6219ece17cd046063b8efee7253b95
--- /dev/null
+++ b/Tests/BaseLib/TestStringTools.cpp
@@ -0,0 +1,45 @@
+/**
+ * \file
+ * \copyright
+ * Copyright (c) 2012-2021, OpenGeoSys Community (http://www.opengeosys.org)
+ *            Distributed under a Modified BSD License.
+ *              See accompanying file LICENSE.txt or
+ *              http://www.opengeosys.org/project/license
+ *
+ */
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include "BaseLib/StringTools.h"
+
+TEST(BaseLibStringTools, SplitString)
+{
+    using namespace testing;
+    using namespace std;
+
+    // empty string
+    EXPECT_THAT(BaseLib::splitString("", ';'), ContainerEq(list<string>{}));
+
+    // no delimiter
+    EXPECT_THAT(BaseLib::splitString("a", ';'), ContainerEq(list<string>{"a"}));
+
+    // some delimited values
+    EXPECT_THAT(BaseLib::splitString("a,b,c", ','),
+                ContainerEq(list<string>{"a", "b", "c"}));
+
+    // leading delimiter
+    EXPECT_THAT(BaseLib::splitString(",a,b,c", ','),
+                ContainerEq(list<string>{"", "a", "b", "c"}));
+
+    // double delimiters
+    EXPECT_THAT(BaseLib::splitString("a,b,,c", ','),
+                ContainerEq(list<string>{"a", "b", "", "c"}));
+
+    // trailing delimiters are ignored
+    EXPECT_THAT(BaseLib::splitString("a,b,c,", ','),
+                ContainerEq(list<string>{"a", "b", "c"}));
+
+    // ... and behave like this if they are on their own:
+    EXPECT_THAT(BaseLib::splitString(",", ','), ContainerEq(list<string>{""}));
+}
diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt
index 4e4db65ca4032dd10035d8a8fac6d714574d1586..2fdf7362b13ab46079e2731866be99aa699fffc8 100644
--- a/Tests/CMakeLists.txt
+++ b/Tests/CMakeLists.txt
@@ -24,6 +24,7 @@ append_source_files(TEST_SOURCES GeoLib/IO)
 append_source_files(TEST_SOURCES MaterialLib)
 append_source_files(TEST_SOURCES MathLib)
 append_source_files(TEST_SOURCES MeshLib)
+append_source_files(TEST_SOURCES ApplicationsLib)
 append_source_files(TEST_SOURCES MeshGeoToolsLib)
 append_source_files(TEST_SOURCES_NUMLIB NumLib)
 # Disable Unity build for NumLib tests
@@ -62,8 +63,10 @@ ogs_add_executable(testrunner ${TEST_SOURCES})
 
 target_link_libraries(
     testrunner
-    PRIVATE ApplicationsFileIO
+    PRIVATE ApplicationsLib
+            ApplicationsFileIO
             autocheck
+            gmock
             gtest
             MeshGeoToolsLib
             MaterialLib