使用 Catch2 编译多个测试源的正确方法是什么?

What is the correct way to compile multiple test sources with Catch2?

本文关键字:方法 是什么 编译 Catch2 测试 使用      更新时间:2023-10-16

我有以下项目结构:

test_main.cc

#define CATCH_CONFIG_MAIN
#include "catch2.hpp"

test1.cc

#include "catch2.hpp"
#include "test_utils.hpp"
TEST_CASE("test1", "[test1]") {
REQUIRE(1 == 1);
}

test2.cc

#include "catch2.hpp"
#include "test_utils.hpp"
TEST_CASE("test2", "[test2]") {
REQUIRE(2 == 2);
}

test_utils.hpp

#pragma once
#include <iostream>
void something_great() {
std::cout << ":)n";
}

如果我使用clang++ -std=c++17 test_main.cc test1.cc test2.cc之类的东西进行编译,函数something_great在 test1.o 和 test2.o 中都有定义。这会导致类似

duplicate symbol __Z15something_greatv in:
test1.cc.o
test2.cc.o
ld: 1 duplicate symbol for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

在 Catch2 文档的"向上扩展"部分中,他们提到为了拆分测试,您可能需要

使用尽可能多的其他 cpp 文件(或任何您称之为 实现文件)作为测试的需要,但是分区 对您的工作方式最有意义。每个额外的文件需要 只有 #include"catch.hpp">

但是在文档的示例部分中,我没有看到像我这样的用例。我读了这篇博文,其中描述了三种对我没有吸引力的解决方案:将函数定义为宏,或使函数staticinline

有没有另一种方法来编译这些文件,这些文件会产生一个具有test_main.cc定义的 main 函数的可执行文件?

这实际上与 Catch 或测试无关。当您#includeC++中的文件时,它会逐字复制粘贴到#include行。如果你把自由函数定义放在头中,你会看到构建实际程序等问题,等等。

根本的问题是#include与大多数语言中的等效指令(importrequire等)不是同一类型的导入模块指令,它们在这种情况下做了理智的事情(确认标头与我们已经看到的标头相同,并忽略重复的方法定义)。

建议你编写inline的注释者在技术上是正确的,从某种意义上说,这将"解决你的问题",因为你的编译器不会多次为该方法生成目标代码。但是,它并没有真正解释正在发生的事情或解决根本问题。


干净的解决方案是:

  • test_utils.hpp中,将方法定义替换为方法声明void something_great();
  • 使用方法的定义创建test_utils.cc(您当前在.hpp中具有)。
  • clang++ -std=c++17 test1.cc -c
  • clang++ -std=c++17 test2.cc -c
  • clang++ -std=c++17 test_main.cc -c
  • clang++ -std=c++17 test_utils.cc -c
  • clang++ -std=c++17 test1.o test2.o test_utils.o test_main.o

我还建议您阅读以下内容:定义和声明之间有什么区别?

明确地:

// test_utils.hpp
#pragma once
// This tells the compiler that when the final executable is linked,
// there will be a method named something_great which takes no arguments
// and returns void defined; the definition lives in test_utils.o in our
// case, although in practice the definition could live in any .o file
// in the final linking clang++ call.
void something_great();

和:

// test_utils.cpp
#include "test_utils.hpp"
#include <iostream>
// Generates a DEFINITION for something_great, which
// will get put in test_utils.o.
void something_great() { std::cout << "Hin"; }

似乎您担心每次更改测试时都会"重新编译 Catch"。我不想打破它给你,但你现在处于C++境:你将毫无意义地重新编译东西很多。当包含它们的源文件更改时,像 Catch CATCH 这样的纯头库必须在某种程度上"重新编译",因为无论好坏,如果源文件或从源文件传递包含的头文件包含catch2.hpp,那么当读取该源文件时,编译器将解析catch2.hpp的源代码。

经过一些实验,我找到了一个合理的解决方案,不需要你在对测试进行更改时完全重新编译 Catch。

以与以前相同的方式定义test_main.cc

#define CATCH_CONFIG_MAIN
#include "catch2.hpp"

添加另一个 .cc 文件,test_root其中包含测试文件作为标头:

#include "test1.hpp"
#include "test2.hpp"

将测试源更改为标头:

测试1.hpp

#pragma once
#include "catch2.hpp"
#include "test_utils.hpp"
TEST_CASE("test1", "[test1]") {
REQUIRE(1 == 1);
}

测试2.hpp

#pragma once
#include "catch2.hpp"
#include "test_utils.hpp"
TEST_CASE("test2", "[test2]") {
REQUIRE(2 == 2);
}

单独编译

clang++ -std=c++17 test_main.cc -c
clang++ -std=c++17 test_root.cc -c
clang++ test_main.o test_root.o

其中 test_main.cc 只需要编译一次。每当更改测试时,都需要重新编译 test_root.cc,当然必须重新链接两个目标文件。

我将暂时不接受这个答案,以防有更好的解决方案。