diff --git a/.github/workflows/openmw.yml b/.github/workflows/openmw.yml
index 2b429df0a0..3df3373003 100644
--- a/.github/workflows/openmw.yml
+++ b/.github/workflows/openmw.yml
@@ -1,11 +1,8 @@
 name: CMake
 
 on:
-  push:
-    branches:
-      - 'master'
-  pull_request:
-    branches: [ master ]
+- push
+- pull_request
 
 env:
   BUILD_TYPE: RelWithDebInfo
@@ -30,13 +27,27 @@ jobs:
           max-size: 1000M
 
       - name: Configure
-        run: cmake . -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DOPENMW_USE_SYSTEM_RECASTNAVIGATION=1 -DUSE_SYSTEM_TINYXML=1 -DBUILD_UNITTESTS=1 -DCMAKE_INSTALL_PREFIX=install
+        run: >
+            cmake .
+            -D CMAKE_BUILD_TYPE=${{env.BUILD_TYPE}}
+            -D OPENMW_USE_SYSTEM_RECASTNAVIGATION=ON
+            -D USE_SYSTEM_TINYXML=ON
+            -D BUILD_COMPONENTS_TESTS=ON
+            -D BUILD_OPENMW_TESTS=ON
+            -D BUILD_OPENCS_TESTS=ON
+            -D CMAKE_INSTALL_PREFIX=install
 
       - name: Build
-        run: make -j3
+        run: cmake --build . -- -j$(nproc)
 
-      - name: Test
-        run: ./openmw_test_suite
+      - name: Run components tests
+        run: ./components-tests
+
+      - name: Run OpenMW tests
+        run: ./openmw-tests
+
+      - name: Run OpenMW-CS tests
+        run: ./openmw-cs-tests
 
           #    - name: Install
           #      shell: bash
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index d592fb644a..76ae281bcd 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -69,15 +69,16 @@ Ubuntu_GCC_preprocess:
     - df -h
     - du -sh .
     - cmake --install .
-    - if [[ "${BUILD_TESTS_ONLY}" ]]; then ./openmw_test_suite --gtest_output="xml:openmw_tests.xml"; fi
-    - if [[ "${BUILD_TESTS_ONLY}" ]]; then ./openmw-cs-tests --gtest_output="xml:openmw_cs_tests.xml"; fi
+    - if [[ "${BUILD_TESTS_ONLY}" ]]; then ./components-tests --gtest_output="xml:components-tests.xml"; fi
+    - if [[ "${BUILD_TESTS_ONLY}" ]]; then ./openmw-tests --gtest_output="xml:openmw-tests.xml"; fi
+    - if [[ "${BUILD_TESTS_ONLY}" ]]; then ./openmw-cs-tests --gtest_output="xml:openmw-cs-tests.xml"; fi
     - if [[ "${BUILD_TESTS_ONLY}" && ! "${BUILD_WITH_CODE_COVERAGE}" ]]; then ./openmw_detournavigator_navmeshtilescache_benchmark; fi
     - if [[ "${BUILD_TESTS_ONLY}" && ! "${BUILD_WITH_CODE_COVERAGE}" ]]; then ./openmw_esm_refid_benchmark; fi
     - if [[ "${BUILD_TESTS_ONLY}" && ! "${BUILD_WITH_CODE_COVERAGE}" ]]; then ./openmw_settings_access_benchmark; fi
     - ccache -s
     - df -h
     - if [[ "${BUILD_WITH_CODE_COVERAGE}" ]]; then gcovr --xml-pretty --exclude-unreachable-branches --print-summary --root "${CI_PROJECT_DIR}" -j $(nproc) -o ../coverage.xml; fi
-    - ls | grep -v -e '^extern$' -e '^install$' -e '^openmw_tests.xml$' -e '^openmw_cs_tests.xml$' | xargs -I '{}' rm -rf './{}'
+    - ls | grep -v -e '^extern$' -e '^install$' -e '^components-tests.xml$' -e '^openmw-tests.xml$' -e '^openmw-cs-tests.xml$' | xargs -I '{}' rm -rf './{}'
     - cd ..
     - df -h
     - du -sh build/
@@ -422,7 +423,7 @@ Ubuntu_Clang_Tidy_other:
   needs:
     - Ubuntu_Clang_Tidy_components
   variables:
-    BUILD_TARGETS: bsatool esmtool openmw-launcher openmw-iniimporter openmw-essimporter openmw-wizard niftest openmw_test_suite openmw-navmeshtool openmw-bulletobjecttool
+    BUILD_TARGETS: bsatool esmtool openmw-launcher openmw-iniimporter openmw-essimporter openmw-wizard niftest components-tests openmw-tests openmw-cs-tests openmw-navmeshtool openmw-bulletobjecttool
   timeout: 3h
 
 .Ubuntu_Clang_tests:
@@ -645,7 +646,7 @@ macOS14_Xcode15_arm64:
   variables:
     config: "RelWithDebInfo"
     # Gitlab can't successfully execute following binaries due to unknown reason
-    # executables: "openmw_test_suite.exe,openmw_detournavigator_navmeshtilescache_benchmark.exe"
+    # executables: "components-tests.exe,openmw-tests.exe,openmw-cs-tests.exe,openmw_detournavigator_navmeshtilescache_benchmark.exe"
 
 .Windows_MSBuild_Base:
   tags:
@@ -747,7 +748,7 @@ Windows_MSBuild_RelWithDebInfo:
   variables:
     config: "RelWithDebInfo"
     # Gitlab can't successfully execute following binaries due to unknown reason
-    # executables: "openmw_test_suite.exe,openmw_detournavigator_navmeshtilescache_benchmark.exe"
+    # executables: "components-tests.exe,openmw-tests.exe,openmw-cs-tests.exe,openmw_detournavigator_navmeshtilescache_benchmark.exe"
   # temporarily enabled while we're linking these on the downloads page
   rules:
     # run this for both pushes and schedules so 'latest successful pipeline for branch' always includes it
diff --git a/CI/before_script.linux.sh b/CI/before_script.linux.sh
index ab61ed3e59..2589c2807e 100755
--- a/CI/before_script.linux.sh
+++ b/CI/before_script.linux.sh
@@ -7,14 +7,6 @@ free -m
 # Silence a git warning
 git config --global advice.detachedHead false
 
