1

我第一次尝试在 Qt应用程序上学习/使用 Catch ( https://github.com/catchorg/Catch2 )。

我正在尝试遵循 Catch 初始页面 ( https://github.com/catchorg/Catch2/blob/devel/docs/tutorial.md#top ) 上提供的教程。

上述教程的第一行说,理想情况下,我应该通过其“CMake 集成”(https://github.com/catchorg/Catch2/blob/devel/docs/cmake-integration.md#top)来使用 Catch2。我忠实地遵循“理想”的道路。

在“CMake 集成”页面的第二段中,我开始迷失:如果您不需要自定义主功能,您应该...

我需要自定义主函数吗?为什么有人需要一个?一个人没有一个人怎么活?我完全不知道,文本既没有解释这些,也没有提供任何合理的默认方向(如果你不知道我们在说什么,就假装你......或类似的东西)。

我试图忽略这一点,然后继续。

在第三段(根据请求在下面复制)呈现了一个代码块,读者会知道它应该足以完成代码块。代码块是做什么的?我应该将此代码包含在一些预先存在的文件中吗?哪个文件?在所述文件的哪一部分?或者我应该使用建议的内容创建一个新文件?哪个文件?我应该把它放在哪里?

这意味着如果系统上已经安装了 Catch2,应该足够了

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

有人可以提供一个在 Qt 项目中简单使用 Catch2 的工作示例吗?最好是桌面应用程序?

2022 年 1 月 14 日更新:

这是我对尝试实现最小的 Qt + Catch2 集成的看法,类似于 Catch 教程中的第一个示例(https://github.com/catchorg/Catch2/blob/v2.x/docs/tutorial.md#writing-tests)。

我创建了一个名为 QtCatch 的 Qt Widget 应用程序。这是它的文件结构:

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

我在下面包含了所有文件内容以供参考。

该文件结构是通过 Qt“新建项目”对话框创建的。主项目是“Application (Qt) > Qt Widgets Application”,测试子项目是“Other Project >> Auto Test Project”

我的 Qt 应用程序运行没有问题。

如果我尝试编译测试子项目或主项目取消注释主 CMakeLists.txt 文件中的“add_subdirectory(tests)”行,我会收到相同的错误:

对 Calculator::Calculator() 的未定义引用

尽管

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

tst_qtcatchtest.cpp中的行

如何使这个简单的 Catch2 测试用例在 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()

主.cpp:

#include "mainwindow.h"

#include <QApplication>

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

主窗口.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

主窗口.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);
}

主窗口.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>

包括/计算器.h:

#ifndef CALCULATOR_H
#define CALCULATOR_H


class Calculator
{
public:
    Calculator();

    int Factorial( int number );
};

#endif // CALCULATOR_H

包括/calculator.cpp:

#include "calculator.h"

Calculator::Calculator()
{

}

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

测试/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:

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

测试/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 );
}
4

2 回答 2

1

你的意思是你甚至不知道你是否需要一个自定义的主函数?!开个玩笑,当然,读起来很有趣,我同意这可以更清楚一点。但是,我熟悉 Catch2 和 CMake,所以我现在将消除所有疑问!

Catch2 测试需要程序函数中的少量代码main,以将命令行参数传递给其实现并开始运行您的测试用例。因此,为了方便起见,它提供了一个默认的 main 函数来为您执行此操作,这通常就足够了。他们自己的文档提供了一些示例,说明您如何提供自己的 main 来更改命令行的解析。另一种情况可能是您使用的外部库,它需要一些全局设置和/或清理。

所以是的,您确实需要一个或多个单独的可执行文件来进行测试,第三段显示了此类可执行文件的基本 CMake 设置。CMake 的主题过于广泛,无法在此答案中涵盖,但我通常使用如下相当标准的目录布局:

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

根 CMakeLists.txt 可用于全局定义,并添加具有自己 CMake 文件的子目录,例如:

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)

测试目标需要链接到与应用程序可执行文件本身相同的对象,因此最简单的配置是将源代码划分为库和可执行目标。示例 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)

请注意,使库目标为 STATIC 是这里最简单的解决方案,因为创建共享 Qt 库涉及额外的步骤

然后 tests/CMakeLists.txt 将使用 Catch2 文档中的命令:

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()

在此处禁用全局 CMAKE_AUTOMOC 是避免重复元对象编译的最简单方法。这将导致链接器错误,因为它已经为 foo_lib 完成了。

另请参阅此答案以获取有关如何扩展此设置以有条件地编译测试的示例,以便您可以默认禁用它们,但为您自己或自动构建测试系统启用它们。

于 2022-01-12T22:24:27.617 回答
1

在这里,我包含了我的问题的最终解决方案以及必要的 Qt 咒语以供将来参考。

请注意,我声称没有此解决方案的作者身份,因为它源自@sigma 的回答

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

这是最终文件版本。

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)

包括/calculator.cpp

#include "calculator.h"

Calculator::Calculator()
{

}

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

包括/计算器.h

#ifndef CALCULATOR_H
#define CALCULATOR_H


class Calculator
{
public:
    Calculator();

    int Factorial( int number );
};

#endif // CALCULATOR_H

主文件

#include "mainwindow.h"

#include <QApplication>

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

主窗口.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);
}

主窗口.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

主窗口.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>

测试/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)

测试/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);
}

测试/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 );
}
于 2022-01-23T11:07:07.887 回答