Search code examples
c++qttestingcmakecatch-unit-test

How can I make my first test with Catch for my Qt application?


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 );
}

Solution

  • 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.