0

I'm currently developing the unit tests for a QT application. This application use a single worker thread that handles all database calls, so all database methods execute a concurrent call and returns a QFuture. The service class gets the Qfuture and usually emits a signal when it's completed(but I'm unsure it's always the case). But for some reason the thread seems to only execute once tests have finished.

I have solved this for instances when the methods return the QFuture and I can call .waitforfinished() on it so it wait till it's finished (WOW). But there are methods that process the QFutures internally and those seem to only execute once all tests have finished.

My question is, how can I force or wait till all is executed? (I found out I can use spy.wait() for this specific case but I'm unsure all methods really emit a signal so are there more ways to wait for the thread inside a method to finishing executing?)

Here is an example, first the log and then the code:

log

********* Start testing of Test *********
Config: Using QtTest library 6.5.3, Qt 6.5.3
QINFO  : Test::initTestCase() Database Connected!
PASS   : Test::initTestCase()
FAIL!  : Test::isDataInserted() Compared values are not the same
   Actual   (spy.count()): 0
   Expected (1)          : 1
/test/tests.cpp(36) : failure location
PASS   : Test::cleanupTestCase()
QDEBUG : Test::isDataInserted() Running query on  QThreadPoolThread(0x168, name = "Database ThreadPool")
Totals: 2 passed, 1 failed, 0 skipped, 0 blacklisted, 10ms
********* Finished testing of Test *********
Query executed on  QThreadPoolThread(0x16828586790, name = "Database ThreadPool")
Query result:  QVariant(qlonglong, 4)
A crash occurred \tests.exe.
Function time: 133ms Total time: 142ms

Exception address: 0x00007ff8fa1612e7
Exception code   : 0xc0000005

tests.cpp

#include <QCoreApplication>
#include <QDebug>
#include <QtTest>

#include "exception.h"
#include "service.h"

class Test : public QObject {
  Q_OBJECT

  const QString dbPath = "test_DB.db";

 private slots:
  void initTestCase() {
    DbManager* db = new DbManager();
    QFuture<void> connection = db->connect(dbPath);

    QFuture test = connection.then([]() { qInfo() << "Database Connected!"; })
                       .onFailed([](Exception e) {
                         qDebug() << "Error initializing database" << e.what();
                       });  // None of this ever runs.

    test.waitForFinished();
  }

  // void cleanupTestCase() {
  //   QFile file(dbPath);
  //   file.remove();
  // }

  void isDataInserted() {
    Service service;
    QSignalSpy spy(&service, SIGNAL(dataInserted()));
    service.insertData("test 1");

    QCOMPARE(spy.count(), 1);
  }
};

QTEST_MAIN(Test)

#include "tests.moc"

dbmanager.h

#ifndef DBMANAGER_H
#define DBMANAGER_H

#include <QFuture>
#include <QSqlQuery>
#include <QtConcurrent/QtConcurrent>

#include "exception.h"
#include "thread.h"

class DbManager : public QObject {
  Q_OBJECT
 public:
  QFuture<void> connect(const QString &path) {
    return QtConcurrent::run(DbThread::instance()->pool(), [path]() {
      QSqlDatabase m_db = QSqlDatabase::addDatabase("QSQLITE", "main");
      m_db.setDatabaseName(path);

      if (!m_db.open()) {
        throw Exception("Error connecting to the database");
      }

      QSqlQuery query(m_db);

      query.prepare(
          "CREATE TABLE IF NOT EXISTS TEST "
          "(id INTEGER PRIMARY KEY AUTOINCREMENT, data TEXT)");
      bool success = query.exec();

      if (!success) {
        throw Exception("Error creating structure");
      }
    });
  }

  QFuture<QSqlQuery> executeQuery(QString queryString, QList<QVariant> args) {
    return QtConcurrent::run(
        DbThread::instance()->pool(), [this, queryString, args]() {
          qDebug() << "Running query on " << QThread::currentThread();
          QSqlQuery query(QSqlDatabase::database("main"));
          query.prepare(queryString);

          for (int i = 0; i < args.length(); i++) {
            query.addBindValue(args[i]);
          }

          if (query.exec()) {
            return query;
          } else {
            throw Exception("Error executing query (" + queryString + ")");
          }
        });
  }
};

#endif  // DBMANAGER_H

service.h

#ifndef SERVICE_H
#define SERVICE_H

#include <QObject>

#include "dbmanager.h"

class Service : public QObject {
  Q_OBJECT

 public:
  void insertData(QString data) {
    QString queryString = "INSERT INTO TEST (data) VALUES (?)";
    auto args = QList<QVariant>{data};

    DbManager db;
    QFuture<QSqlQuery> result = db.executeQuery(queryString, args);

    result
        .then([this](QSqlQuery query) {
          qDebug() << "Query executed on " << QThread::currentThread();
          qDebug() << "Query result: " << query.lastInsertId();
          emit dataInserted();
        })
        .onFailed([this](Exception e) {
          qDebug() << "Error executing query: " << e.what();
          emit dataInsertedFailed();
        });
  }

 signals:
  void dataInserted();
  void dataInsertedFailed();
};

#endif  // SERVICE_H

exception.h

#ifndef EXCEPTION_H
#define EXCEPTION_H

#include <QException>

class Exception : public QException {
 public:
  Exception(QString text) { m_text = text.toLocal8Bit(); }

  const char* what() const noexcept {
    return m_text
        .data();  // https://wiki.qt.io/Technical_FAQ#How_can_I_convert_a_QString_to_char.2A_and_vice_versa.3F
  }

  void raise() const {
    Exception e = *this;
    throw e;
  }

  Exception* clone() const { return new Exception(*this); }

 private:
  QByteArray m_text;
};

#endif  // EXCEPTION_H

thread.h

#ifndef THREAD_H
#define THREAD_H

#include <QThreadPool>

class DbThread
{
    DbThread() {
        m_pool = new QThreadPool();
        m_pool->setMaxThreadCount(1);
        m_pool->setExpiryTimeout(-1);
        m_pool->setObjectName("Database ThreadPool");
    };

    virtual ~DbThread() {
        m_pool->deleteLater();
    };

public:
    DbThread( const DbThread& ) = delete;               // singletons should not be copy-constructed
    DbThread& operator=( const DbThread& ) = delete;    // singletons should not be assignable

    static DbThread* instance() {

        if ( !m_instance )
            m_instance = new DbThread();

        return m_instance;
    }

    QThreadPool* pool() const { return m_pool; };

private:
    inline static DbThread* m_instance = nullptr; // https://stackoverflow.com/a/61519399/12172630
    QThreadPool* m_pool;

};

#endif // THREAD_H

CMakeLists.txt

cmake_minimum_required(VERSION 3.5)

project(tests LANGUAGES CXX)

enable_testing()

find_package(Qt6 6.5 REQUIRED COMPONENTS Sql Gui Quick Test)

set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_library(sources STATIC
    dbmanager.h thread.h exception.h
    service.h
)

add_executable(tests tests.cpp)
add_test(NAME tests COMMAND tests)

target_link_libraries(sources
    Qt6::Sql
)

target_link_libraries(tests
    PRIVATE Qt6::Quick
    PRIVATE Qt6::Gui
    Qt6::Test
    sources
)
3
  • While writing this I found out I can use the spy.wait() function so it will wait the signal and it will work but I would like other options as I'm unsure all future calls really emit a signal. Commented Jan 3 at 14:58
  • Please don't use comments to add information that is relevant to the question, edit the post instead. Commented Jan 3 at 15:01
  • Please read about Qt::AutoConnection and Qt::QueuedConnection. Add spy.wait(100); before QCOMPARE(spy.count(), 1); to activate event loop and let QueuedConnection to do its job.
    – Marek R
    Commented Jan 3 at 15:21

0

Browse other questions tagged or ask your own question.