如何使用 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 方法。我的方法是:

  1. m_Process 添加 setter 和 getter(这是很常见的事情,您需要调整代码以使其可测试。)。
  2. 创建 class 派生自 QProcess 覆盖 startwaitForFinished(不要忘记创建它 virtual)并具有附加功能 QList<QList<QString>> start_calls_arguments()int waitForFinished_calls_count()start 只会将参数列表添加到一些内部列表列表中,waitForFinished 会增加一些内部计数器,上述新函数会 return 这些。
  3. 运行 您的测试并获取 QProcess 的派生模拟,向上转换并检查 start 调用的参数和 waitForFinished 调用的计数。

所以,这就是我想要的。

我让 QProcess 由继承自接口 IProcessProcess 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);
}