0

Related Posts:

Injecting a mock of a QTimer

Qt5/QTest: How to mock the clock speed (QTimer, etc.)?

Why they don't answer my question:

  1. The only hint given in the first is, "Mock it using composition." There is no answer or code example. I also don't want to test if QTimer::start was called, but what I want is control over when the timer fires its signal so that I can test deterministically, and without waiting needlessly for any amount of time.

  2. The second is talks about manipulating CPU time, which is not what I want to do at all. Nor is it what the OP of that post should be doing. It does talk about directly calling the slot the timer is hooked up to, but that isn't possible without changing production code and making the slot accessible to the outside.

So, the question once again is, how can we mock QTimer? ...Or at least, have the ability to test a class that uses it deterministically?

Here is a minimal example demonstrating what I need and why I need it. The entire thing is currently checked into https://github.com/ChristopherPisz/MockQTimer But I will leave the listing of the minimal example here as well:

cow.hpp

#ifndef MOCKQTIMER_COW_HPP
#define MOCKQTIMER_COW_HPP

#include <QObject>
#include <QTimer>

class Cow : public QObject
{
    Q_OBJECT
public:
    Cow();
    ~Cow() override = default;

signals:
    void signalSaidMoo();

private slots:
    void onMooDue();

private:
    QTimer m_timerMoo;
};

#endif // MOCKQTIMER_COW_HPP

cow.cpp

#include "cow.hpp"
#include <iostream>


unsigned const defaultMooIntervalMilliseconds = 1000;

Cow::Cow()
:
QObject()
{
    m_timerMoo.setInterval(defaultMooIntervalMilliseconds);
    QObject::connect(&m_timerMoo, &QTimer::timeout, this, &Cow::onMooDue);
    m_timerMoo.start();
}

void Cow::onMooDue()
{
    std::cout << "The cow says Moo" << std::endl;
    emit signalSaidMoo();
}

animal_tests.hpp

#ifndef MOCKQTIMER_ANIMAL_TESTS_HPP
#define MOCKQTIMER_ANIMAL_TESTS_HPP

#include "cow.hpp"
#include <QCoreApplication>
#include <QtTest>


class animal_tests : public QObject
{
Q_OBJECT

public:
    animal_tests();
    ~animal_tests() override = default;

public slots:

    /*
     * @brief Called when the Cow's Moo signal is fired
     */
    void onCowMooed();

private slots:
    void init();
    void cleanup();

    void test_moo();

private:
    Cow      m_testCow;
    unsigned m_countMoo;
};

#endif // MOCKQTIMER_ANIMAL_TESTS_HPP

animal_tests.cpp

#include "animal_tests.hpp"


animal_tests::animal_tests()
    :
    m_countMoo(0)
{}

void animal_tests::onCowMooed()
{
    ++m_countMoo;
}

void animal_tests::init()
{
    m_countMoo = 0;
    connect(&m_testCow, &Cow::signalSaidMoo, this, &animal_tests::onCowMooed);
}

void animal_tests::cleanup()
{
}

void animal_tests::test_moo()
{
    // We want to test that the cow moos once a second
    //
    // This is terrible to actually wait a second using timers and non deterministic results! We had to add an extra 500 ms to make it pass
    // and that is completely dependant on cpu speed and accuracy of timer. We also don't want to wait eons for the unit tests to complete.
    // We need dependency injection and a mock timer, so we can control when the timer fires!
    QVERIFY(QTest::qWaitFor([this] () { return m_countMoo >= 3; }, 3500));
}

QTEST_MAIN(animal_tests)

CMakeLists.txt

cmake_minimum_required(VERSION 3.0)
project(mockqtimer)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)

find_package(Qt5 5.14 REQUIRED COMPONENTS Core Test)

# Library
set(animals_MOC_HEADERS
        cow.hpp
        )

add_library(animals
        ${animals_MOC_HEADERS}
        cow.cpp
        )

target_link_libraries(animals PRIVATE
        Qt5::Core
        )

# Tests
set(animal_tests_MOC_HEADERS
        ${animals_MOC_HEADERS}
        animal_tests.hpp
        )

add_executable(animal_tests
        ${animal_tests_MOC_HEADERS}
        animal_tests.cpp
        )

add_test(NAME animal_tests COMMAND animal_tests)

target_link_libraries(animal_tests PRIVATE
        Qt5::Test
        Qt5::Core
        animals
        )
Christopher Pisz
  • 3,757
  • 4
  • 29
  • 65
  • Maybe i do not understand you correctly, but i think you should not mock timer. All you need is to mock you `cow`. – Andrey Semenov Jul 23 '20 at 16:02
  • @AndreySemenov I'm fine with that, but I do not fully envision how I would go about mocking the cow such that I can test that onCowMooed() is signaled properly when the timer elapses, while being deterministic. Can you write up a code example of what the mock and test would look like and I can mark it as an answer? – Christopher Pisz Jul 23 '20 at 17:18

1 Answers1

0

Ok, first i would like to change your slot visibility from private to protected. That can help with mocking and not brake encapsulation. So your cow code will look like that. And add method to start timer. Cow.h

#ifndef COW_H
#define COW_H

#include <QObject>
#include <QTimer>
#include <optional>

