首页 > 解决方案 > Qt 使用 exec() 循环测试 QMessageBoxes(或通用 QDialog)

问题描述

我想测试QMessageBox我创建的自定义。特别是,我的 API 将自动执行 的exec()循环QMessageBox,并且我想在用户单击按钮或关闭小部件时检查结果。

问题是我被困在exec()循环中。

我找到的唯一解决方案是创建一个QTimer在循环执行之前执行我想要的操作。对此解决方案的引用是thisthis

测试工作正常,但在 Qt 中真的没有更原生的exec()方式来测试 s 的循环QDialog吗?我的意思是,有一些方法可以激活/模拟信号QTest,没有什么可以模拟循环exec()

标签: c++qtunit-testing

解决方案


为什么要“模拟”事件循环?你想运行真正的事件循环!您只想将代码注入其中,这就是计时器的用途(或一般的事件)。

Qt Test 在事件循环周围有各种方便的包装器,例如keyClick传递事件并排空事件队列等,但您不需要模拟任何东西。如果你没有找到你需要的东西,你可以自己做。

您使用的 API 当然是损坏的:它假装异步操作是同步的。它应该将控件反转为延续传递样式,即:

// horrible
int Api::askForValue();
// better
Q_SIGNAL void askForValueResult(int);
void Api::askForValue(); // emits the signal
template <typename F>
void Api::askForValue(F &&fun) { // convenience wrapper
  auto *conn = new QMetaObject::Connection;
  *conn = connect(this, &Class::askForValueResult, 
    [fun = std::forward<F>(fun), 
     c = std::unique_ptr<QMetaObject::Connection>(conn)]
    (int val){
      fun(val);
      QObject::disconnect(*c);
    });
  askForValue();
}

同步 API 之类的让很多应用程序代码不得不面临重入,这不是一个假设的问题。关于它的问题并不少见。很少有人意识到这是一个多么糟糕的问题。

但是假设您被迫坚持使用可怕的 API:您真正想要的是对显示的消息窗口做出反应。您可以通过在QEvent::WindowActivate. 只需在应用程序实例上安装您的事件过滤器对象。这可以包含在一些帮助代码中:

/// Invokes setup, waits for a new window of a type T to be activated,
/// then invokes fun on it. Returns true when fun was invoked before the timeout elapsed.
template <typename T, typename F1, typename F2>
bool waitForWindowAnd(F1 &&setup, F2 &&fun, int timeout = -1) {
  QEventLoop loop;
  QWidgetList const exclude = QApplication::topLevelWidgets();
  struct Filter : QObject {
    QEventLoop &loop;
    F2 &fun;
    bool eventFilter(QObject *obj, QEvent *ev) override {
      if (ev.type() == QEvent::WindowActivate)
        if (auto *w = qobject_cast<T*>(obj))
          if (w->isWindow() && !exclude.contains(w)) {
            fun(w);
            loop.exit(0);
          }
      return false;
    }
    Filter(QEventLoop &loop, F2 &fun) : loop(loop), fun(fun) {}
  } filter{loop, fun};
  qApp->installEventFilter(&filter);

  QTimer::singleShot(0, std::forward<F1>(setup));
  if (timeout > -1)
    QTimer::singleShot(timeout, &loop, [&]{ loop.exit(1); });
  return loop.exec() == 0;
}

可以制作进一步的包装器,以排除常见的要求:

/// Invokes setup, waits for new widget of type T to be shown,
/// then clicks the standard OK button on it. Returns true if the button
/// was clicked before the timeout.
template <typename T, typename F>
bool waitForStandardOK(F &&setup, int timeout = -1) {
  return waitForWindowAnd<T>(setup,
    [](QWidget *w){
        if (auto *box = w->findChild<QDialogButtonBox*>())
          if (auto *ok = box->standardButton(QDialogButtonBox::Ok))
            ok->click();
    }, timeout);
}

然后,假设您想测试阻塞 API QDialogInput::getInt

waitForStandardOK<QDialog>([]{ QInputDialog::getInt(nullptr, "Hello", "Gimme int!"); }, 500);

您还可以制作一个包装器来构建绑定调用,而无需使用 lambda:

/// Binds and invokes the setup call, waits for new widget of type T to be shown,
/// then clicks the standard OK button on it. Returns true if the button
/// was clicked before the timeout.
template<typename T, class ...Args>
void waitForStandardOKCall(int timeout, Args&&... setup) {
  auto bound = std::bind(&invoke<Args&...>, std::forward<Args>(args)...);
  return waitForStandardOK<T>(bound, timeout);
}

因此:

waitForStandardOKCall<QDialog>(500, &QInputDialog::getInt, nullptr, "Hello", "Gimme int!");

有关std::bind完美转发的更多信息,请参阅此问题


推荐阅读