1

I am trying to learn/use Catch (https://github.com/catchorg/Catch2) for the first time on a Qt applcation.

I am trying to follow the tutorial presented on Catch's initial page (https://github.com/catchorg/Catch2/blob/devel/docs/tutorial.md#top).

The first line of the above tutorial says that ideally I should be using Catch2 through its "CMake integration" (https://github.com/catchorg/Catch2/blob/devel/docs/cmake-integration.md#top). I faithfully follow the "ideal" path.

On the second paragraph of the "CMake integration" page I start to get lost: If you do not need custom main function, you should...

Do I need a custom main function? Why would anyone need one? How can a person live without one? I have no idea at all and the text neither explains any of this nor provides any kind of sensible default orientation (If you don't know what we are talking about just pretend you... or something similar).

I tried to ignore that and just follow on.

On the third paragraph (reproduced below per request) it is presented a block of code and the reader gets to know that it should be enough to do the block of code. What is to do a block of code? Should I include this code in some pre existing file? Which file? In what part of said file? Or should I create a new file with the proposed content? Which file? Where should I put it?

This means that if Catch2 has been installed on the system, it should be enough to do

> find_package(Catch2 3 REQUIRED)
> # These tests can use the Catch2-provided main add_executable(tests test.cpp) target_link_libraries(tests PRIVATE Catch2::Catch2WithMain)
> 
> # These tests need their own main add_executable(custom-main-tests test.cpp test-main.cpp) target_link_libraries(custom-main-tests
> PRIVATE Catch2::Catch2)

Can someone please present an working example of a simple use of Catch2 on a Qt project? Preferably a desktop application?

Update 2022-01-14:

Here is my take on trying to implement a minimal Qt + Catch2 integration similar to the first example in Catch's tutorial (https://github.com/catchorg/Catch2/blob/v2.x/docs/tutorial.md#writing-tests).

I created a Qt Widget application called QtCatch. Here is it's file structure:

.
├── CMakeLists.txt
├── include
│   ├── calculator.cpp
│   └── calculator.h
├── main.cpp
├── mainwindow.cpp
├── mainwindow.h
├── mainwindow.ui
└── tests
    ├── CMakeLists.txt
    ├── main.cpp
    └── tst_qtcatchtest.cpp

I included all files contents below for reference.

This file structure was created through Qt "New Project" dialog box. The main project is a "Application (Qt) > Qt Widgets Application" and the tests subproject is a "Other Project >> Auto Test Project"

My Qt app runs without problem.

If I try to compile either the tests subproject or the main project uncommenting the "add_subdirectory(tests)" line in main CMakeLists.txt file I get the same error:

undefined reference to Calculator::Calculator()

despite the

#include "../include/calculator.h"

line in tst_qtcatchtest.cpp

How can I make this simple Catch2 test case work in Qt 6?

CMakeLists.txt:

cmake_minimum_required(VERSION 3.5)

project(QtCatch VERSION 0.1 LANGUAGES CXX)

set(CMAKE_INCLUDE_CURRENT_DIR ON)

set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(QT NAMES Qt6 Qt5 COMPONENTS Widgets REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets REQUIRED)

# Manually added
#add_subdirectory(tests)

set(PROJECT_SOURCES
        main.cpp
        mainwindow.cpp
        mainwindow.h
        mainwindow.ui
        include/calculator.h include/calculator.cpp
)

if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
    qt_add_executable(QtCatch
        MANUAL_FINALIZATION
        ${PROJECT_SOURCES}
    )
# Define target properties for Android with Qt 6 as:
#    set_property(TARGET QtCatch APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR
#                 ${CMAKE_CURRENT_SOURCE_DIR}/android)
# For more information, see https://doc.qt.io/qt-6/qt-add-executable.html#target-creation
else()
    if(ANDROID)
        add_library(QtCatch SHARED
            ${PROJECT_SOURCES}
        )
# Define properties for Android with Qt 5 after find_package() calls as:
#    set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android")
    else()
        add_executable(QtCatch
            ${PROJECT_SOURCES}
        )
    endif()
endif()

target_link_libraries(QtCatch PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)

set_target_properties(QtCatch PROPERTIES
    MACOSX_BUNDLE_GUI_IDENTIFIER my.example.com
    MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
    MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
)

if(QT_VERSION_MAJOR EQUAL 6)
    qt_finalize_executable(QtCatch)
endif()

main.cpp:

#include "mainwindow.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}

mainwindow.h:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void on_factorialPushButton_clicked();

private:
    Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H

mainwindow.cpp:

#include "mainwindow.h"
#include "./ui_mainwindow.h"

#include "include/calculator.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
}

MainWindow::~MainWindow()
{
    delete ui;
}


void MainWindow::on_factorialPushButton_clicked()
{
    Calculator aCalc;
    int factorial = aCalc.Factorial(ui->numberLineEdit->text().toInt());
    QString result = QString("Result: %1").arg(factorial);
    ui->resultLabel->setText(result);
}

mainwindow.ui:

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>800</width>
    <height>214</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <layout class="QVBoxLayout" name="verticalLayout">
    <item>
     <layout class="QHBoxLayout" name="horizontalLayout">
      <item>
       <widget class="QLabel" name="numberLabel">
        <property name="text">
         <string>Number</string>
        </property>
       </widget>
      </item>
      <item>
       <widget class="QLineEdit" name="numberLineEdit"/>
      </item>
      <item>
       <widget class="QPushButton" name="factorialPushButton">
        <property name="text">
         <string>Calculate Factorial</string>
        </property>
       </widget>
      </item>
     </layout>
    </item>
    <item>
     <widget class="QLabel" name="resultLabel">
      <property name="text">
       <string>Result</string>
      </property>
     </widget>
    </item>
   </layout>
  </widget>
  <widget class="QStatusBar" name="statusbar"/>
 </widget>
 <resources/>
 <connections/>
</ui>

include/calculator.h:

#ifndef CALCULATOR_H
#define CALCULATOR_H


class Calculator
{
public:
    Calculator();

    int Factorial( int number );
};

#endif // CALCULATOR_H

include/calculator.cpp:

#include "calculator.h"

Calculator::Calculator()
{

}

int Calculator::Factorial( int number )
{
    return number <= 1 ? 1      : Factorial( number - 1 ) * number;
}

tests/CMakeLists.txt:

cmake_minimum_required(VERSION 3.5)

project(QtCatch VERSION 0.1 LANGUAGES CXX)

set(CMAKE_INCLUDE_CURRENT_DIR ON)

set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(QT NAMES Qt6 Qt5 COMPONENTS Widgets REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets REQUIRED)

# Manually added
add_subdirectory(tests)

set(PROJECT_SOURCES
        main.cpp
        mainwindow.cpp
        mainwindow.h
        mainwindow.ui
        include/calculator.h include/calculator.cpp
)

if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
    qt_add_executable(QtCatch
        MANUAL_FINALIZATION
        ${PROJECT_SOURCES}
    )
# Define target properties for Android with Qt 6 as:
#    set_property(TARGET QtCatch APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR
#                 ${CMAKE_CURRENT_SOURCE_DIR}/android)
# For more information, see https://doc.qt.io/qt-6/qt-add-executable.html#target-creation
else()
    if(ANDROID)
        add_library(QtCatch SHARED
            ${PROJECT_SOURCES}
        )
# Define properties for Android with Qt 5 after find_package() calls as:
#    set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android")
    else()
        add_executable(QtCatch
            ${PROJECT_SOURCES}
        )
    endif()
endif()

target_link_libraries(QtCatch PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)

set_target_properties(QtCatch PROPERTIES
    MACOSX_BUNDLE_GUI_IDENTIFIER my.example.com
    MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
    MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
)

if(QT_VERSION_MAJOR EQUAL 6)
    qt_finalize_executable(QtCatch)
endif()

tests/main.cpp:

#define CATCH_CONFIG_RUNNER
#include <catch2/catch.hpp>
#include <QtGui/QGuiApplication>

int main(int argc, char** argv)
{
    QGuiApplication app(argc, argv);
    return Catch::Session().run(argc, argv);
}

tests/tst_qtcatchtest.cpp:

#include <catch2/catch.hpp>

#include "../include/calculator.h"

TEST_CASE( "Factorial of 0 is 1 (fail)", "[qt]" ) {
    Calculator aCalc;

    REQUIRE( aCalc.Factorial(0) == 1 );
}

TEST_CASE( "Factorials of 1 and higher are computed (pass)", "[qt]" ) {
    Calculator aCalc;

    REQUIRE( aCalc.Factorial(1) == 1 );
    REQUIRE( aCalc.Factorial(2) == 2 );
    REQUIRE( aCalc.Factorial(3) == 6 );
    REQUIRE( aCalc.Factorial(10) == 3628800 );
}
Rsevero
  • 157
  • 2
  • 11
  • ***If you do not need custom main function,...*** as per the docs, it probably means if you need a separate executable for tests. `# These tests need their own main` this line is in the code below that para and below this line is the `add_executable`. – kiner_shah Jan 11 '22 at 13:23
  • ***it should be enough to do the block of code...*** maybe you have to provide the exact link to this para (or you can copy and paste it in the post itself). – kiner_shah Jan 11 '22 at 13:28
  • @kiner_shah, I don't know if I need a separate executable for tests. Do I? There is no info the above mentioned pages to give a clue to a Catch begginner to help they decide if there is need for a separate executable. – Rsevero Jan 11 '22 at 17:32
  • @kiner_shah, the link is in the first version of my question but I will repeat it here anyway: https://github.com/catchorg/Catch2/blob/devel/docs/cmake-integration.md#top It's in the third pargraph. – Rsevero Jan 11 '22 at 17:34
  • By third paragraph you mean third section? It's unclear to me. Maybe you should just add the contents of this "paragraph" in the question itself. – kiner_shah Jan 12 '22 at 05:48
  • BTW, if you feel that docs of that repo can be improved, you can (1) raise an issue and ask for enhancement, or (2) do the changes as per your understanding yourself and submit a pull request. – kiner_shah Jan 12 '22 at 05:56
  • I've just included the reproduction you asked despite not seeing the real value of it. All the references of Catch's docs in my question serve the sole purpose of demonstrating that I tried to solved the questions issue by myself before coming here. BUT the real question, as stated is "Can someone please present an working example of a simple use of Catch2 on a Qt project? Preferably a desktop application?" – Rsevero Jan 12 '22 at 11:56
  • @kiner_shah, I do feel that Catch repo docs need improv and the first thing I did after realizing it was to create an issue about it (https://github.com/catchorg/Catch2/issues/2351). If I actually knew the missing info I would (1) answer myself this StackOverFlow question (2) proposed a patch for Catch's docs but unfortunatelly I am definetely not there yet. – Rsevero Jan 12 '22 at 11:59
  • https://github.com/catchorg/Catch2/blob/devel/docs/own-main.md This may be helpful for custom main. – kiner_shah Jan 12 '22 at 12:15

2 Answers2

3

You mean you do not even know whether you need a custom main function?! Just kidding, of course, that was entertaining to read and I agree this could be made a little clearer. However, I am familiar with Catch2 and CMake, so I shall now expel all doubt!

Catch2 tests need a small amount of code in a program's main function, to pass the command line arguments to its implementation and start running your test cases. So, as a convenience, it offers a default main function that does this for you, which is normally sufficient. Their own documentation gives some examples of how you might supply your own main to alter the parsing of the command line. Another case could be an external library you use that requires some global setup and/or cleanup.

So yes, you do need one or more separate executables for your tests, and the third paragraph shows the basic CMake setup for such an executable. CMake is far too broad of a topic to cover in this answer, but I typically use a fairly standard directory layout like this:

|- build/ // all compilation output
|- src/
|  |- // project sources
|  |- CMakeLists.txt
|- tests/
|  |- test.cpp
|  |- CMakeLists.txt
|- CMakeLists.txt

The root CMakeLists.txt can be used for global definitions and adds the subdirectories that have their own CMake files, for example:

cmake_minimum_required(VERSION 3.5)
project(baz LANGUAGES CXX VERSION 0.0.1)

find_package(Qt5 CONFIG REQUIRED COMPONENTS Core Gui)

add_subdirectory(src)
add_subdirectory(tests)

The test target(s) will need to be linked against the same objects as the application executable itself, so the easiest configuration is to divide your source code into a library and executable target. Example src/CMakeLists.txt:

set(CMAKE_AUTOMOC ON)
set(lib_SRC
    foo.cpp
    bar.cpp
    // sources excluding main.cpp
)
add_library(foo_lib STATIC ${lib_SRC})
target_link_libraries(foo_lib Qt5::Core)

add_executable(foo main.cpp)
target_link_libraries(foo foo_lib)

Note that making the library target STATIC is the easiest solution here, as creating shared Qt libraries involves additional steps.

Then tests/CMakeLists.txt would use commands as in the Catch2 documentation:

set(CMAKE_AUTOMOC OFF)
find_package(Catch2 3 REQUIRED)

add_executable(test test.cpp)
target_link_libraries(test PRIVATE foo_lib Catch2::Catch2WithMain)

include(CTest)
include(Catch)
catch_discover_tests()

Disabling the global CMAKE_AUTOMOC here is the easiest way to avoid duplicate meta-object compilation. This would cause linker errors as it has already been done for foo_lib.

See also this answer for an example of how to extend this setup to compile the tests conditionally, so that you could disable them by default, but enable them for yourself or automated build testing systems.

sigma
  • 2,758
  • 1
  • 14
  • 18
  • first of all, thanks for your help. Can you add a minimal code example (where there is some regular Qt code) and some Catch code testing it? Something like the Qt version of Catch's tutorial initial test case (https://github.com/catchorg/Catch2/blob/v2.x/examples/010-TestCase.cpp)? I am having trouble making the Test subproject properly relate to the main project: simple includes don't work, i.e., despite having included the proper files my test subproject won't compile beacuse it can't find my classes constructors. – Rsevero Jan 14 '22 at 19:10
  • @Rsevero: Forgot about that! My most recent tests were for library code, so I just linked the tests to that target. Making a library for both the application and tests to link against is probably also the best way to go for you, I'll add some more example code to illustrate. – sigma Jan 14 '22 at 23:12
  • @Rsevero I forgot to mention that Qt also has an official test framework: [QTest](https://doc.qt.io/qt-5/qtest-overview.html). Although you can get everything to work just as well with Catch2, certain Qt features, like signal/slot connections, depend on a running event loop. If necessary for your tests, that's something you would need to set up in a custom main function with Catch2, but would work by default with QTest. – sigma Jan 23 '22 at 23:19
2

Here I include the final solution for my question with the necessary Qt incantations for future reference.

Please be aware that I claim no authorship of this solution as it is derived from @sigma's answer

├── CMakeLists.txt
├── include
│   ├── calculator.cpp
│   └── calculator.h
├── main.cpp
├── mainwindow.cpp
├── mainwindow.h
├── mainwindow.ui
└── tests
    ├── CMakeLists.txt
    ├── main.cpp
    └── tst_qtcatchtest.cpp

Here are the final file versions.

CMakeLists.txt

cmake_minimum_required(VERSION 3.5)

project(QtCatch VERSION 0.1 LANGUAGES CXX)

enable_testing()

set(CMAKE_INCLUDE_CURRENT_DIR ON)

set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(QT NAMES Qt6 Qt5 COMPONENTS Widgets REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets REQUIRED)

if(CMAKE_TESTING_ENABLED)
    add_subdirectory(tests)
endif()

set(LIB_SOURCES
        mainwindow.cpp
        mainwindow.h
        mainwindow.ui
        include/calculator.h include/calculator.cpp
)

add_library(QtCatchLib STATIC ${LIB_SOURCES})

target_link_libraries(QtCatchLib PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)

add_executable(QtCatch main.cpp)
target_link_libraries(QtCatch PRIVATE QtCatchLib)
target_link_libraries(QtCatch PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)

include/calculator.cpp

#include "calculator.h"

Calculator::Calculator()
{

}

int Calculator::Factorial( int number )
{
    return number <= 1 ? 1 : Factorial( number - 1 ) * number;
}

include/calculator.h

#ifndef CALCULATOR_H
#define CALCULATOR_H


class Calculator
{
public:
    Calculator();

    int Factorial( int number );
};

#endif // CALCULATOR_H

main.cpp

#include "mainwindow.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}

mainwindow.cpp

#include "mainwindow.h"
#include "./ui_mainwindow.h"

#include "include/calculator.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_factorialPushButton_clicked()
{
    Calculator aCalc;
    int factorial = aCalc.Factorial(ui->numberLineEdit->text().toInt());
    QString result = QString("Result: %1").arg(factorial);
    ui->resultLabel->setText(result);
}

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void on_factorialPushButton_clicked();

private:
    Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H

mainwindow.ui

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>800</width>
    <height>214</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <layout class="QVBoxLayout" name="verticalLayout">
    <item>
     <layout class="QHBoxLayout" name="horizontalLayout">
      <item>
       <widget class="QLabel" name="numberLabel">
        <property name="text">
         <string>Number</string>
        </property>
       </widget>
      </item>
      <item>
       <widget class="QLineEdit" name="numberLineEdit"/>
      </item>
      <item>
       <widget class="QPushButton" name="factorialPushButton">
        <property name="text">
         <string>Calculate Factorial</string>
        </property>
       </widget>
      </item>
     </layout>
    </item>
    <item>
     <widget class="QLabel" name="resultLabel">
      <property name="text">
       <string>Result</string>
      </property>
     </widget>
    </item>
   </layout>
  </widget>
  <widget class="QStatusBar" name="statusbar"/>
 </widget>
 <resources/>
 <connections/>
</ui>

tests/CMakeLists.txt

cmake_minimum_required(VERSION 3.5)

project(QtCatchTest LANGUAGES CXX)

SET(CMAKE_CXX_STANDARD 11)

find_package(QT NAMES Qt6 Qt5 COMPONENTS Gui REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Gui REQUIRED)

add_executable(QtCatchTest tst_qtcatchtest.cpp main.cpp)

target_link_libraries(QtCatchTest PRIVATE Qt${QT_VERSION_MAJOR}::Gui)
target_link_libraries(QtCatchTest PRIVATE QtCatchLib)

tests/main.cpp

#define CATCH_CONFIG_RUNNER
#include <catch2/catch.hpp>
#include <QtGui/QGuiApplication>

int main(int argc, char** argv)
{
    QGuiApplication app(argc, argv);
    return Catch::Session().run(argc, argv);
}

tests/tst_qtcatchtest.cpp

#include <catch2/catch.hpp>

#include "../include/calculator.h"

TEST_CASE( "Factorial of 0 is 1 (fail)", "[qt]" ) {
    Calculator *aCalc = new Calculator();

    REQUIRE( aCalc->Factorial(0) == 1 );
}

TEST_CASE( "Factorials of 1 and higher are computed (pass)", "[qt]" ) {
    Calculator aCalc;

    REQUIRE( aCalc.Factorial(1) == 1 );
    REQUIRE( aCalc.Factorial(2) == 2 );
    REQUIRE( aCalc.Factorial(3) == 6 );
    REQUIRE( aCalc.Factorial(10) == 3628800 );
}
Rsevero
  • 157
  • 2
  • 11