Search code examples
qtgoogletestgooglemock

How to test a class using Qt objects


I am writing unit tests for my Qt based application but I struggle to find the correct way to test the behavior of a class properly. I have a SystemDateTimeUpdater class that possesses a QProcess object to update the system time.

class SystemDateTimeUpdater : public QObject
{
public:
  explicit SystemDateTimeUpdater(const QDateTime& dateTime, QObject* parent = nullptr)
    : QObject(parent), m_DateTime(dateTime)
  {
    m_Process.setParent(this);
    connects(m_Process);
  }
  /*...*/

  void updateSystemDateTime()
  {
    QString dateTimeISO = m_DateTime.toString(Qt::DateFormat::ISODate);
    QStringList arguments;
    arguments << "--iso-8601"
              << "-s"
              << dateTimeISO;

    m_Process.start("/bin/date", arguments);
    m_Process.waitForFinished(); 
    emit notifyFinished();
  }

signals:
  void notifyFinished();
  void notifySuccess();

private:
  QProcess m_Process;
  QDateTime m_DateTime;

  void connects(QProcess &process)
  {
    connect(&process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
            this, [this](int exitCode, QProcess::ExitStatus exitStatus)
    {
      if (exitStatus == QProcess::NormalExit && exitCode == 0)
        emit notifySuccess();
    });
  }
}

Of course, I don't want that my class under test tries to update the system datetime. I have two naive ideas in mind.

The first is to inject an utility class that manages system paths. I can then mock this dependency and use QProcess to start a script that simply returns 0 to simulate success. That seems like an awkward solution to me, though.

The second idea I had is to inject the QProcess itself to loosen the coupling with my class. But I'm not entirely satisfied with this idea either. I would need to hide the QProcess behind an interface of my own in order to be able to mock it properly. If I have to do the same kind of work for every non trivial Qt class (eg. I use QDbus to monitor the battery), it seems like a lot of work and I'm sure there's some better way to do it. Moreover, if I multiply dependency injections, I am wondering how to manage that many dependencies for my classes. Should I have some kind of factory that relays ownership to the calling class? I suppose that's a whole different topic but I am still wondering about this.

I would be happy to read your input on this.


Solution

  • So, here's what I went for.

    I made the QProcess be instantiated (could also have been a class member) by a Process class that inherits from an interface IProcess. My SystemDateTimeUpdater holds a pointer to the interface. Basically, I have:

    class IProcess : public QObject
    {
      Q_OBJECT
    
    public:
      explicit IProcess(QObject* parent = nullptr) : QObject(parent) {};
      virtual ~IProcess() {};
    
      virtual void start(const QString& program, const QStringList& arguments = QStringList()) = 0;
    
    signals:
      void notifyStarted();
      void notifyFinished(bool success);
    };
    
    class Process : public IProcess
    {
      Q_OBJECT
    public:
      explicit Process(QObject* parent = nullptr) : IProcess(parent)
    {}
      virtual ~Process() {};
    
      void start(const QString& program, const QStringList& arguments = QStringList()) override
      {
        QProcess* process = new QProcess(this);
        QObject::connect(process, &QProcess::started, this, &Process::notifyStarted);
        QObject::connect(process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
                         this, [=](int exitCode, QProcess::ExitStatus exitStatus)
        {
          if (exitStatus == QProcess::NormalExit && exitCode == 0)
            emit notifyFinished(true);
          else
            emit notifyFinished(false);
    
          process->deleteLater();
        });
        QObject::connect(process, &QProcess::errorOccurred,
                         this, [=](){ qWarning() << process->errorString(); });
    
        process->start(program, arguments);
      }
    };
    
    class SystemDateTimeUpdater : public QObject
    {
    public:
      explicit SystemDateTimeUpdater(const QDateTime& dateTime, QObject* parent = nullptr)
        : QObject(parent), m_DateTime(dateTime)
      {
      }
    
      /*...*/
    
      void start()
      {
        if (isValid()) // private function that checks that the members are valid
        {
          updateSystemDateTime();
        }
      }
    
      void setProcess(IProcess* process)
      {
        if(m_Process)
            delete m_Process;
    
        QObject::connect(process, &IProcess::notifyFinished, this, &ISystemDateTimeUpdater::notifyUpdateFinished);
        process->setParent(this);
        m_Process = process;
      }
    
    private:
      void updateSystemDateTime()
      {
        if(!m_Process)
        {
            qWarning() << "Process is nullptr. Abort update.";
            return;
        }
    
        QString dateTimeISO = m_DateTime.toString(Qt::DateFormat::ISODate);
        QStringList arguments;
        arguments << "--iso-8601"
                  << "-s"
                  << dateTimeISO;
    
    
        m_Process->start("my/date/command/path", arguments);
      }
    
    signals:
      void notifyUpdateFinished(bool success);
      
    private:
      IProcess* m_Process;
      QDateTime m_DateTime;
    };
    

    Now before calling the start function I need to set a process from some place.

      // My object has previously been initialized
      
      systemDateTimeUpater->setProcess(new Process); //systemDateTimeUpdater takes ownership of this ptr
    

    I can easily mock it with gmock using the interface.

    class MockProcess : public IProcess
    {
    public:
      MOCK_METHOD(void, start, (const QString& program, const QStringList& arguments), (override));
    };
    

    Here's two basic tests using the mock object.

    TEST(SystemDateTimeUpdaterTest, StartSucceeds)
    {
      SystemDateTimeUpdater updater(QDateTime(QDate(1992,7,17), QTime(7,45)));
      MockProcess process;
      EXPECT_CALL(process, start)
              .WillOnce([&process](){ emit process.notifyFinished(true); });
      updater.setProcess(&process);
      QSignalSpy finishedSpy(&updater, &SystemDateTimeUpdater::notifyUpdateFinished);
    
      updater.start();
    
      ASSERT_EQ(finishedSpy.count(), 1);
      EXPECT_TRUE(finishedSpy.takeFirst().at(0).toBool());
    }
    
    TEST(SystemDateTimeUpdaterTest, SetProcessObjectTakesOwnership)
    {
        SystemDateTimeUpdater updater(QDateTime(QDate(1992,7,17), QTime(7,45)));
        MockProcess* process = new MockProcess;
        updater.setProcess(process);
    
        EXPECT_EQ(process->parent(), &updater);
    }