Модульное тестирование Catch.hpp: как динамически создавать тестовые случаи?

Я использую CATCH v1.1 build 14 для модульного тестирования моего кода C++.

В рамках тестирования я хотел бы проверить выходные данные нескольких модулей в моем коде. Количество модулей не установлено; другие модули могут быть добавлены в любое время. Однако код для проверки каждого модуля идентичен. Поэтому я думаю, что было бы идеально поместить код тестирования в for петля. На самом деле, используя catch.hppЯ проверил, что могу динамически создавать разделы в тестовом примере, где каждый раздел соответствует модулю. Я могу сделать это, приложив SECTION макрос в цикле for, например:

#include "catch.hpp"
#include <vector>
#include <string>
#include "myHeader.h"

TEST_CASE("Module testing", "[module]") {
    myNamespace::myManagerClass manager;
    std::vector<std::string> modList;
    size_t n;

    modList = manager.getModules();
    for (n = 0; n < modList.size(); n++) {
        SECTION(modList[n].c_str()) {
            REQUIRE(/*insert testing code here*/);
        }
    }
}

(Это не полный рабочий пример, но вы поняли идею.)

Вот моя дилемма. Я хотел бы протестировать модули независимо, чтобы в случае сбоя одного из модулей он продолжал тестировать другие модули, а не прерывать его. Однако, как работает CATCH, он прервет весь тестовый случай, если один REQUIRE выходит из строя. По этой причине я хотел бы создать отдельный контрольный пример для каждого модуля, а не просто отдельный раздел. Я пытался положить мой for петля за пределами TEST_CASE макрос, но этот код не компилируется (как я и ожидал):

#include "catch.hpp"
#include <vector>
#include <string>
#include "myHeader.h"

myNamespace::myManagerClass manager;
std::vector<std::string> modList;
size_t n;

modList = manager.getModules();
for (n = 0; n < modList.size(); n++) {
    TEST_CASE("Module testing", "[module]") {
        SECTION(modList[n].c_str()) {
            REQUIRE(/*insert testing code here*/);
        }
    }
}

Это можно сделать, написав мой собственный main(), но я не вижу, как именно это сделать. (Буду ли я поставить свой TEST_CASE код непосредственно в main()? Что делать, если я хочу сохранить свой TEST_CASE код в другом файле? Кроме того, это повлияет на мои другие, более стандартные тестовые случаи?)

Я также могу использовать CHECK макросы вместо REQUIRE макросы, чтобы избежать прерывания тестового примера, когда модуль выходит из строя, но затем я получаю противоположную проблему: он пытается продолжить тестирование на модуле, который должен был произойти сбой в начале. Если бы я мог просто поместить каждый модуль в свой собственный тестовый пример, это дало бы мне идеальное поведение.

Есть ли простой способ динамически создавать тестовые случаи в CATCH? Если да, можете ли вы дать мне пример того, как это сделать? Я прочитал документацию CATCH и искал в Интернете, но не смог найти никаких указаний, как это сделать.

2 ответа

Решение

Похоже, что Catch может перейти к тестированию на основе свойств, которое, я надеюсь, позволит динамически создавать тестовые случаи. А пока вот что я в итоге сделал.

Я создал .cpp файл с одним TEST_CASE для одного модуля и глобальной переменной для имени модуля. (Да, я знаю, что глобальные переменные - это зло, поэтому я осторожен и использую их как последнее средство):

module_unit_test.cpp:

#include "catch.hpp"
#include <string>
#include "myHeader.h"

extern const std::string g_ModuleName;  // global variable: module name

TEST_CASE("Module testing", "[module]") {
    myNamespace::myManagerClass manager;
    myNamespace::myModuleClass *pModule;
    SECTION(g_ModuleName.c_str()) {
        pModule = manager.createModule(g_ModuleName.c_str());
        REQUIRE(pModule != 0);
        /*insert more testing code here*/
    }
}

Затем я создаю исполняемый файл, который будет запускать этот тест на одном модуле, указанном в командной строке. (Я пытался перебирать Catch::Session().run() ниже, но Catch не позволяет запустить его более одного раза.) Файл объекта из кода ниже module_test.cpp и из кода модульного теста выше module_unit_test.cpp связаны при создании исполняемого файла.

