Qt 使用 exec() 循环测试 QMessageBoxes(或通用 QDialog)

n3m*_*3mo 2 c++ qt unit-testing

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

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

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

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

Rei*_*ica 5

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

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();
}
Run Code Online (Sandbox Code Playgroud)

同步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;
}
Run Code Online (Sandbox Code Playgroud)

可以制作更多的包装器来排除常见的要求:

/// 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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

您还可以创建一个包装器来构建绑定调用,而无需使用 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);
}
Run Code Online (Sandbox Code Playgroud)

因此:

waitForStandardOKCall<QDialog>(500, &QInputDialog::getInt, nullptr, "Hello", "Gimme int!");
Run Code Online (Sandbox Code Playgroud)

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