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

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

我有以下项目结构:

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 文档的 Scaling Up 部分,他们提到为了拆分您的测试,您可能需要

Use as many additional cpp files (or whatever you call your implementation files) as you need for your tests, partitioned however makes most sense for your way of working. Each additional file need only #include "catch.hpp"

但在 examples section of the documentation I don't see a use case like mine. I read this blog post 中描述了三个对我没有吸引力的解决方案:将函数定义为宏,或创建函数 staticinline.

是否有另一种编译这些文件的方法,它会生成一个可执行文件,其主要功能由 test_main.cc 定义?

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

定义test_main.cc和之前一样:

#define CATCH_CONFIG_MAIN

#include "catch2.hpp"

添加另一个 .cc 文件,test_root,其中包含您的测试文件 headers:

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

将您的测试源更改为 headers:

test1.hpp

#pragma once
#include "catch2.hpp"
#include "test_utils.hpp"

TEST_CASE("test1", "[test1]") {
  REQUIRE(1 == 1);
}

test2.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 每当您更改测试时都需要重新编译,当然您必须重新链接这两个 object 文件。

如果有更好的解决方案,我暂时不接受这个答案。

这其实和Catch和测试无关。当你用 C++ #include 一个文件时,它在 #include 行逐字地得到 copy-pasted。如果您将自由函数定义放在 header 中,您将在构建实际程序等时看到此问题。

潜在的问题是 #include 与大多数情况下的等效指令(importrequire 等)不是同一种 import-a-module 指令语言,在这种情况下做理智的事情(确认 header 与我们已经看到的相同,并忽略重复的方法定义)。

建议您编写 inline 的评论者在技术上是正确的,因为这将 "solve your problem" 因为您的编译器不会多次为该方法生成 object 代码。但是,它并没有真正解释正在发生的事情或解决根本问题。


干净的解决方案是:

  • 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

我还建议您阅读此内容:What is the difference between a definition and a declaration?

明确地:

// 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 << "Hi\n"; }

看来您每次更改测试时都担心"recompiling Catch"。我不想告诉你,但你现在在 C++ 领域:你将毫无意义地重新编译很多东西。 Header-only 像 Catch 这样的库必须在某种程度上 "recompiled" 当包含它们的源文件发生变化时,因为无论好坏,如果源文件或 header 文件从源文件传递包含包含 catch2.hpp,那么 catch2.hpp 的源代码将在读取该源文件时由编译器解析。