module_test.cpp:

#define CATCH_CONFIG_RUNNER
#include "catch.hpp"
#include <string>
#include <cstdio>

std::string g_ModuleName;  // global variable: module name

int main(int argc, char* argv[]) {
    // Make sure the user specified a module name.
    if (argc < 2) {
        std::cout << argv[0] << " <module name> <Catch options>" << std::endl;
        return 1;
    }

    size_t n;
    char* catch_argv[argc-1];
    int result;

    // Modify the input arguments for the Catch Session.
    // (Remove the module name, which is only used by this program.)
    catch_argv[0] = argv[0];
    for (n = 2; n < argc; n++) {
        catch_argv[n-1] = argv[n];
    }

    // Set the value of the global variable.
    g_ModuleName = argv[1];

    // Run the test with the modified command line arguments.
    result = Catch::Session().run(argc-1, catch_argv);

    return result;
}

Затем я делаю цикл в отдельном исполняемом файле (не связанном с объектными файлами из кода выше):

module_test_all.cpp:

#include <cstdlib>
#include <vector>
#include <string>
#include "myHeader.h"

int main(int argc, char* argv[]) {
    std::string commandStr;
    int result, status = 0;
    myNamespace::myManagerClass manager;
    std::vector<std::string> modList;
    size_t m, n;

    // Scan for modules.
    modList = manager.getModules();

    // Loop through the module list.
    for (n = 0; n < modList.size(); n++) {
        // Build the command line.
        commandStr = "module_test " + modList[n];
        for (m = 1; m < argc; m++) {
            commandStr += " ";
            commandStr += argv[m];
        }

        // Do a system call to the first executable.
        result = system(commandStr.c_str());

        // If a test fails, I keep track of the status but continue
        // looping so all the modules get tested.
        status = status ? status : result;
    }

    return status;
}

Да, это некрасиво, но я подтвердил, что это работает.

Есть способ достичь того, что вы ищете, но я бы заметил, что вы идете по этому пути неправильно:-

Модульные тесты предназначены для тестирования каждого модуля, то есть вы пишете компонент и тест для проверки правильности поведения этого компонента. Если позже вы решите каким-либо образом изменить один компонент, вы обновите соответствующий тест.

Если вы объединяете все свои тесты для всех ваших компонентов в один файл, становится намного сложнее изолировать модуль, который ведет себя по-разному.

Если вы хотите исключить тестирование компонента, поскольку оно практически одинаково для всех ваших компонентов, вы можете выполнить одно из следующих действий:

1. Распакуйте общие тесты в отдельный заголовочный файл.

Вы можете # определить имя типа компонента, который вы хотите протестировать, а затем включить заголовочный файл со всеми тестами в нем:

// CommonTests.t.h
#include "catch.hpp"
TEST_CASE("Object Can be instantiated", "[ctor]")
{
   REQUIRE_NOTHROW(COMPONENT component);
}

// SimpleComponent.t.cpp
#define COMPONENT SimpleComponent
#include "CommonTests.t.h"

Это просто сделать, но имеет недостаток - при запуске тестового прогона у вас будут дубликаты тестов (по имени), поэтому вы сможете запускать только все тесты или по тегам.

Вы можете решить эту проблему, оцифровав имя компонента и предварительно добавив его к имени тестового примера.

** 2. Вызовите общие тесты путем параметризации компонента **

Поместите ваши общие тесты в отдельный файл и напрямую вызовите общие методы тестирования:

// CommonTests.t.h
void RunCommonTests(ComponentInterface& itf);

// CommonTests.t.cpp
void RunCommonTests(ComponentInterface& itf)
{
  REQUIRE(itf.answerToLifeUniverseAndEverything() == 42);
}

// SimpleComponent.t.cpp
#include "SimpleComponent.h"
#include "CommonTest.t.h"
#include "catch.hpp"

TEST_CASE("SimpleComponent is default-constructible", "[ctor]")
{
   REQUIRE_NOTHROW(SimpleComponent sc);
}

TEST_CASE("SimpleComponent behaves like common components", "[common]")
{
  SimpleComponent sc;
  RunCommonTests(sc);
}
Другие вопросы по тегам