There is already some reading available regarding the pImpl idiom and what its usage means for app testability (e.g. see: The pImpl idiom and Testability). Whilst the most common "official" answer is always something like: "Everything which is hidden behind a pImpl construct is not meant for you to test. Just assume that part of the code is sound, and black-box-test the available part of the code", I haven't been able to find a way (or a common practice) to have unit tests co-exist with the opaque pointer when what is hidden behind it are GUIs (or other objects for what's worth) that expect some user kind of interaction to later interact with the "available" part of the code, i.e. the caller method.
In particular, I am thinking about the opaque pointer behind which Qt hides the implementation of widgets/dialogs designed with Qt Creator. Let's suppose I have a main window with a text display and a "New" button. If I press the button, a new dialog appears with a lineEdit and buttonBox (OK/Cancel). If I write some text in the lineEdit and press "OK", the new dialog closes and the text gets displayed in the text display of the main window. If I press "Cancel" in the new dialog (irrespective of the text in the lineEdit), the dialog closes with no further effects.
The design of the above simple program looks as follows:
//mainwindow.h
#pragma once
#include <QMainWindow>
...
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
newDialog* newdialog;
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void on_buttonNew_clicked();
private:
Ui::MainWindow *ui;
};
The actual implementation of the ui
object (A class named MainWindow
in the namespace Ui
) can be found in the header file ui_mainwindow.h
, which Qt generates automatically from the XML file, but which is not important for the current question.
The MainWindow
source file provides a private slot for invoking the new dialog class and collect the text the user has input in case the user clicks on "OK":
//mainwindow.cpp
...
void MainWindow::on_buttonNew_clicked()
{
int res;
this->newdialog = new newDialog(this);
res = newdialog->exec();
if (res == QDialog::Rejected)
{
// do nothing
return;
}
QString newDialogText = newdialog->get_text();
// set newDialog text in mainwindow text display
}
Similarly, we have the newDialog
header and source files:
//newdialog.cpp
#include "newdialog.h"
#include "ui_newdialog.h"
newDialog::newDialog(QWidget *parent) : QDialog(parent), ui(new Ui::newDialog)
{
ui->setupUi(this);
}
newDialog::~newDialog()
{
delete ui;
}
void newDialog::on_buttonBox_rejected()
{
reject();
}
void newDialog::on_buttonBox_accepted()
{
accept();
}
QString newDialog::get_text()
{
QString text = ui->lineEdit->text();
return text;
}
//newdialog.h
#pragma once
#include <QDialog>
namespace Ui {
class newDialog;
}
class newDialog : public QDialog
{
Q_OBJECT
public:
explicit newDialog(QWidget *parent = nullptr);
~newDialog();
QString get_text();
private slots:
void on_buttonBox_rejected();
void on_buttonBox_accepted();
private:
Ui::newIssue *ui;
};
And, lastly, the (hidden) implementation of the newDialog
class, in the header file ui_newdialog.h
:
//ui_newdialog.h
#ifndef UI_NEWDIALOG_H
#define UI_NEWDIALOG_H
#include <QtCore/QVariant>
...
QT_BEGIN_NAMESPACE
class Ui_newDialog
{
public:
QLineEdit *lineedit;
QDialogButtonBox *buttonBox;
void setupUi(QDialog *newDialog)
{
...
}
void retranslateUi(QDialog *newDialog)
{
...
}
};
namespace Ui {
class newDialog: public Ui_newDialog {};
}
QT_END_NAMESPACE
#endif
Now, what I seem to be unable to wrap my head around is the following. I want to test the method MainWindow::on_buttonNew_clicked()
. In the header file of my testing class I have declared a MainWindow*
instance, and since the MainWindow
class has newDialog* newdialog;
declared as apublic member, I can access the new dialog public methods from my testing class (i.e. only newDialog::get_text()
).
Nevertheless, during testing I want to automatically provide some dummy text to the new dialog to see if e.g. it gets correctly parsed into the mainwindow newDialogText
variable. For that, QTest
provides some functionalities like e.g. QTest::mouseClick(..., Qt::LeftButton)
, but:
- How am I supposed to use it if the actual access to the
lineedit
or thebuttonBox
is protected behind the pImpl? - Is there any way to carry out this test without having to undo the pImpl?
- Does the "you are not supposed to test this" still apply to this case?
- Is my software design fundamentally wrong and this is because I am running into this situation?
Any help will be much appreciated. Thanks in advance.
EDIT 1:
Adding the friend
declaration to the newDialog
class does not help. This is:
//ui_newdialog.h
...
namespace Ui {
class newDialog: public Ui_newDialog
{
friend class TestMainWindow;
};
}
instead of:
//ui_newdialog.h
...
namespace Ui {
class newDialog: public Ui_newDialog {};
}
where TestMainWindow
is obviously the class that will run the tests.