Skip to content

Latest commit

 

History

History
264 lines (198 loc) · 7.05 KB

qt.md

File metadata and controls

264 lines (198 loc) · 7.05 KB

samples/qt

This sample demonstrates how to use DrMock to mock an interface IFoo that inherits from QObject and uses the Q_OBJECT macro.

Table of contents

Project structure

samples/qt
│   CMakeLists.txt
│   Makefile
│
└───src
│   │   CMakeLists.txt
│   │   IFoo.h
│
└───tests
    │   CMakeLists.txt
    │   FooTest.cpp

Requirements

This project requires an installation of DrMock in build/install/ or a path container in the CMAKE_PREFIX_PATH variable. If your installation of DrMock is located elsewhere, you must change the value of CMAKE_PREFIX_PATH.

Furthermore, unless you wish to call cmake directly, it is required that you define the environment variable DRMOCK_QT_PATH to contain the path to an installation of Qt5.

Setup

Let's take a look at the changes make to CMakeLists.txt.

# samples/qt/CMakeLists.txt

# ...

find_package(DrMock)
drmock_enable_qt()
find_package(Qt5 COMPONENTS Core REQUIRED)

# ...

To enable Qt for DrMock, the drmock_enable_qt() macro must be called after finding the DrMock package. This macro enables CMAKE_AUTOMOC and a CMake policy that allows mocking generated source code. Of course, one must also find the Qt5 package.

When calling DrMockModule, the required Qt5 modules must now bespecified using the keyword QTMODULES, as follows. DrMock will automatically link TARGET to the required libraries.

DrMockModule(
  TARGET DrMockSampleQtMocked
  QTMODULES
    Qt5::Widget
    # ...
  HEADERS
    IFoo.h
)

Furthermore, in order to use DrMockModule with Qt, the $DRMOCK_QT_PATH environment variable must be set (see Building DrMock for details).

The drmock_test call requires no changes for the use of Qt.

Mocking a QObject

A class that is derived from QObject3 and holds the Q_OBJECT macro in the private section of its body may be mocked if it satisfies the following rules:

  • The only declarations in the interface shall be public methods, public slots, signals and type alias (template) declarations.

  • All methods shall be declared pure virtual, with the exception of the signals.

  • The interface shall not contain any conversion functions.

  • If an operator is defined in the interface, the interface shall not have a method called operator[SYMBOL], where [SYMBOL] is determined by the operator's symbol according to the table below.

  • None of the interface's method shall be a volatile qualified method or a method with volatile qualified parameters.

3: Note that condition is satisfied if, for instance, the class is derived from QWidget, which in turn is derived from QObject.

The interface IFoo inhertis from QWidget, has a pure virtual slot and a signal.

class IFoo : public QWidget
{
  Q_OBJECT

public:
  virtual ~IFoo() = default;

public slots:
  virtual void theSlot(const std::string&) = 0;

signals:
  void theSignal(const std::string&);
};

To demonstrate the mock object, two instances of FooMock are made and theSignal of foo connected to the slot of bar.

QObject::connect(
    foo.get(), &IFoo::theSignal,
    bar.get(), &IFoo::theSlot
  );

Before that, bar is instructed to expect a call of theSlot with the argument "foo". After foo emits theSignal, this may be verified by bar:

emit foo->theSignal("foo");

DRMOCK_ASSERT(bar->mock.verify());

The emit foo->theSignal("foo"); looks a bit off. That's how we had to do it prior to DrMock<0.3.0. In DrMock>=0.3.0 the emits configuration call is added to Behavior and StateBehavior. It can be used to instruct a mock object to emit a signal:

// emit foo->theSignal("foo");  // Old, boring and explicit.
foo->mock.theSlot().push()
    .emits<const std::string&>(&IFoo::theSignal, "foo")
    .expects("bar")
    .times(1);  // Optional.
foo->theSlot("bar");  // `emit foo->theSignal("foo")` happens here!

Note. As in the case above, you have to be explicit when passing const T& instead of relying on template deduction. Only calling emits(&IFoo::theSignal, "foo") will result in a deduced conflicting types for parameter 'SigArgs' error.

Running the tests

Do make. If everything checks out, this should return

    Start 1: FooTest
1/1 Test #1: FooTest ..........................   Passed    0.02 sec

100% tests passed, 0 tests failed out of 1

Total Test time (real) =   0.02 sec

Event loops and the DRTEST_USE_QT macro

Some QObjects, such as QEventLoop require a QApplication to run in the main thread of the program to function correctly. To run a QApplication in the main thread during a DRTEST_TEST, define the DRTEST_USE_QT macro before including DrMock/Test.h.

Let's take a look at the previous example, but this time, connect the two FooMock instances via Qt::QueuedConnection:

DRTEST_TEST(useQt)
{
  auto foo = std::make_shared<FooMock>();
  auto bar = std::make_shared<FooMock>();

  QObject::connect(
      foo.get(), &IFoo::theSignal,
      bar.get(), &IFoo::theSlot,
      Qt::QueuedConnection  // Connect via event loop.
    );

  bar->mock.theSlot().push().times(1);
  emit foo->theSignal();

  // ...
}

To process the emitted signal, we must use an event loop, something like this:

DRTEST_TEST(useQt)
{
  // ...

  QCoreApplication::processEvents();

  DRTEST_ASSERT(bar->mock.verify());
}

If you run this application (don't forget to include <QCoreApplication>), you will receive the following error:

    Start 1: FooTest
1/1 Test #1: FooTest ..........................***Failed    0.02 sec
TEST   signalsAndSlots
*FAIL  signalsAndSlots (45): bar->mock.verify()
****************
1 FAILED

0% tests passed, 1 tests failed out of 1

Total Test time (real) =   0.02 sec

The following tests FAILED:
	  1 - FooTest (Failed)
Errors while running CTest
make: *** [default] Error 8

Due to the missing QCoreApplication in the main thread, processEvents() fails to process theSignal, so theSlot never gets called the expected number of times and bar->mock.verify() rightfully returns false.

Now add #define DRTEST_USE_QT before #include "DrMock/Test.h" and run the test again. The test should now succeed.