-BUILD_UNITTESTS=OFF
-BUILD_BENCHMARKS=OFF
-
-if [[ "${BUILD_TESTS_ONLY}" ]]; then
-    BUILD_UNITTESTS=ON
-    BUILD_BENCHMARKS=ON
-fi
-
 # setup our basic cmake build options
 declare -a CMAKE_CONF_OPTS=(
     -DCMAKE_C_COMPILER="${CC:-/usr/bin/cc}"
@@ -46,14 +38,14 @@ fi
 
 if [[ $CI_CLANG_TIDY ]]; then
     CMAKE_CONF_OPTS+=(
-          -DCMAKE_CXX_CLANG_TIDY="clang-tidy;--warnings-as-errors=*"
-          -DBUILD_UNITTESTS=ON
-          -DBUILD_OPENCS_TESTS=ON
-          -DBUILD_BENCHMARKS=ON
+        -DCMAKE_CXX_CLANG_TIDY="clang-tidy;--warnings-as-errors=*"
+        -DBUILD_COMPONENTS_TESTS=ON
+        -DBUILD_OPENMW_TESTS=ON
+        -DBUILD_OPENCS_TESTS=ON
+        -DBUILD_BENCHMARKS=ON
     )
 fi
 
-
 if [[ "${CMAKE_BUILD_TYPE}" ]]; then
     CMAKE_CONF_OPTS+=(
         -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
@@ -103,9 +95,10 @@ if [[ "${BUILD_TESTS_ONLY}" ]]; then
         -DBUILD_NAVMESHTOOL=OFF \
         -DBUILD_BULLETOBJECTTOOL=OFF \
         -DBUILD_NIFTEST=OFF \
-        -DBUILD_UNITTESTS=${BUILD_UNITTESTS} \
-        -DBUILD_OPENCS_TESTS=${BUILD_UNITTESTS} \
-        -DBUILD_BENCHMARKS=${BUILD_BENCHMARKS} \
+        -DBUILD_COMPONENTS_TESTS=ON \
+        -DBUILD_OPENMW_TESTS=ON \
+        -DBUILD_OPENCS_TESTS=ON \
+        -DBUILD_BENCHMARKS=ON \
         ..
 elif [[ "${BUILD_OPENMW_ONLY}" ]]; then
     ${ANALYZE} cmake \
diff --git a/CI/before_script.msvc.sh b/CI/before_script.msvc.sh
index 3d480ff500..6b58dbe1c3 100644
--- a/CI/before_script.msvc.sh
+++ b/CI/before_script.msvc.sh
@@ -1169,8 +1169,9 @@ if [ "${BUILD_BENCHMARKS}" ]; then
 fi
 
 if [ -n "${TEST_FRAMEWORK}" ]; then
-	add_cmake_opts -DBUILD_UNITTESTS=ON
+	add_cmake_opts -DBUILD_COMPONENTS_TESTS=ON
 	add_cmake_opts -DBUILD_OPENCS_TESTS=ON
+	add_cmake_opts -DBUILD_OPENMW_TESTS=ON
 fi
 
 if [ -n "$ACTIVATE_MSVC" ]; then
diff --git a/CI/file_name_exceptions.txt b/CI/file_name_exceptions.txt
index dff3527348..14d106169b 100644
--- a/CI/file_name_exceptions.txt
+++ b/CI/file_name_exceptions.txt
@@ -8,28 +8,26 @@ apps/openmw/mwsound/sound_buffer.cpp
 apps/openmw/mwsound/sound_buffer.hpp
 apps/openmw/mwsound/sound_decoder.hpp
 apps/openmw/mwsound/sound_output.hpp
-apps/openmw_test_suite/esm/test_fixed_string.cpp
-apps/openmw_test_suite/files/conversion_tests.cpp
-apps/openmw_test_suite/lua/test_async.cpp
-apps/openmw_test_suite/lua/test_configuration.cpp
-apps/openmw_test_suite/lua/test_l10n.cpp
-apps/openmw_test_suite/lua/test_lua.cpp
-apps/openmw_test_suite/lua/test_scriptscontainer.cpp
-apps/openmw_test_suite/lua/test_serialization.cpp
-apps/openmw_test_suite/lua/test_storage.cpp
-apps/openmw_test_suite/lua/test_ui_content.cpp
-apps/openmw_test_suite/lua/test_utilpackage.cpp
-apps/openmw_test_suite/lua/test_inputactions.cpp
-apps/openmw_test_suite/lua/test_yaml.cpp
-apps/openmw_test_suite/misc/test_endianness.cpp
-apps/openmw_test_suite/misc/test_resourcehelpers.cpp
-apps/openmw_test_suite/misc/test_stringops.cpp
-apps/openmw_test_suite/mwdialogue/test_keywordsearch.cpp
-apps/openmw_test_suite/mwscript/test_scripts.cpp
-apps/openmw_test_suite/mwscript/test_utils.hpp
-apps/openmw_test_suite/mwworld/test_store.cpp
-apps/openmw_test_suite/openmw_test_suite.cpp
-apps/openmw_test_suite/testing_util.hpp
+apps/components_tests/esm/test_fixed_string.cpp
+apps/components_tests/files/conversion_tests.cpp
+apps/components_tests/lua/test_async.cpp
+apps/components_tests/lua/test_configuration.cpp
+apps/components_tests/lua/test_l10n.cpp
+apps/components_tests/lua/test_lua.cpp
+apps/components_tests/lua/test_scriptscontainer.cpp
+apps/components_tests/lua/test_serialization.cpp
+apps/components_tests/lua/test_storage.cpp
+apps/components_tests/lua/test_ui_content.cpp
+apps/components_tests/lua/test_utilpackage.cpp
+apps/components_tests/lua/test_inputactions.cpp
+apps/components_tests/lua/test_yaml.cpp
+apps/components_tests/misc/test_endianness.cpp
+apps/components_tests/misc/test_resourcehelpers.cpp
+apps/components_tests/misc/test_stringops.cpp
+apps/openmw_tests/mwdialogue/test_keywordsearch.cpp
+apps/openmw_tests/mwscript/test_scripts.cpp
+apps/openmw_tests/mwscript/test_utils.hpp
+apps/openmw_tests/mwworld/test_store.cpp
 components/bsa/bsa_file.cpp
 components/bsa/bsa_file.hpp
 components/crashcatcher/windows_crashcatcher.cpp
diff --git a/CI/ubuntu_gcc_preprocess.sh b/CI/ubuntu_gcc_preprocess.sh
index 05d7528e41..d519d178aa 100755
--- a/CI/ubuntu_gcc_preprocess.sh
+++ b/CI/ubuntu_gcc_preprocess.sh
@@ -31,7 +31,8 @@ cmake \
     -D BUILD_OPENCS=ON \
     -D BUILD_OPENCS_TESTS=ON \
     -D BUILD_OPENMW=ON \
-    -D BUILD_UNITTESTS=ON \
+    -D BUILD_OPENMW_TESTS=ON \
+    -D BUILD_COMPONENTS_TESTS=ON \
     -D BUILD_WIZARD=ON \
     "${SRC}"
 cmake --build . --parallel
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e04c3b59a7..4b1c084077 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -39,11 +39,12 @@ option(BUILD_ESMTOOL            "Build ESM inspector" ON)
 option(BUILD_NIFTEST            "Build nif file tester" ON)
 option(BUILD_DOCS               "Build documentation." OFF )
 option(BUILD_WITH_CODE_COVERAGE "Enable code coverage with gconv" OFF)
-option(BUILD_UNITTESTS          "Enable Unittests with Google C++ Unittest" OFF)
+option(BUILD_COMPONENTS_TESTS   "Build components library tests" OFF)
 option(BUILD_BENCHMARKS         "Build benchmarks with Google Benchmark" OFF)
 option(BUILD_NAVMESHTOOL        "Build navmesh tool" ON)
 option(BUILD_BULLETOBJECTTOOL   "Build Bullet object tool" ON)
 option(BUILD_OPENCS_TESTS       "Build OpenMW Construction Set tests" OFF)
+option(BUILD_OPENMW_TESTS       "Build OpenMW tests" OFF)
 option(PRECOMPILE_HEADERS_WITH_MSVC "Precompile most common used headers with MSVC (alternative to ccache)" ON)
 
 set(OpenGL_GL_PREFERENCE LEGACY)  # Use LEGACY as we use GL2; GLNVD is for GL3 and up.
@@ -305,7 +306,7 @@ if (OPENMW_USE_SYSTEM_YAML_CPP)
     find_package(yaml-cpp REQUIRED)
 endif()
 
-if ((BUILD_UNITTESTS OR BUILD_OPENCS_TESTS) AND OPENMW_USE_SYSTEM_GOOGLETEST)
+if ((BUILD_COMPONENTS_TESTS OR BUILD_OPENCS_TESTS OR BUILD_OPENMW_TESTS) AND OPENMW_USE_SYSTEM_GOOGLETEST)
     find_package(GTest 1.10 REQUIRED)
     find_package(GMock 1.10 REQUIRED)
 endif()
@@ -642,7 +643,7 @@ endif()
 add_subdirectory (components)
 
 # Apps and tools
-if (BUILD_OPENMW)
+if (BUILD_OPENMW OR BUILD_OPENMW_TESTS)
     add_subdirectory( apps/openmw )
 endif()
 
@@ -676,11 +677,10 @@ endif()
 
 if (BUILD_NIFTEST)
     add_subdirectory(apps/niftest)
-endif(BUILD_NIFTEST)
+endif()
 
-# UnitTests
-if (BUILD_UNITTESTS)
-    add_subdirectory( apps/openmw_test_suite )
+if (BUILD_COMPONENTS_TESTS)
+    add_subdirectory(apps/components_tests)
 endif()
 
 if (BUILD_BENCHMARKS)
@@ -692,13 +692,17 @@ if (BUILD_NAVMESHTOOL)
 endif()
 
 if (BUILD_BULLETOBJECTTOOL)
-    add_subdirectory( apps/bulletobjecttool )
+    add_subdirectory(apps/bulletobjecttool)
 endif()
 
 if (BUILD_OPENCS_TESTS)
     add_subdirectory(apps/opencs_tests)
 endif()
 
+if (BUILD_OPENMW_TESTS)
+    add_subdirectory(apps/openmw_tests)
+endif()
+
 if (WIN32)
     if (MSVC)
         foreach( OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES} )
@@ -780,8 +784,8 @@ if (WIN32)
             target_compile_options(openmw-wizard PRIVATE ${WARNINGS})
         endif()
 
-        if (BUILD_UNITTESTS)
-            target_compile_options(openmw_test_suite PRIVATE ${WARNINGS})
+        if (BUILD_COMPONENTS_TESTS)
+            target_compile_options(components-tests PRIVATE ${WARNINGS})
         endif()
 
         if (BUILD_BENCHMARKS)
@@ -795,6 +799,14 @@ if (WIN32)
         if (BUILD_BULLETOBJECTTOOL)
             target_compile_options(openmw-bulletobjecttool PRIVATE ${WARNINGS} ${MT_BUILD})
         endif()
+
+        if (BUILD_OPENCS_TESTS)
+            target_compile_options(openmw-cs-tests PRIVATE ${WARNINGS})
+        endif()
+
+        if (BUILD_OPENMW_TESTS)
+            target_compile_options(openmw-tests PRIVATE ${WARNINGS})
+        endif()
     endif(MSVC)
 
     # TODO: At some point release builds should not use the console but rather write to a log file
diff --git a/apps/openmw_test_suite/CMakeLists.txt b/apps/components_tests/CMakeLists.txt
similarity index 77%
rename from apps/openmw_test_suite/CMakeLists.txt
rename to apps/components_tests/CMakeLists.txt
index db6ecb816a..567051b16e 100644
--- a/apps/openmw_test_suite/CMakeLists.txt
+++ b/apps/components_tests/CMakeLists.txt
@@ -2,19 +2,7 @@ include_directories(SYSTEM ${GTEST_INCLUDE_DIRS})
 include_directories(SYSTEM ${GMOCK_INCLUDE_DIRS})
 
 file(GLOB UNITTEST_SRC_FILES
-    testing_util.hpp
-
-    ../openmw/mwworld/store.cpp
-    ../openmw/mwworld/esmstore.cpp
-    ../openmw/mwworld/timestamp.cpp
-
-    mwworld/test_store.cpp
-    mwworld/testduration.cpp
-    mwworld/testtimestamp.cpp
-
-    mwdialogue/test_keywordsearch.cpp
-
-    mwscript/test_scripts.cpp
+    main.cpp
 
     esm/test_fixed_string.cpp
     esm/variant.cpp
@@ -66,9 +54,6 @@ file(GLOB UNITTEST_SRC_FILES
     shader/parselinks.cpp
     shader/shadermanager.cpp
 
-    ../openmw/options.cpp
-    openmw/options.cpp
-
     sqlite3/db.cpp
     sqlite3/request.cpp
     sqlite3/statement.cpp
@@ -104,19 +89,24 @@ file(GLOB UNITTEST_SRC_FILES
     sceneutil/osgacontroller.cpp
 )
 
-source_group(apps\\openmw_test_suite FILES openmw_test_suite.cpp ${UNITTEST_SRC_FILES})
+source_group(apps\\components-tests FILES ${UNITTEST_SRC_FILES})
 
-openmw_add_executable(openmw_test_suite openmw_test_suite.cpp ${UNITTEST_SRC_FILES})
+openmw_add_executable(components-tests ${UNITTEST_SRC_FILES})
+
+target_link_libraries(components-tests
+    GTest::GTest
+    GMock::GMock
+    components
+)
 
-target_link_libraries(openmw_test_suite GTest::GTest GMock::GMock components)
 # Fix for not visible pthreads functions for linker with glibc 2.15
 if (UNIX AND NOT APPLE)
-    target_link_libraries(openmw_test_suite ${CMAKE_THREAD_LIBS_INIT})
+    target_link_libraries(components-tests ${CMAKE_THREAD_LIBS_INIT})
 endif()
 
 if (BUILD_WITH_CODE_COVERAGE)
-    target_compile_options(openmw_test_suite PRIVATE --coverage)
-    target_link_libraries(openmw_test_suite gcov)
+    target_compile_options(components-tests PRIVATE --coverage)
+    target_link_libraries(components-tests gcov)
 endif()
 
 file(DOWNLOAD
@@ -125,12 +115,12 @@ file(DOWNLOAD
     EXPECTED_HASH SHA512=6e38642bcf013c5f496a9cb0bf3ec7c9553b6e86b836e7844824c5a05f556c9391167214469b6318401684b702d7569896bf743c85aee4198612b3315ba778d6
 )
 
-target_compile_definitions(openmw_test_suite
+target_compile_definitions(components-tests
     PRIVATE OPENMW_DATA_DIR=u8"${CMAKE_CURRENT_BINARY_DIR}/data"
             OPENMW_PROJECT_SOURCE_DIR=u8"${PROJECT_SOURCE_DIR}")
 
 if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC)
-    target_precompile_headers(openmw_test_suite PRIVATE
+    target_precompile_headers(components-tests PRIVATE
         <boost/program_options/options_description.hpp>
 
         <gtest/gtest.h>
diff --git a/apps/openmw_test_suite/detournavigator/asyncnavmeshupdater.cpp b/apps/components_tests/detournavigator/asyncnavmeshupdater.cpp
similarity index 100%
rename from apps/openmw_test_suite/detournavigator/asyncnavmeshupdater.cpp
rename to apps/components_tests/detournavigator/asyncnavmeshupdater.cpp
diff --git a/apps/openmw_test_suite/detournavigator/generate.hpp b/apps/components_tests/detournavigator/generate.hpp
similarity index 100%
rename from apps/openmw_test_suite/detournavigator/generate.hpp
rename to apps/components_tests/detournavigator/generate.hpp
diff --git a/apps/openmw_test_suite/detournavigator/gettilespositions.cpp b/apps/components_tests/detournavigator/gettilespositions.cpp
similarity index 100%
rename from apps/openmw_test_suite/detournavigator/gettilespositions.cpp
rename to apps/components_tests/detournavigator/gettilespositions.cpp
diff --git a/apps/openmw_test_suite/detournavigator/navigator.cpp b/apps/components_tests/detournavigator/navigator.cpp
similarity index 100%
rename from apps/openmw_test_suite/detournavigator/navigator.cpp
rename to apps/components_tests/detournavigator/navigator.cpp
diff --git a/apps/openmw_test_suite/detournavigator/navmeshdb.cpp b/apps/components_tests/detournavigator/navmeshdb.cpp
similarity index 100%
rename from apps/openmw_test_suite/detournavigator/navmeshdb.cpp
rename to apps/components_tests/detournavigator/navmeshdb.cpp
diff --git a/apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp b/apps/components_tests/detournavigator/navmeshtilescache.cpp
similarity index 100%
rename from apps/openmw_test_suite/detournavigator/navmeshtilescache.cpp
rename to apps/components_tests/detournavigator/navmeshtilescache.cpp
diff --git a/apps/openmw_test_suite/detournavigator/operators.hpp b/apps/components_tests/detournavigator/operators.hpp
similarity index 100%
rename from apps/openmw_test_suite/detournavigator/operators.hpp
rename to apps/components_tests/detournavigator/operators.hpp
diff --git a/apps/openmw_test_suite/detournavigator/recastmeshbuilder.cpp b/apps/components_tests/detournavigator/recastmeshbuilder.cpp
similarity index 100%
rename from apps/openmw_test_suite/detournavigator/recastmeshbuilder.cpp
rename to apps/components_tests/detournavigator/recastmeshbuilder.cpp
diff --git a/apps/openmw_test_suite/detournavigator/recastmeshobject.cpp b/apps/components_tests/detournavigator/recastmeshobject.cpp
similarity index 100%
rename from apps/openmw_test_suite/detournavigator/recastmeshobject.cpp
rename to apps/components_tests/detournavigator/recastmeshobject.cpp
diff --git a/apps/openmw_test_suite/detournavigator/settings.hpp b/apps/components_tests/detournavigator/settings.hpp
similarity index 100%
rename from apps/openmw_test_suite/detournavigator/settings.hpp
rename to apps/components_tests/detournavigator/settings.hpp
diff --git a/apps/openmw_test_suite/detournavigator/settingsutils.cpp b/apps/components_tests/detournavigator/settingsutils.cpp
similarity index 100%
rename from apps/openmw_test_suite/detournavigator/settingsutils.cpp
rename to apps/components_tests/detournavigator/settingsutils.cpp
diff --git a/apps/openmw_test_suite/detournavigator/tilecachedrecastmeshmanager.cpp b/apps/components_tests/detournavigator/tilecachedrecastmeshmanager.cpp
similarity index 100%
rename from apps/openmw_test_suite/detournavigator/tilecachedrecastmeshmanager.cpp
rename to apps/components_tests/detournavigator/tilecachedrecastmeshmanager.cpp
diff --git a/apps/openmw_test_suite/esm/test_fixed_string.cpp b/apps/components_tests/esm/test_fixed_string.cpp
similarity index 100%
rename from apps/openmw_test_suite/esm/test_fixed_string.cpp
rename to apps/components_tests/esm/test_fixed_string.cpp
diff --git a/apps/openmw_test_suite/esm/testrefid.cpp b/apps/components_tests/esm/testrefid.cpp
similarity index 99%
rename from apps/openmw_test_suite/esm/testrefid.cpp
rename to apps/components_tests/esm/testrefid.cpp
index 168f9e0b9e..1911cd1a5a 100644
--- a/apps/openmw_test_suite/esm/testrefid.cpp
+++ b/apps/components_tests/esm/testrefid.cpp
@@ -1,6 +1,7 @@
-#include "components/esm/refid.hpp"
-#include "components/esm3/esmreader.hpp"
-#include "components/esm3/esmwriter.hpp"
+#include <components/esm/refid.hpp>
+#include <components/esm3/esmreader.hpp>
+#include <components/esm3/esmwriter.hpp>
+#include <components/testing/expecterror.hpp>
 
 #include <gmock/gmock.h>
 #include <gtest/gtest.h>
@@ -9,8 +10,6 @@
 #include <map>
 #include <string>
 
-#include "../testing_util.hpp"
-
 MATCHER(IsPrint, "")
 {
     return std::isprint(arg) != 0;
diff --git a/apps/openmw_test_suite/esm/variant.cpp b/apps/components_tests/esm/variant.cpp
similarity index 100%
rename from apps/openmw_test_suite/esm/variant.cpp
rename to apps/components_tests/esm/variant.cpp
diff --git a/apps/openmw_test_suite/esm3/readerscache.cpp b/apps/components_tests/esm3/readerscache.cpp
similarity index 100%
rename from apps/openmw_test_suite/esm3/readerscache.cpp
rename to apps/components_tests/esm3/readerscache.cpp
diff --git a/apps/openmw_test_suite/esm3/testesmwriter.cpp b/apps/components_tests/esm3/testesmwriter.cpp
similarity index 100%
rename from apps/openmw_test_suite/esm3/testesmwriter.cpp
rename to apps/components_tests/esm3/testesmwriter.cpp
diff --git a/apps/openmw_test_suite/esm3/testinfoorder.cpp b/apps/components_tests/esm3/testinfoorder.cpp
similarity index 100%
rename from apps/openmw_test_suite/esm3/testinfoorder.cpp
rename to apps/components_tests/esm3/testinfoorder.cpp
diff --git a/apps/openmw_test_suite/esm3/testsaveload.cpp b/apps/components_tests/esm3/testsaveload.cpp
similarity index 100%
rename from apps/openmw_test_suite/esm3/testsaveload.cpp
rename to apps/components_tests/esm3/testsaveload.cpp
diff --git a/apps/openmw_test_suite/esm4/includes.cpp b/apps/components_tests/esm4/includes.cpp
similarity index 100%
rename from apps/openmw_test_suite/esm4/includes.cpp
rename to apps/components_tests/esm4/includes.cpp
diff --git a/apps/openmw_test_suite/esmloader/esmdata.cpp b/apps/components_tests/esmloader/esmdata.cpp
similarity index 100%
rename from apps/openmw_test_suite/esmloader/esmdata.cpp
rename to apps/components_tests/esmloader/esmdata.cpp
diff --git a/apps/openmw_test_suite/esmloader/load.cpp b/apps/components_tests/esmloader/load.cpp
similarity index 100%
rename from apps/openmw_test_suite/esmloader/load.cpp
rename to apps/components_tests/esmloader/load.cpp
diff --git a/apps/openmw_test_suite/esmloader/record.cpp b/apps/components_tests/esmloader/record.cpp
similarity index 100%
rename from apps/openmw_test_suite/esmloader/record.cpp
rename to apps/components_tests/esmloader/record.cpp
diff --git a/apps/openmw_test_suite/esmterrain/testgridsampling.cpp b/apps/components_tests/esmterrain/testgridsampling.cpp
similarity index 100%
rename from apps/openmw_test_suite/esmterrain/testgridsampling.cpp
rename to apps/components_tests/esmterrain/testgridsampling.cpp
diff --git a/apps/openmw_test_suite/files/conversion_tests.cpp b/apps/components_tests/files/conversion_tests.cpp
similarity index 100%
rename from apps/openmw_test_suite/files/conversion_tests.cpp
rename to apps/components_tests/files/conversion_tests.cpp
diff --git a/apps/openmw_test_suite/files/hash.cpp b/apps/components_tests/files/hash.cpp
similarity index 98%
rename from apps/openmw_test_suite/files/hash.cpp
rename to apps/components_tests/files/hash.cpp
index 32c8380422..793965112b 100644
--- a/apps/openmw_test_suite/files/hash.cpp
+++ b/apps/components_tests/files/hash.cpp
@@ -1,5 +1,7 @@
 #include <components/files/constrainedfilestream.hpp>
+#include <components/files/conversion.hpp>
 #include <components/files/hash.hpp>
+#include <components/testing/util.hpp>
 
 #include <gmock/gmock.h>
 #include <gtest/gtest.h>
@@ -10,10 +12,6 @@
 #include <sstream>
 #include <string>
 
-#include <components/files/conversion.hpp>
-
-#include "../testing_util.hpp"
-
 namespace
 {
     using namespace testing;
diff --git a/apps/openmw_test_suite/fx/lexer.cpp b/apps/components_tests/fx/lexer.cpp
similarity index 100%
rename from apps/openmw_test_suite/fx/lexer.cpp
rename to apps/components_tests/fx/lexer.cpp
diff --git a/apps/openmw_test_suite/fx/technique.cpp b/apps/components_tests/fx/technique.cpp
similarity index 99%
rename from apps/openmw_test_suite/fx/technique.cpp
rename to apps/components_tests/fx/technique.cpp
index 352dd9ca09..4a74ace7a8 100644
--- a/apps/openmw_test_suite/fx/technique.cpp
+++ b/apps/components_tests/fx/technique.cpp
@@ -1,12 +1,11 @@
-#include "gmock/gmock.h"
+#include <gmock/gmock.h>
 #include <gtest/gtest.h>
 
 #include <components/files/configurationmanager.hpp>
 #include <components/fx/technique.hpp>
 #include <components/resource/imagemanager.hpp>
 #include <components/settings/settings.hpp>
-
-#include "../testing_util.hpp"
+#include <components/testing/util.hpp>
 
 namespace
 {
diff --git a/apps/openmw_test_suite/lua/test_async.cpp b/apps/components_tests/lua/test_async.cpp
similarity index 94%
rename from apps/openmw_test_suite/lua/test_async.cpp
rename to apps/components_tests/lua/test_async.cpp
index aa4059b632..e4010e319f 100644
--- a/apps/openmw_test_suite/lua/test_async.cpp
+++ b/apps/components_tests/lua/test_async.cpp
@@ -1,15 +1,13 @@
-#include "gmock/gmock.h"
+#include <gmock/gmock.h>
 #include <gtest/gtest.h>
 
 #include <components/lua/asyncpackage.hpp>
 #include <components/lua/luastate.hpp>
-
-#include "../testing_util.hpp"
+#include <components/testing/expecterror.hpp>
 
 namespace
 {
     using namespace testing;
-    using namespace TestingOpenMW;
 
     struct LuaCoroutineCallbackTest : Test
     {
diff --git a/apps/openmw_test_suite/lua/test_configuration.cpp b/apps/components_tests/lua/test_configuration.cpp
similarity index 99%
rename from apps/openmw_test_suite/lua/test_configuration.cpp
rename to apps/components_tests/lua/test_configuration.cpp
index 49fc93a3d8..4d48927ecc 100644
--- a/apps/openmw_test_suite/lua/test_configuration.cpp
+++ b/apps/components_tests/lua/test_configuration.cpp
@@ -1,4 +1,4 @@
-#include "gmock/gmock.h"
+#include <gmock/gmock.h>
 #include <gtest/gtest.h>
 
 #include <fstream>
@@ -9,8 +9,8 @@
 #include <components/esm3/readerscache.hpp>
 #include <components/lua/configuration.hpp>
 #include <components/lua/serialization.hpp>
-
-#include "../testing_util.hpp"
+#include <components/testing/expecterror.hpp>
+#include <components/testing/util.hpp>
 
 namespace
 {
diff --git a/apps/openmw_test_suite/lua/test_inputactions.cpp b/apps/components_tests/lua/test_inputactions.cpp
similarity index 97%
rename from apps/openmw_test_suite/lua/test_inputactions.cpp
rename to apps/components_tests/lua/test_inputactions.cpp
index 5bdd39ada1..cad17a5b99 100644
--- a/apps/openmw_test_suite/lua/test_inputactions.cpp
+++ b/apps/components_tests/lua/test_inputactions.cpp
@@ -1,10 +1,9 @@
-#include "gmock/gmock.h"
+#include <gmock/gmock.h>
 #include <gtest/gtest.h>
 
 #include <components/lua/inputactions.hpp>
 #include <components/lua/scriptscontainer.hpp>
-
-#include "../testing_util.hpp"
+#include <components/testing/util.hpp>
 
 namespace
 {
diff --git a/apps/openmw_test_suite/lua/test_l10n.cpp b/apps/components_tests/lua/test_l10n.cpp
similarity index 99%
rename from apps/openmw_test_suite/lua/test_l10n.cpp
rename to apps/components_tests/lua/test_l10n.cpp
index 835492f13c..a9194ffd7b 100644
--- a/apps/openmw_test_suite/lua/test_l10n.cpp
+++ b/apps/components_tests/lua/test_l10n.cpp
@@ -1,13 +1,11 @@
-#include "gmock/gmock.h"
+#include <gmock/gmock.h>
 #include <gtest/gtest.h>
 
 #include <components/files/fixedpath.hpp>
-
 #include <components/l10n/manager.hpp>
 #include <components/lua/l10n.hpp>
 #include <components/lua/luastate.hpp>
-
-#include "../testing_util.hpp"
+#include <components/testing/util.hpp>
 
 namespace
 {
diff --git a/apps/openmw_test_suite/lua/test_lua.cpp b/apps/components_tests/lua/test_lua.cpp
similarity index 98%
rename from apps/openmw_test_suite/lua/test_lua.cpp
rename to apps/components_tests/lua/test_lua.cpp
index a669a3c670..09f0151267 100644
--- a/apps/openmw_test_suite/lua/test_lua.cpp
+++ b/apps/components_tests/lua/test_lua.cpp
@@ -1,9 +1,9 @@
-#include "gmock/gmock.h"
+#include <gmock/gmock.h>
 #include <gtest/gtest.h>
 
 #include <components/lua/luastate.hpp>
-
-#include "../testing_util.hpp"
+#include <components/testing/expecterror.hpp>
+#include <components/testing/util.hpp>
 
 namespace
 {
diff --git a/apps/openmw_test_suite/lua/test_scriptscontainer.cpp b/apps/components_tests/lua/test_scriptscontainer.cpp
similarity index 99%
rename from apps/openmw_test_suite/lua/test_scriptscontainer.cpp
rename to apps/components_tests/lua/test_scriptscontainer.cpp
index dc99caefda..6e562e7541 100644
--- a/apps/openmw_test_suite/lua/test_scriptscontainer.cpp
+++ b/apps/components_tests/lua/test_scriptscontainer.cpp
@@ -1,4 +1,4 @@
-#include "gmock/gmock.h"
+#include <gmock/gmock.h>
 #include <gtest/gtest.h>
 
 #include <components/esm/luascripts.hpp>
@@ -7,7 +7,7 @@
 #include <components/lua/luastate.hpp>
 #include <components/lua/scriptscontainer.hpp>
 
-#include "../testing_util.hpp"
+#include <components/testing/util.hpp>
 
 namespace
 {
diff --git a/apps/openmw_test_suite/lua/test_serialization.cpp b/apps/components_tests/lua/test_serialization.cpp
similarity index 99%
rename from apps/openmw_test_suite/lua/test_serialization.cpp
rename to apps/components_tests/lua/test_serialization.cpp
index 56a6210b28..cff41dde9a 100644
--- a/apps/openmw_test_suite/lua/test_serialization.cpp
+++ b/apps/components_tests/lua/test_serialization.cpp
@@ -1,4 +1,4 @@
-#include "gmock/gmock.h"
+#include <gmock/gmock.h>
 #include <gtest/gtest.h>
 
 #include <osg/Matrixf>
@@ -13,7 +13,7 @@
 #include <components/misc/color.hpp>
 #include <components/misc/endianness.hpp>
 
-#include "../testing_util.hpp"
+#include <components/testing/expecterror.hpp>
 
 namespace
 {
diff --git a/apps/openmw_test_suite/lua/test_storage.cpp b/apps/components_tests/lua/test_storage.cpp
similarity index 100%
rename from apps/openmw_test_suite/lua/test_storage.cpp
rename to apps/components_tests/lua/test_storage.cpp
diff --git a/apps/openmw_test_suite/lua/test_ui_content.cpp b/apps/components_tests/lua/test_ui_content.cpp
similarity index 100%
rename from apps/openmw_test_suite/lua/test_ui_content.cpp
rename to apps/components_tests/lua/test_ui_content.cpp
diff --git a/apps/openmw_test_suite/lua/test_utilpackage.cpp b/apps/components_tests/lua/test_utilpackage.cpp
similarity index 99%
rename from apps/openmw_test_suite/lua/test_utilpackage.cpp
rename to apps/components_tests/lua/test_utilpackage.cpp
index 26bdf3408b..3eb22a9a46 100644
--- a/apps/openmw_test_suite/lua/test_utilpackage.cpp
+++ b/apps/components_tests/lua/test_utilpackage.cpp
@@ -1,10 +1,9 @@
-#include "gmock/gmock.h"
+#include <gmock/gmock.h>
 #include <gtest/gtest.h>
 
 #include <components/lua/luastate.hpp>
 #include <components/lua/utilpackage.hpp>
-
-#include "../testing_util.hpp"
+#include <components/testing/expecterror.hpp>
 
 namespace
 {
diff --git a/apps/openmw_test_suite/lua/test_yaml.cpp b/apps/components_tests/lua/test_yaml.cpp
similarity index 100%
rename from apps/openmw_test_suite/lua/test_yaml.cpp
rename to apps/components_tests/lua/test_yaml.cpp
diff --git a/apps/openmw_test_suite/openmw_test_suite.cpp b/apps/components_tests/main.cpp
similarity index 75%
rename from apps/openmw_test_suite/openmw_test_suite.cpp
rename to apps/components_tests/main.cpp
index a2b9f1ae73..e3c01d6982 100644
--- a/apps/openmw_test_suite/openmw_test_suite.cpp
+++ b/apps/components_tests/main.cpp
@@ -6,15 +6,8 @@
 
 #include <filesystem>
 
-#ifdef WIN32
-// we cannot use GTEST_API_ before main if we're building standalone exe application,
-// and we're linking GoogleTest / GoogleMock as DLLs and not linking gtest_main / gmock_main
 int main(int argc, char** argv)
 {
-#else
-GTEST_API_ int main(int argc, char** argv)
-{
-#endif
     const std::filesystem::path settingsDefaultPath = std::filesystem::path{ OPENMW_PROJECT_SOURCE_DIR } / "files"
         / Misc::StringUtils::stringToU8String("settings-default.cfg");
 
diff --git a/apps/openmw_test_suite/misc/compression.cpp b/apps/components_tests/misc/compression.cpp
similarity index 100%
rename from apps/openmw_test_suite/misc/compression.cpp
rename to apps/components_tests/misc/compression.cpp
diff --git a/apps/openmw_test_suite/misc/progressreporter.cpp b/apps/components_tests/misc/progressreporter.cpp
similarity index 100%
rename from apps/openmw_test_suite/misc/progressreporter.cpp
rename to apps/components_tests/misc/progressreporter.cpp
diff --git a/apps/openmw_test_suite/misc/test_endianness.cpp b/apps/components_tests/misc/test_endianness.cpp
similarity index 100%
rename from apps/openmw_test_suite/misc/test_endianness.cpp
rename to apps/components_tests/misc/test_endianness.cpp
diff --git a/apps/openmw_test_suite/misc/test_resourcehelpers.cpp b/apps/components_tests/misc/test_resourcehelpers.cpp
similarity index 96%
rename from apps/openmw_test_suite/misc/test_resourcehelpers.cpp
rename to apps/components_tests/misc/test_resourcehelpers.cpp
index 48edb72578..964442e735 100644
--- a/apps/openmw_test_suite/misc/test_resourcehelpers.cpp
+++ b/apps/components_tests/misc/test_resourcehelpers.cpp
@@ -1,5 +1,6 @@
-#include "../testing_util.hpp"
-#include "components/misc/resourcehelpers.hpp"
+#include <components/misc/resourcehelpers.hpp>
+#include <components/testing/util.hpp>
+
 #include <gtest/gtest.h>
 
 namespace
diff --git a/apps/openmw_test_suite/misc/test_stringops.cpp b/apps/components_tests/misc/test_stringops.cpp
similarity index 100%
rename from apps/openmw_test_suite/misc/test_stringops.cpp
rename to apps/components_tests/misc/test_stringops.cpp
diff --git a/apps/openmw_test_suite/nif/node.hpp b/apps/components_tests/nif/node.hpp
similarity index 100%
rename from apps/openmw_test_suite/nif/node.hpp
rename to apps/components_tests/nif/node.hpp
diff --git a/apps/openmw_test_suite/nifloader/testbulletnifloader.cpp b/apps/components_tests/nifloader/testbulletnifloader.cpp
similarity index 100%
rename from apps/openmw_test_suite/nifloader/testbulletnifloader.cpp
rename to apps/components_tests/nifloader/testbulletnifloader.cpp
diff --git a/apps/openmw_test_suite/nifosg/testnifloader.cpp b/apps/components_tests/nifosg/testnifloader.cpp
similarity index 100%
rename from apps/openmw_test_suite/nifosg/testnifloader.cpp
rename to apps/components_tests/nifosg/testnifloader.cpp
diff --git a/apps/openmw_test_suite/resource/testobjectcache.cpp b/apps/components_tests/resource/testobjectcache.cpp
similarity index 100%
rename from apps/openmw_test_suite/resource/testobjectcache.cpp
rename to apps/components_tests/resource/testobjectcache.cpp
diff --git a/apps/openmw_test_suite/sceneutil/osgacontroller.cpp b/apps/components_tests/sceneutil/osgacontroller.cpp
similarity index 100%
rename from apps/openmw_test_suite/sceneutil/osgacontroller.cpp
rename to apps/components_tests/sceneutil/osgacontroller.cpp
diff --git a/apps/openmw_test_suite/serialization/binaryreader.cpp b/apps/components_tests/serialization/binaryreader.cpp
similarity index 100%
rename from apps/openmw_test_suite/serialization/binaryreader.cpp
rename to apps/components_tests/serialization/binaryreader.cpp
diff --git a/apps/openmw_test_suite/serialization/binarywriter.cpp b/apps/components_tests/serialization/binarywriter.cpp
similarity index 100%
rename from apps/openmw_test_suite/serialization/binarywriter.cpp
rename to apps/components_tests/serialization/binarywriter.cpp
diff --git a/apps/openmw_test_suite/serialization/format.hpp b/apps/components_tests/serialization/format.hpp
similarity index 100%
rename from apps/openmw_test_suite/serialization/format.hpp
rename to apps/components_tests/serialization/format.hpp
diff --git a/apps/openmw_test_suite/serialization/integration.cpp b/apps/components_tests/serialization/integration.cpp
similarity index 100%
rename from apps/openmw_test_suite/serialization/integration.cpp
rename to apps/components_tests/serialization/integration.cpp
diff --git a/apps/openmw_test_suite/serialization/sizeaccumulator.cpp b/apps/components_tests/serialization/sizeaccumulator.cpp
similarity index 100%
rename from apps/openmw_test_suite/serialization/sizeaccumulator.cpp
rename to apps/components_tests/serialization/sizeaccumulator.cpp
diff --git a/apps/openmw_test_suite/settings/parser.cpp b/apps/components_tests/settings/parser.cpp
similarity index 99%
rename from apps/openmw_test_suite/settings/parser.cpp
rename to apps/components_tests/settings/parser.cpp
index 3712ca6513..af514dbdd7 100644
--- a/apps/openmw_test_suite/settings/parser.cpp
+++ b/apps/components_tests/settings/parser.cpp
@@ -1,11 +1,10 @@
 #include <components/settings/parser.hpp>
+#include <components/testing/util.hpp>
 
 #include <fstream>
 
 #include <gtest/gtest.h>
 
-#include "../testing_util.hpp"
-
 namespace
 {
     using namespace testing;
diff --git a/apps/openmw_test_suite/settings/shadermanager.cpp b/apps/components_tests/settings/shadermanager.cpp
similarity index 98%
rename from apps/openmw_test_suite/settings/shadermanager.cpp
rename to apps/components_tests/settings/shadermanager.cpp
index c252e08fc6..c3ba5084c2 100644
--- a/apps/openmw_test_suite/settings/shadermanager.cpp
+++ b/apps/components_tests/settings/shadermanager.cpp
@@ -1,12 +1,11 @@
 #include <components/settings/shadermanager.hpp>
+#include <components/testing/util.hpp>
 
 #include <filesystem>
 #include <fstream>
 
 #include <gtest/gtest.h>
 
-#include "../testing_util.hpp"
-
 namespace
 {
     using namespace testing;
diff --git a/apps/openmw_test_suite/settings/testvalues.cpp b/apps/components_tests/settings/testvalues.cpp
similarity index 100%
rename from apps/openmw_test_suite/settings/testvalues.cpp
rename to apps/components_tests/settings/testvalues.cpp
diff --git a/apps/openmw_test_suite/shader/parsedefines.cpp b/apps/components_tests/shader/parsedefines.cpp
similarity index 100%
rename from apps/openmw_test_suite/shader/parsedefines.cpp
rename to apps/components_tests/shader/parsedefines.cpp
diff --git a/apps/openmw_test_suite/shader/parsefors.cpp b/apps/components_tests/shader/parsefors.cpp
similarity index 100%
rename from apps/openmw_test_suite/shader/parsefors.cpp
rename to apps/components_tests/shader/parsefors.cpp
diff --git a/apps/openmw_test_suite/shader/parselinks.cpp b/apps/components_tests/shader/parselinks.cpp
similarity index 100%
rename from apps/openmw_test_suite/shader/parselinks.cpp
rename to apps/components_tests/shader/parselinks.cpp
diff --git a/apps/openmw_test_suite/shader/shadermanager.cpp b/apps/components_tests/shader/shadermanager.cpp
similarity index 99%
rename from apps/openmw_test_suite/shader/shadermanager.cpp
rename to apps/components_tests/shader/shadermanager.cpp
index 3d7eaecf00..5b11d31a44 100644
--- a/apps/openmw_test_suite/shader/shadermanager.cpp
+++ b/apps/components_tests/shader/shadermanager.cpp
@@ -1,12 +1,11 @@
 #include <components/files/conversion.hpp>
 #include <components/shader/shadermanager.hpp>
+#include <components/testing/util.hpp>
 
 #include <fstream>
 
 #include <gtest/gtest.h>
 
-#include "../testing_util.hpp"
-
 namespace
 {
     using namespace testing;
diff --git a/apps/openmw_test_suite/sqlite3/db.cpp b/apps/components_tests/sqlite3/db.cpp
similarity index 100%
rename from apps/openmw_test_suite/sqlite3/db.cpp
rename to apps/components_tests/sqlite3/db.cpp
diff --git a/apps/openmw_test_suite/sqlite3/request.cpp b/apps/components_tests/sqlite3/request.cpp
similarity index 100%
rename from apps/openmw_test_suite/sqlite3/request.cpp
rename to apps/components_tests/sqlite3/request.cpp
diff --git a/apps/openmw_test_suite/sqlite3/statement.cpp b/apps/components_tests/sqlite3/statement.cpp
similarity index 100%
rename from apps/openmw_test_suite/sqlite3/statement.cpp
rename to apps/components_tests/sqlite3/statement.cpp
diff --git a/apps/openmw_test_suite/sqlite3/transaction.cpp b/apps/components_tests/sqlite3/transaction.cpp
similarity index 100%
rename from apps/openmw_test_suite/sqlite3/transaction.cpp
rename to apps/components_tests/sqlite3/transaction.cpp
diff --git a/apps/openmw_test_suite/toutf8/data/french-utf8.txt b/apps/components_tests/toutf8/data/french-utf8.txt
similarity index 100%
rename from apps/openmw_test_suite/toutf8/data/french-utf8.txt
rename to apps/components_tests/toutf8/data/french-utf8.txt
diff --git a/apps/openmw_test_suite/toutf8/data/french-win1252.txt b/apps/components_tests/toutf8/data/french-win1252.txt
similarity index 100%
rename from apps/openmw_test_suite/toutf8/data/french-win1252.txt
rename to apps/components_tests/toutf8/data/french-win1252.txt
diff --git a/apps/openmw_test_suite/toutf8/data/russian-utf8.txt b/apps/components_tests/toutf8/data/russian-utf8.txt
similarity index 100%
rename from apps/openmw_test_suite/toutf8/data/russian-utf8.txt
rename to apps/components_tests/toutf8/data/russian-utf8.txt
diff --git a/apps/openmw_test_suite/toutf8/data/russian-win1251.txt b/apps/components_tests/toutf8/data/russian-win1251.txt
similarity index 100%
rename from apps/openmw_test_suite/toutf8/data/russian-win1251.txt
rename to apps/components_tests/toutf8/data/russian-win1251.txt
diff --git a/apps/openmw_test_suite/toutf8/toutf8.cpp b/apps/components_tests/toutf8/toutf8.cpp
similarity index 99%
rename from apps/openmw_test_suite/toutf8/toutf8.cpp
rename to apps/components_tests/toutf8/toutf8.cpp
index 9a259c69ab..704ee6742d 100644
--- a/apps/openmw_test_suite/toutf8/toutf8.cpp
+++ b/apps/components_tests/toutf8/toutf8.cpp
@@ -26,7 +26,7 @@ namespace
     {
         std::ifstream file;
         file.exceptions(std::ios::failbit | std::ios::badbit);
-        file.open(std::filesystem::path{ OPENMW_PROJECT_SOURCE_DIR } / "apps" / "openmw_test_suite" / "toutf8" / "data"
+        file.open(std::filesystem::path{ OPENMW_PROJECT_SOURCE_DIR } / "apps" / "components_tests" / "toutf8" / "data"
             / Misc::StringUtils::stringToU8String(fileName));
         std::stringstream buffer;
         buffer << file.rdbuf();
diff --git a/apps/openmw_test_suite/vfs/testpathutil.cpp b/apps/components_tests/vfs/testpathutil.cpp
similarity index 100%
rename from apps/openmw_test_suite/vfs/testpathutil.cpp
rename to apps/components_tests/vfs/testpathutil.cpp
diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt
index d10882e0ae..80aa04d42c 100644
--- a/apps/opencs/CMakeLists.txt
+++ b/apps/opencs/CMakeLists.txt
@@ -196,8 +196,8 @@ if(BUILD_OPENCS)
     target_link_libraries(openmw-cs openmw-cs-lib)
 
     if (BUILD_WITH_CODE_COVERAGE)
-        target_compile_options(openmw-cs-lib PRIVATE --coverage)
-        target_link_libraries(openmw-cs-lib gcov)
+        target_compile_options(openmw-cs PRIVATE --coverage)
+        target_link_libraries(openmw-cs gcov)
     endif()
 endif()
 
diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt
index b1d17b0a61..c2293c9228 100644
--- a/apps/openmw/CMakeLists.txt
+++ b/apps/openmw/CMakeLists.txt
@@ -1,22 +1,25 @@
-# local files
-set(GAME
-    main.cpp
+set(OPENMW_SOURCES
     engine.cpp
     options.cpp
+)
 
+set(OPENMW_RESOURCES
     ${CMAKE_SOURCE_DIR}/files/windows/openmw.rc
     ${CMAKE_SOURCE_DIR}/files/windows/openmw.exe.manifest
 )
 
 if (ANDROID)
-    set(GAME ${GAME} android_main.cpp)
+    set(OPENMW_SOURCES ${OPENMW_SOURCES} android_main.cpp)
 endif()
 
-set(GAME_HEADER
+set(OPENMW_HEADERS
+    doc.hpp
     engine.hpp
+    options.hpp
+    profile.hpp
 )
 
-source_group(game FILES ${GAME} ${GAME_HEADER})
+source_group(apps/openmw FILES main.cpp ${OPENMW_SOURCES} ${OPENMW_HEADERS} ${OPENMW_RESOURCES})
 
 add_openmw_dir (mwrender
     actors objects renderingmanager animation rotatecontroller sky skyutil npcanimation esm4npcanimation vismask
@@ -120,26 +123,41 @@ add_openmw_dir (mwbase
 # Main executable
 
 if (NOT ANDROID)
-    openmw_add_executable(openmw
+    add_library(openmw-lib STATIC
         ${OPENMW_FILES}
-        ${GAME} ${GAME_HEADER}
-        ${APPLE_BUNDLE_RESOURCES}
-    )
-else ()
-    add_library(openmw
-        SHARED
-        ${OPENMW_FILES}
-        ${GAME} ${GAME_HEADER}
+        ${OPENMW_SOURCES}
     )
+
+    # Otherwise linker fails with LNK1149 because main.cpp has __declspec(dllexport)
+    if(NOT WIN32)
+        set_target_properties(openmw-lib PROPERTIES OUTPUT_NAME openmw)
+    endif()
 endif ()
 
+if(BUILD_OPENMW)
+    if (ANDROID)
+        add_library(openmw-lib SHARED
+            ${OPENMW_FILES}
+            ${OPENMW_SOURCES}
+        )
+    else()
+        openmw_add_executable(openmw
+            ${APPLE_BUNDLE_RESOURCES}
+            ${OPENMW_RESOURCES}
+            main.cpp
+        )
+
+        target_link_libraries(openmw openmw-lib)
+    endif()
+endif()
+
 # Sound stuff - here so CMake doesn't stupidly recompile EVERYTHING
 # when we change the backend.
 include_directories(
     ${FFmpeg_INCLUDE_DIRS}
 )
 
-target_link_libraries(openmw
+target_link_libraries(openmw-lib
     # CMake's built-in OSG finder does not use pkgconfig, so we have to
     # manually ensure the order is correct for inter-library dependencies.
     # This only makes a difference with `-DOPENMW_USE_SYSTEM_OSG=ON -DOSG_STATIC=ON`.
@@ -163,7 +181,7 @@ target_link_libraries(openmw
 )
 
 if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC)
-    target_precompile_headers(openmw PRIVATE
+    target_precompile_headers(openmw-lib PRIVATE
         <boost/program_options/options_description.hpp>
 
         <sol/sol.hpp>
@@ -191,23 +209,23 @@ endif()
 add_definitions(-DMYGUI_DONT_USE_OBSOLETE=ON)
 
 if (ANDROID)
-    target_link_libraries(openmw EGL android log z)
+    target_link_libraries(openmw-lib EGL android log z)
 endif (ANDROID)
 
 if (USE_SYSTEM_TINYXML)
-    target_link_libraries(openmw ${TinyXML_LIBRARIES})
+    target_link_libraries(openmw-lib ${TinyXML_LIBRARIES})
 endif()
 
 if (NOT UNIX)
-    target_link_libraries(openmw ${SDL2MAIN_LIBRARY})
+    target_link_libraries(openmw-lib ${SDL2MAIN_LIBRARY})
 endif()
 
 # Fix for not visible pthreads functions for linker with glibc 2.15
 if (UNIX AND NOT APPLE)
-    target_link_libraries(openmw ${CMAKE_THREAD_LIBS_INIT})
+    target_link_libraries(openmw-lib ${CMAKE_THREAD_LIBS_INIT})
 endif()
 
-if(APPLE)
+if(APPLE AND BUILD_OPENMW)
     set(BUNDLE_RESOURCES_DIR "${APP_BUNDLE_DIR}/Contents/Resources")
 
     set(OPENMW_RESOURCES_ROOT ${BUNDLE_RESOURCES_DIR})
@@ -236,10 +254,14 @@ if(APPLE)
 endif(APPLE)
 
 if (BUILD_WITH_CODE_COVERAGE)
-    target_compile_options(openmw PRIVATE --coverage)
-    target_link_libraries(openmw gcov)
+    target_compile_options(openmw-lib PRIVATE --coverage)
+    target_link_libraries(openmw-lib gcov)
+    if (NOT ANDROID AND BUILD_OPENMW)
+        target_compile_options(openmw PRIVATE --coverage)
+        target_link_libraries(openmw gcov)
+    endif()
 endif()
 
-if (WIN32)
+if (WIN32 AND BUILD_OPENMW)
     INSTALL(TARGETS openmw RUNTIME DESTINATION ".")
 endif (WIN32)
diff --git a/apps/openmw_tests/CMakeLists.txt b/apps/openmw_tests/CMakeLists.txt
new file mode 100644
index 0000000000..0c6b0d84df
--- /dev/null
+++ b/apps/openmw_tests/CMakeLists.txt
@@ -0,0 +1,58 @@
+include_directories(SYSTEM ${GTEST_INCLUDE_DIRS})
+include_directories(SYSTEM ${GMOCK_INCLUDE_DIRS})
+
+file(GLOB UNITTEST_SRC_FILES
+    main.cpp
+
+    options.cpp
+
+    mwworld/test_store.cpp
+    mwworld/testduration.cpp
+    mwworld/testtimestamp.cpp
+
+    mwdialogue/test_keywordsearch.cpp
+
+    mwscript/test_scripts.cpp
+)
+
+source_group(apps\\openmw-tests FILES ${UNITTEST_SRC_FILES})
+
+openmw_add_executable(openmw-tests ${UNITTEST_SRC_FILES})
+
+target_link_libraries(openmw-tests
+    GTest::GTest
+    GMock::GMock
+    openmw-lib
+)
+
+# Fix for not visible pthreads functions for linker with glibc 2.15
+if (UNIX AND NOT APPLE)
+    target_link_libraries(openmw-tests ${CMAKE_THREAD_LIBS_INIT})
+endif()
+
+if (BUILD_WITH_CODE_COVERAGE)
+    target_compile_options(openmw-tests PRIVATE --coverage)
+    target_link_libraries(openmw-tests gcov)
+endif()
+
+target_compile_definitions(openmw-tests
+    PRIVATE OPENMW_DATA_DIR=u8"${CMAKE_CURRENT_BINARY_DIR}/data"
+            OPENMW_PROJECT_SOURCE_DIR=u8"${PROJECT_SOURCE_DIR}")
+
+if (MSVC AND PRECOMPILE_HEADERS_WITH_MSVC)
+    target_precompile_headers(openmw-tests PRIVATE
+        <boost/program_options/options_description.hpp>
+
+        <gtest/gtest.h>
+
+        <sol/sol.hpp>
+
+        <algorithm>
+        <filesystem>
+        <fstream>
+        <functional>
+        <memory>
+        <string>
+        <vector>
+    )
+endif()
diff --git a/apps/openmw_tests/main.cpp b/apps/openmw_tests/main.cpp
new file mode 100644
index 0000000000..e1a8e67397
--- /dev/null
+++ b/apps/openmw_tests/main.cpp
@@ -0,0 +1,7 @@
+#include <gtest/gtest.h>
+
+int main(int argc, char* argv[])
+{
+    testing::InitGoogleTest(&argc, argv);
+    return RUN_ALL_TESTS();
+}
diff --git a/apps/openmw_test_suite/mwdialogue/test_keywordsearch.cpp b/apps/openmw_tests/mwdialogue/test_keywordsearch.cpp
similarity index 100%
rename from apps/openmw_test_suite/mwdialogue/test_keywordsearch.cpp
rename to apps/openmw_tests/mwdialogue/test_keywordsearch.cpp
diff --git a/apps/openmw_test_suite/mwscript/test_scripts.cpp b/apps/openmw_tests/mwscript/test_scripts.cpp
similarity index 100%
rename from apps/openmw_test_suite/mwscript/test_scripts.cpp
rename to apps/openmw_tests/mwscript/test_scripts.cpp
diff --git a/apps/openmw_test_suite/mwscript/test_utils.hpp b/apps/openmw_tests/mwscript/test_utils.hpp
similarity index 100%
rename from apps/openmw_test_suite/mwscript/test_utils.hpp
rename to apps/openmw_tests/mwscript/test_utils.hpp
diff --git a/apps/openmw_test_suite/mwworld/test_store.cpp b/apps/openmw_tests/mwworld/test_store.cpp
similarity index 99%
rename from apps/openmw_test_suite/mwworld/test_store.cpp
rename to apps/openmw_tests/mwworld/test_store.cpp
index 27010ef851..ba48193836 100644
--- a/apps/openmw_test_suite/mwworld/test_store.cpp
+++ b/apps/openmw_tests/mwworld/test_store.cpp
@@ -22,21 +22,10 @@
 #include <components/files/conversion.hpp>
 #include <components/loadinglistener/loadinglistener.hpp>
 #include <components/misc/strings/algorithm.hpp>
+#include <components/testing/util.hpp>
 
-#include "apps/openmw/mwmechanics/spelllist.hpp"
 #include "apps/openmw/mwworld/esmstore.hpp"
 
-#include "../testing_util.hpp"
-
-namespace MWMechanics
-{
-    SpellList::SpellList(const ESM::RefId& id, int type)
-        : mId(id)
-        , mType(type)
-    {
-    }
-}
-
 static Loading::Listener dummyListener;
 
 /// Base class for tests of ESMStore that rely on external content files to produce the test results
diff --git a/apps/openmw_test_suite/mwworld/testduration.cpp b/apps/openmw_tests/mwworld/testduration.cpp
similarity index 100%
rename from apps/openmw_test_suite/mwworld/testduration.cpp
rename to apps/openmw_tests/mwworld/testduration.cpp
diff --git a/apps/openmw_test_suite/mwworld/testtimestamp.cpp b/apps/openmw_tests/mwworld/testtimestamp.cpp
similarity index 100%
rename from apps/openmw_test_suite/mwworld/testtimestamp.cpp
rename to apps/openmw_tests/mwworld/testtimestamp.cpp
diff --git a/apps/openmw_test_suite/openmw/options.cpp b/apps/openmw_tests/options.cpp
similarity index 100%
rename from apps/openmw_test_suite/openmw/options.cpp
rename to apps/openmw_tests/options.cpp
diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt
index f669bfdd04..fb5718a979 100644
--- a/components/CMakeLists.txt
+++ b/components/CMakeLists.txt
@@ -512,6 +512,11 @@ else ()
         )
 endif()
 
+add_component_dir(testing
+    expecterror
+    util
+)
+
 set (ESM_UI ${CMAKE_CURRENT_SOURCE_DIR}/contentselector/contentselector.ui
     )
 
diff --git a/components/testing/expecterror.hpp b/components/testing/expecterror.hpp
new file mode 100644
index 0000000000..abd59124af
--- /dev/null
+++ b/components/testing/expecterror.hpp
@@ -0,0 +1,20 @@
+#ifndef OPENMW_COMPONENTS_TESTING_EXPECTERROR_H
+#define OPENMW_COMPONENTS_TESTING_EXPECTERROR_H
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#include <exception>
+
+#define EXPECT_ERROR(X, ERR_SUBSTR)                                                                                    \
+    try                                                                                                                \
+    {                                                                                                                  \
+        X;                                                                                                             \
+        FAIL() << "Expected error";                                                                                    \
+    }                                                                                                                  \
+    catch (const std::exception& e)                                                                                    \
+    {                                                                                                                  \
+        EXPECT_THAT(e.what(), ::testing::HasSubstr(ERR_SUBSTR));                                                       \
+    }
+
+#endif
diff --git a/apps/openmw_test_suite/testing_util.hpp b/components/testing/util.hpp
similarity index 66%
rename from apps/openmw_test_suite/testing_util.hpp
rename to components/testing/util.hpp
index 0afd04e639..65dbfe927d 100644
--- a/apps/openmw_test_suite/testing_util.hpp
+++ b/components/testing/util.hpp
@@ -1,8 +1,9 @@
-#ifndef TESTING_UTIL_H
-#define TESTING_UTIL_H
+#ifndef OPENMW_COMPONENTS_TESTING_UTIL_H
+#define OPENMW_COMPONENTS_TESTING_UTIL_H
 
 #include <filesystem>
 #include <initializer_list>
+#include <memory>
 #include <sstream>
 
 #include <components/misc/strings/conversion.hpp>
@@ -13,7 +14,6 @@
 
 namespace TestingOpenMW
 {
-
     inline std::filesystem::path outputFilePath(const std::string name)
     {
         std::filesystem::path dir("tests_output");
@@ -79,18 +79,6 @@ namespace TestingOpenMW
     {
         return createTestVFS(VFS::FileMap(files.begin(), files.end()));
     }
-
-#define EXPECT_ERROR(X, ERR_SUBSTR)                                                                                    \
-    try                                                                                                                \
-    {                                                                                                                  \
-        X;                                                                                                             \
-        FAIL() << "Expected error";                                                                                    \
-    }                                                                                                                  \
-    catch (std::exception & e)                                                                                         \
-    {                                                                                                                  \
-        EXPECT_THAT(e.what(), ::testing::HasSubstr(ERR_SUBSTR));                                                       \
-    }
-
 }
 
-#endif // TESTING_UTIL_H
+#endif