Модульное тестирование 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 и искал в Интернете, но не смог найти никаких указаний, как это сделать.

5

Решение

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

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

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

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

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

Другие решения

Похоже, что 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;
}

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

0