如何使用 Qt 对象测试 class
How to test a class using Qt objects
我正在为基于 Qt 的应用程序编写单元测试,但我很难找到正确的方法来正确测试 class 的行为。我有一个 SystemDateTimeUpdater class,它拥有一个 QProcess 对象来更新系统时间。
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();
});
}
}
当然,我不希望我的 class 测试中尝试更新系统日期时间。
我有两个 天真的 想法。
首先是注入管理系统路径的实用程序class。然后我可以模拟这种依赖关系并使用 QProcess 启动一个简单的脚本 returns 0 来模拟成功。不过,这对我来说似乎是一个尴尬的解决方案。
我的第二个想法是注入 QProcess 本身以放松与我的 class 的耦合。但我对这个想法也不完全满意。我需要将 QProcess 隐藏在我自己的接口后面,以便能够正确地模拟它。如果我必须为每个非平凡的 Qt class 做同样的工作(例如,我使用 QDbus 来监控电池),这似乎是很多工作,我相信有一些更好的方法可以做它。此外,如果我增加依赖项注入,我想知道如何为我的 classes 管理那么多依赖项。我应该有某种工厂将所有权传递给调用 class 吗?我想这是一个完全不同的话题,但我仍然对此感到疑惑。
我很乐意阅读您对此的意见。
标准方法是将 m_Process
替换为模拟对象,该模拟对象仅保存调用了多少次以及调用了哪些参数的函数。在这种情况下,我将测试 start
方法。我的方法是:
- 为
m_Process
添加 setter 和 getter(这是很常见的事情,您需要调整代码以使其可测试。)。
- 创建 class 派生自
QProcess
覆盖 start
和 waitForFinished
(不要忘记创建它 virtual
)并具有附加功能 QList<QList<QString>> start_calls_arguments()
和 int waitForFinished_calls_count()
。 start
只会将参数列表添加到一些内部列表列表中,waitForFinished
会增加一些内部计数器,上述新函数会 return 这些。
- 运行 您的测试并获取
QProcess
的派生模拟,向上转换并检查 start
调用的参数和 waitForFinished
调用的计数。
所以,这就是我想要的。
我让 QProcess
由继承自接口 IProcess
的 Process
class 实例化(也可能是 class 成员) .我的 SystemDateTimeUpdater 持有一个指向接口的指针。
基本上,我有:
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;
};
现在在调用启动函数之前我需要从某个地方设置一个进程。
// My object has previously been initialized
systemDateTimeUpater->setProcess(new Process); //systemDateTimeUpdater takes ownership of this ptr
我可以使用界面使用 gmock 轻松模拟它。
class MockProcess : public IProcess
{
public:
MOCK_METHOD(void, start, (const QString& program, const QStringList& arguments), (override));
};
这里有两个使用模拟对象的基本测试。
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);
}
我正在为基于 Qt 的应用程序编写单元测试,但我很难找到正确的方法来正确测试 class 的行为。我有一个 SystemDateTimeUpdater class,它拥有一个 QProcess 对象来更新系统时间。
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();
});
}
}
当然,我不希望我的 class 测试中尝试更新系统日期时间。 我有两个 天真的 想法。
首先是注入管理系统路径的实用程序class。然后我可以模拟这种依赖关系并使用 QProcess 启动一个简单的脚本 returns 0 来模拟成功。不过,这对我来说似乎是一个尴尬的解决方案。
我的第二个想法是注入 QProcess 本身以放松与我的 class 的耦合。但我对这个想法也不完全满意。我需要将 QProcess 隐藏在我自己的接口后面,以便能够正确地模拟它。如果我必须为每个非平凡的 Qt class 做同样的工作(例如,我使用 QDbus 来监控电池),这似乎是很多工作,我相信有一些更好的方法可以做它。此外,如果我增加依赖项注入,我想知道如何为我的 classes 管理那么多依赖项。我应该有某种工厂将所有权传递给调用 class 吗?我想这是一个完全不同的话题,但我仍然对此感到疑惑。
我很乐意阅读您对此的意见。
标准方法是将 m_Process
替换为模拟对象,该模拟对象仅保存调用了多少次以及调用了哪些参数的函数。在这种情况下,我将测试 start
方法。我的方法是:
- 为
m_Process
添加 setter 和 getter(这是很常见的事情,您需要调整代码以使其可测试。)。 - 创建 class 派生自
QProcess
覆盖start
和waitForFinished
(不要忘记创建它virtual
)并具有附加功能QList<QList<QString>> start_calls_arguments()
和int waitForFinished_calls_count()
。start
只会将参数列表添加到一些内部列表列表中,waitForFinished
会增加一些内部计数器,上述新函数会 return 这些。 - 运行 您的测试并获取
QProcess
的派生模拟,向上转换并检查start
调用的参数和waitForFinished
调用的计数。
所以,这就是我想要的。
我让 QProcess
由继承自接口 IProcess
的 Process
class 实例化(也可能是 class 成员) .我的 SystemDateTimeUpdater 持有一个指向接口的指针。
基本上,我有:
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;
};
现在在调用启动函数之前我需要从某个地方设置一个进程。
// My object has previously been initialized
systemDateTimeUpater->setProcess(new Process); //systemDateTimeUpdater takes ownership of this ptr
我可以使用界面使用 gmock 轻松模拟它。
class MockProcess : public IProcess
{
public:
MOCK_METHOD(void, start, (const QString& program, const QStringList& arguments), (override));
};
这里有两个使用模拟对象的基本测试。
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);
}