0

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 the buttonBox 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.

mosegui
  • 461
  • 3
  • 14
  • With [MVC](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) (care, [Qt doesn't use same nomenclature](https://stackoverflow.com/questions/5543198/why-qt-is-misusing-model-view-terminology)), you might dissociate GUI from action. that ease the tests. – Jarod42 Feb 11 '21 at 14:47

0 Answers0