diff --git a/cicd/stages/test.yml b/cicd/stages/test.yml
index 2d1ee04c672461779d77c2e004b44d43f392b3ed..5c9943ab8f28e626a0f01ec7a60487d22111c38e 100644
--- a/cicd/stages/test.yml
+++ b/cicd/stages/test.yml
@@ -20,8 +20,35 @@ test-x86_64:
     - cd ${QDS_CI_ARTIFACTS_PATH}/android/x86_64/test || exit 1
     - mkdir -p ${QDS_CI_JOB_TEST_RESULTS_PATH}
     - adb install -r -g android-build-release.apk
-    - adb shell am start -e applicationArguments "'-o output.txt,txt -o output.xml,xml -o output.junitxml,junitxml'" -n io.qt.qtuiviewer.test/org.qtproject.qt.android.bindings.QtActivity
+    - adb shell "run-as io.qt.qtuiviewer.test rm -rf /data/data/io.qt.qtuiviewer.test/files/output.junitxml"
+    - adb shell am start -e applicationArguments "'-o output.junitxml,junitxml'" -n io.qt.qtuiviewer.test/org.qtproject.qt.android.bindings.QtActivity
+    - |
+      counter=0
+      while [ -z "$(adb shell pidof -s io.qt.qtuiviewer.test)" ]; do
+        echo "Waiting for test to start"
+        sleep 0.1
+        counter=$((counter+1))
+        if [ $counter -gt 60 ]; then
+          echo "Test did not start in time"
+          exit 1
+        fi
+      done
+    - PID_OF_TEST=$(adb shell pidof -s io.qt.qtuiviewer.test)
+    - 'echo "PID of test: ${PID_OF_TEST}"'
+    - |
+      counter=0
+      while [ -n "$(adb shell pidof -s io.qt.qtuiviewer.test)" ]; do
+        echo "Waiting for test to finish"
+        sleep 0.1
+        counter=$((counter+1))
+        if [ $counter -gt 60 ]; then
+          echo "Test did not finish in time"
+          exit 1
+        fi
+      done
+    - adb logcat -d --pid=${PID_OF_TEST} > ${QDS_CI_JOB_TEST_RESULTS_PATH}/logcat.txt
     - adb shell "run-as io.qt.qtuiviewer.test cat /data/data/io.qt.qtuiviewer.test/files/output.junitxml" > output.junit.xml
+    - adb uninstall io.qt.qtuiviewer.test
     - mv output.junit.xml ${QDS_CI_JOB_TEST_RESULTS_PATH}/test.junit.xml
   artifacts:
     paths:
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index cef94e9fafd108e973ea3b8d482261dccb6c9981..746e970ebbaca3563690d6f9573e39231fc42a4e 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -26,6 +26,28 @@ target_link_libraries(${PROJECT_NAME} PRIVATE
     ZXing::ZXing
 )
 
+qt_add_library(qtuiviewerlib OBJECT
+    EXCLUDE_FROM_ALL
+    backend/projectmanager.cpp backend/projectmanager.h
+    backend/serviceconnector.cpp backend/serviceconnector.h
+    backend/dsconnector.cpp backend/dsconnector.h
+)
+
+target_link_libraries(qtuiviewerlib PRIVATE
+    Qt6::Core
+    Qt6::Quick
+    Qt6::Qml
+    Qt6::Gui
+    Qt6::GuiPrivate
+    Qt6::Multimedia
+    Qt6::MultimediaWidgets
+    Qt6::Concurrent
+)
+
+target_include_directories(qtuiviewerlib PUBLIC
+    ${CMAKE_CURRENT_SOURCE_DIR}
+)
+
 set_property(TARGET ${PROJECT_NAME}
     APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android
 )
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 0733caf57be2f2a29a9df7c7fa26f24b1e7f34bc..e13567c9280fd89be3dbddbd818a2feb390fca58 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -4,13 +4,23 @@ find_package(Qt6 REQUIRED COMPONENTS Test Core Quick Gui)
 
 enable_testing(true)
 qt_add_executable(${TARGET_NAME}
-    unit/tst_qtuiviewer.cpp
-    unit/tst_qtuiviewer.h
+    tst_qtuiviewer.cpp
+    tst_qtuiviewer.h
 )
 
 add_test(NAME ${TARGET_NAME} COMMAND qtuiviewer_test)
-target_link_libraries(${TARGET_NAME} PRIVATE Qt::Test Qt::Core Qt::Quick Qt::Gui)
+target_link_libraries(
+    ${TARGET_NAME}
+    PRIVATE
+    Qt::Test Qt::Core Qt::Quick Qt::Gui
+    qtuiviewerlib
+)
 
 set_property(TARGET ${TARGET_NAME}
     APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/android
 )