class Cow : public QObject
{
    Q_OBJECT
public:
    explicit Cow(std::optional<size_t> interval = {}, bool startTimer = true, QObject *parent = nullptr);
    void startTimer();
signals:
    void signalSaidMoo();

protected slots:
    void onMooDue();

private:
    QTimer m_timerMoo;
};

#endif // COW_H

Cow.cpp

#include "Cow.h"
#include <iostream>

namespace {
constexpr size_t defaultMooIntervalMilliseconds = 1000;
} // namespace

Cow::Cow(std::optional<size_t> interval, bool startTimer, QObject *parent)
    : QObject(parent) {
    if(interval.has_value()) {
        m_timerMoo.setInterval(interval.value());
    } else {
        m_timerMoo.setInterval(defaultMooIntervalMilliseconds);
    }
    QObject::connect(&m_timerMoo, &QTimer::timeout, this, &Cow::onMooDue);
    if(startTimer) {
        m_timerMoo.start();
    }
}

void Cow::startTimer() {
    if(m_timerMoo.isActive()) {
        std::cout << "Could not start timer, it is already active" << std::endl;
        return;
    }
    m_timerMoo.start();
}

void Cow::onMooDue()
{
    std::cout << "The cow says Moo" << std::endl;
    emit signalSaidMoo();
}

MocCow.h

#ifndef MOCKCOW_H
#define MOCKCOW_H

#include "Cow.h"

class MockCow : public Cow
{
    Q_OBJECT
public:
    explicit MockCow(size_t iterationsCount = 0)
        : Cow(1, false),
          mIterationsCount(iterationsCount) {}
    void mockMooTriger() {
         onMooDue();
    }
    void mockMooSeriesTriger() {
        connect(this, &MockCow::signalSaidMoo, this, [&] {
             if(mCurrentIteration++ < mIterationsCount) {
                 onMooDue();
                 startTimer();
             }
        });
        startTimer();
    }
private:
    size_t mIterationsCount;
    size_t mCurrentIteration{1};
};
#endif // COW_H

animal_tests.hpp

#ifndef MOCKQTIMER_ANIMAL_TESTS_HPP
#define MOCKQTIMER_ANIMAL_TESTS_HPP

#include "Cow.h"
#include <QCoreApplication>
#include <QtTest>


class animal_tests : public QObject
{
Q_OBJECT

public:
    animal_tests();
    ~animal_tests() override = default;

public slots:

    /*
     * @brief Called when the Cow's Moo signal is fired
     */
    void onCowMooed();

private slots:
    void init();
    void cleanup();

    void test_moo();
    void test_mock_moo_once();
    void test_mock_moo_series();

private:
    Cow      m_testCow;
    unsigned m_countMoo;
};

#endif // MOCKQTIMER_ANIMAL_TESTS_HPP

animal_tests.cpp

#include "animal_tests.hpp"
#include "MockCow.h"

animal_tests::animal_tests()
    :
    m_countMoo(0)
{}

void animal_tests::onCowMooed()
{
    ++m_countMoo;
}

void animal_tests::init()
{
    m_countMoo = 0;
    connect(&m_testCow, &Cow::signalSaidMoo, this, &animal_tests::onCowMooed);
}

void animal_tests::cleanup()
{
}

void animal_tests::test_moo()
{
    // We want to test that the cow moos once a second
    //
    // This is terrible to actually wait a second using timers and non deterministic results! We had to add an extra 500 ms to make it pass
    // and that is completely dependant on cpu speed and accuracy of timer. We also don't want to wait eons for the unit tests to complete.
    // We need dependency injection and a mock timer, so we can control when the timer fires!
    QVERIFY(QTest::qWaitFor([this] () { return m_countMoo >= 3; }, 3500));
}

void animal_tests::test_mock_moo_once() {
    MockCow cow;
    QSignalSpy signalSpy(&cow, &MockCow::signalSaidMoo);
    cow.mockMooTriger();
    QCOMPARE(signalSpy.count(), 1);
}

void animal_tests::test_mock_moo_series() {
    MockCow cow(3);
    QSignalSpy signalSpy(&cow, &MockCow::signalSaidMoo);
    cow.mockMooSeriesTriger();
    signalSpy.wait(100);
    QCOMPARE(signalSpy.count(), 3);
}

QTEST_MAIN(animal_tests)

CMakeLists.txt

cmake_minimum_required(VERSION 3.0)
project(mockqtimer)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)

find_package(Qt5 REQUIRED COMPONENTS Core Test)

# Library
set(animals_MOC_HEADERS
        Cow.h
        )

add_library(animals
        ${animals_MOC_HEADERS}
        Cow.cpp
        )

target_link_libraries(animals PRIVATE
        Qt5::Core
        )

# Tests
set(animal_tests_MOC_HEADERS
        ${animals_MOC_HEADERS}
        animal_tests.hpp
        MockCow.h
        )

add_executable(animal_tests
        ${animal_tests_MOC_HEADERS}
        animal_tests.cpp
        )

add_test(NAME animal_tests COMMAND animal_tests)

target_link_libraries(animal_tests PRIVATE
        Qt5::Test
        Qt5::Core
        animals
        )
Andrey Semenov
  • 901
  • 11
  • 17