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.
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);
}