+
+set_property(TARGET ${TARGET_NAME} PROPERTY QT_ANDROID_EXTRA_LIBS
+    ${ANDROID_OPENSSL_PATH}/libcrypto_3.so
+    ${ANDROID_OPENSSL_PATH}/libssl_3.so
+)
diff --git a/tests/scripts/run_test.sh b/tests/scripts/run_test.sh
new file mode 100755
index 0000000000000000000000000000000000000000..fd542e5bfc4d1a55db63c6efd8bdddca51f7e845
--- /dev/null
+++ b/tests/scripts/run_test.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+# This script is used to run the test on the device and collect the logs and the test results.
+# Precondition: The device/emulator is connected/running and the test application is installed on it.
+
+adb shell "run-as io.qt.qtuiviewer.test rm -rf /data/data/io.qt.qtuiviewer.test/files/output.junitxml"
+adb shell am start -e applicationArguments "'-o output.junitxml,junitxml'" -n io.qt.qtuiviewer.test/org.qtproject.qt.android.bindings.QtActivity
+
+counter=0
+while [ -z "$(adb shell pidof -s io.qt.qtuiviewer.test)" ]; do
+    echo "Waiting for test to start"
+    sleep 0.1
+    counter=$((counter + 1))
+    if [ $counter -gt 60 ]; then
+        echo "Test did not start in time"
+        exit 1
+    fi
+done
+
+PID_OF_TEST=$(adb shell pidof -s io.qt.qtuiviewer.test)
+echo "PID of test: ${PID_OF_TEST}"
+
+counter=0
+while [ -n "$(adb shell pidof -s io.qt.qtuiviewer.test)" ]; do
+    echo "Waiting for test to finish"
+    sleep 0.1
+    counter=$((counter + 1))
+    if [ $counter -gt 60 ]; then
+        echo "Test did not finish in time"
+        exit 1
+    fi
+done
+
+adb logcat -d --pid=${PID_OF_TEST}
+adb logcat -d --pid=${PID_OF_TEST} >logcat.txt
+adb shell "run-as io.qt.qtuiviewer.test cat /data/data/io.qt.qtuiviewer.test/files/output.junitxml" >output.junit.xml
diff --git a/tests/tst_qtuiviewer.cpp b/tests/tst_qtuiviewer.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..e0c111e0f7a10a5a4fd43661685c0c997df44d25
--- /dev/null
+++ b/tests/tst_qtuiviewer.cpp
@@ -0,0 +1,224 @@
+/****************************************************************************
+**
+** Copyright (C) 2024 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the Qt Design Viewer of the Qt Toolkit.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#include "tst_qtuiviewer.h"
+
+#include <QSignalSpy>
+
+#include "backend/dsconnector.h"
+#include "backend/projectmanager.h"
+#include "backend/serviceconnector.h"
+
+#define DEMO_PROJECT_NAME "ClusterTutorial"
+
+void TestQtUiViewer::initTestCase()
+{
+    qDebug() << "initTestCase";
+    ServiceConnector sc;
+    m_demoProjectList = sc.fetchDemoList();
+    QVERIFY(m_demoProjectList.has_value());
+
+    m_demoProjectData = sc.fetchDemo(DEMO_PROJECT_NAME);
+    QVERIFY(m_demoProjectData.has_value());
+
+    QJsonObject projectInfo;
+    for (auto project : m_demoProjectList.value()) {
+        if (DEMO_PROJECT_NAME == project.toObject().value("name").toString().remove(".qmlrc")) {
+            m_demoInfoCorrect = project.toObject();
+            break;
+        }
+    }
+
+    m_demoInfoIncorrect = QJsonObject{
+        {{"lastUpdate", 1707822220200.017}, {"name", "ClusterTutorial.qmlrc"}}};
+
+    m_userProjectInfoCorrect = QJsonObject{{{"appName", "Testmcu"},
+                                            {"id", "0sadf8fa9s8df67s8998690a7sdf"},
+                                            {"owner", ""},
+                                            {"passwordHash", ""},
+                                            {"qdsIsEnterprise", true},
+                                            {"qdsVersion", "Qt Design Studio 4.2.0"},
+                                            {"ttlDays", 31},
+                                            {"uploadTime", "2023-10-27T13:57:22"},
+                                            {"userHash", "12038740912873462987"}}};
+
+    m_userProjectInfoIncorrect = QJsonObject{{{"appName", "Testmcu"},
+                                              {"id", "0sadf8fa9s8df67s8998690a7sdf"},
+                                              {"owner", ""},
+                                              {"passwordHash", ""},
+                                              {"qdsIsEnterprise", true},
+                                              {"qdsVersion", "Qt Design Studio 4.2.0"},
+                                              {"ttlDays", 31},
+                                              {"uploadTime", "2023-10-27T13:58:22"},
+                                              {"userHash", "12038740912873462987"}}};
+    qDebug() << "initTestCase done";
+}
+
+void TestQtUiViewer::fetchDemoProjectList()
+{
+    QStringList projectNames{"ClusterTutorial.qmlrc",
+                             "CoffeeMachine.qmlrc",
+                             "EBikeDesign.qmlrc",
+                             "MaterialBundle.qmlrc",
+                             "SideMenu.qmlrc",
+                             "WebinarDemo.qmlrc"};
+
+    QStringList projectNamesFromJson;
+    for (const auto &project : m_demoProjectList.value()) {
+        projectNamesFromJson.append(project.toObject().value("name").toString());
+    }
+
+    QCOMPARE(projectNamesFromJson, projectNames);
+}
+
+void TestQtUiViewer::fetchDemoProject()
+{
+    QCOMPARE(m_demoProjectData.has_value(), true);
+}
+
+void TestQtUiViewer::cacheDemoProject()
+{
+    ProjectManager pm;
+
+    QCOMPARE(pm.cacheDemoProject(m_demoProjectData.value(), m_demoInfoCorrect), true);
+}
+
+void TestQtUiViewer::isDemoProjectCachedTrue()
+{
+    ProjectManager pm;
+
+    QVERIFY(pm.cacheDemoProject(m_demoProjectData.value(), m_demoInfoCorrect));
+    QCOMPARE(pm.isDemoProjectCached(m_demoInfoCorrect), true);
+}
+
+void TestQtUiViewer::isDemoProjectCachedFalse()
+{
+    ProjectManager pm;
+
+    QVERIFY(pm.cacheDemoProject(m_demoProjectData.value(), m_demoInfoCorrect));
+    QCOMPARE(pm.isDemoProjectCached(m_demoInfoIncorrect), false);
+}
+
+void TestQtUiViewer::clearDemoProjectCache()
+{
+    ProjectManager pm;
+    QVERIFY(pm.cacheDemoProject(m_demoProjectData.value(), m_demoInfoCorrect));
+
+    pm.clearDemoCaches();
+    QCOMPARE(pm.isDemoProjectCached(m_demoInfoCorrect), false);
+}
+
+void TestQtUiViewer::cacheUserProject()
+{
+    ProjectManager pm;
+    QCOMPARE(pm.cacheProject(m_demoProjectData.value(), m_userProjectInfoCorrect), true);
+}
+
+void TestQtUiViewer::isUserProjectCachedTrue()
+{
+    ProjectManager pm;
+    pm.cacheProject(m_demoProjectData.value(), m_userProjectInfoCorrect);
+    QCOMPARE(pm.isProjectCached(m_userProjectInfoCorrect), true);
+}
+
+void TestQtUiViewer::isUserProjectCachedFalse()
+{
+    ProjectManager pm;
+    QVERIFY(pm.cacheProject(m_demoProjectData.value(), m_userProjectInfoCorrect));
+    QCOMPARE(pm.isProjectCached(m_userProjectInfoIncorrect), false);
+}
+
+void TestQtUiViewer::clearUserProjectCache()
+{
+    ProjectManager pm;
+    QVERIFY(pm.cacheProject(m_demoProjectData.value(), m_userProjectInfoCorrect));
+    QVERIFY(pm.isProjectCached(m_userProjectInfoCorrect));
+
+    pm.clearCachedProject(m_userProjectInfoCorrect);
+    QCOMPARE(pm.isProjectCached(m_userProjectInfoCorrect), false);
+}
+
+void TestQtUiViewer::clearNonExistentUserProjectCache()
+{
+    ProjectManager pm;
+    QVERIFY(!pm.isProjectCached(m_userProjectInfoIncorrect));
+    pm.clearCachedProject(m_userProjectInfoIncorrect);
+    QCOMPARE(pm.isProjectCached(m_userProjectInfoIncorrect), false);
+}
+
+void TestQtUiViewer::initDsConnector()
+{
+    DesignStudioConnector dsc;
+
+    QTcpSocket socket;
+    socket.connectToHost("localhost", 40000);
+
+    QVERIFY(socket.waitForConnected(1000));
+}
+
+// void TestQtUiViewer::incomingProjectTrue()
+// {
+//     DesignStudioConnector dsc;
+
+//     QTcpSocket socket;
+//     socket.connectToHost("localhost", 40000);
+
+//     QVERIFY(socket.waitForConnected(1000));
+
+//     // we'll write a sample project over the tcp connection
+//     // and expect the DesignStudioConnector to emit the projectIncoming signal
+//     QSignalSpy spy(&dsc, &DesignStudioConnector::projectIncoming);
+//     QVERIFY(spy.isValid());
+
+//     QByteArray projectData = "qres::qmlrc-start::\n"
+//                              "qmlrc-end::";
+//     socket.write(projectData);
+//     QVERIFY(socket.waitForBytesWritten(1000));
+//     QThread::msleep(100);
+//     QCOMPARE(spy.count(), 1);
+// }
+
+// void TestQtUiViewer::incomingProjectFalse()
+// {
+//     DesignStudioConnector dsc;
+
+//     QTcpSocket socket;
+//     socket.connectToHost("localhost", 40000);
+
+//     QVERIFY(socket.waitForConnected(1000));
+
+//     // we'll write a sample project over the tcp connection
+//     // and expect the DesignStudioConnector to emit the projectIncoming signal
+//     QSignalSpy spy(&dsc, SIGNAL(projectIncoming()));
+
+//     QByteArray projectData = "some arbitrary data";
+//     socket.write(projectData);
+
+//     QVERIFY(socket.waitForBytesWritten(1000));
+
+//     QCOMPARE(spy.count(), 0);
+// }
+
+QTEST_MAIN(TestQtUiViewer);
diff --git a/tests/tst_qtuiviewer.h b/tests/tst_qtuiviewer.h
new file mode 100644
index 0000000000000000000000000000000000000000..a1dee3f0b420a9b608d8c5c614fe0853f29612f7
--- /dev/null
+++ b/tests/tst_qtuiviewer.h
@@ -0,0 +1,65 @@
+/****************************************************************************
+**
+** Copyright (C) 2024 The Qt Company Ltd.
+** Contact: https://www.qt.io/licensing/
+**
+** This file is part of the Qt Design Viewer of the Qt Toolkit.
+**
+** Commercial License Usage
+** Licensees holding valid commercial Qt licenses may use this file in
+** accordance with the commercial license agreement provided with the
+** Software or, alternatively, in accordance with the terms contained in
+** a written agreement between you and The Qt Company. For licensing terms
+** and conditions see https://www.qt.io/terms-conditions. For further
+** information use the contact form at https://www.qt.io/contact-us.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3 as published by the Free Software
+** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
+** included in the packaging of this file. Please review the following
+** information to ensure the GNU General Public License requirements will
+** be met: https://www.gnu.org/licenses/gpl-3.0.html.
+**
+****************************************************************************/
+
+#include <QJsonArray>
+#include <QJsonObject>
+#include <QtTest/QTest>
+
+class TestQtUiViewer : public QObject
+{
+    Q_OBJECT
+private:
+    std::optional<QJsonArray> m_demoProjectList;
+    std::optional<QByteArray> m_demoProjectData;
+    QJsonObject m_demoInfoCorrect;
+    QJsonObject m_demoInfoIncorrect;
+    QJsonObject m_userProjectInfoCorrect;
+    QJsonObject m_userProjectInfoIncorrect;
+
+private slots:
+    void initTestCase();
+
+    // service connector tests
+    void fetchDemoProjectList();
+    void fetchDemoProject();
+
+    // project manager tests - demo project
+    void cacheDemoProject();
+    void isDemoProjectCachedTrue();
+    void isDemoProjectCachedFalse();
+    void clearDemoProjectCache();
+
+    // project manager tests - user project
+    void cacheUserProject();
+    void isUserProjectCachedTrue();
+    void isUserProjectCachedFalse();
+    void clearUserProjectCache();
+    void clearNonExistentUserProjectCache();
+
+    // ds connector tests
+    void initDsConnector();
+    // void incomingProjectTrue();
+    // void incomingProjectFalse();
+};
diff --git a/tests/unit/tst_qtuiviewer.cpp b/tests/unit/tst_qtuiviewer.cpp
deleted file mode 100644
index bf2aff2dae2ec9418e0ccbcb651177d4f0438cc2..0000000000000000000000000000000000000000
--- a/tests/unit/tst_qtuiviewer.cpp
+++ /dev/null
@@ -1,17 +0,0 @@
-#include "tst_qtuiviewer.h"
-
-void TestQString::toUpper()
-{
-    qDebug("TestQString::toUpper");
-    QString str = "Hello";
-    QVERIFY(str.toUpper() == "HELLO");
-}
-
-void TestQString::toLower()
-{
-    qDebug("TestQString::toLower");
-    QString str = "Hello";
-    QCOMPARE(str.toLower(), "hell");
-}
-
-QTEST_MAIN(TestQString)
diff --git a/tests/unit/tst_qtuiviewer.h b/tests/unit/tst_qtuiviewer.h
deleted file mode 100644
index cffffcf03f4442c003bf1c86e5ca38b9176fcce7..0000000000000000000000000000000000000000
--- a/tests/unit/tst_qtuiviewer.h
+++ /dev/null
@@ -1,11 +0,0 @@
-#pragma once
-
-#include <QtTest/QTest>
-
-class TestQString : public QObject
-{
-    Q_OBJECT
-private slots:
-    void toUpper();
-    void toLower();
-};