随着CMake工具越来越强大便捷,越来越多的C/C++项目转而使用CMake来进行编译管理,它还提供了用于测试的ctest命令来执行项目中编写的单元测试。

本文就以一个实例来介绍如何使用ctest来进行单元测试。

一、环境准备

本文实例环境VSCode+MinGW64+CMake+gtest。

需要在MinGW中安装gtest,如果没有安装也没有关系,在CMakeLists.txt中进行检测,如果发现没有安装,则自动下载源码进行编译。

二、新建项目

新建一个目录,比如demo,然后使用VSCode打开目录,创建一个CMake项目。 创建CMake项目可以使用VSCode的CMake向导来创建,也可以直接在目录中编写一个CMakeLists.txt来创建。

使用VSCode的CMake向导创建项目

在VSCode中按F1,在弹出的选项中选择CMake:快速入门

然后选择编译套件,如果需要搜索,可以选择[Scan for kits]

然后输入项目名称,比如demo

根据项目需要选择库(Library)或者可执行体(Executable),这里选择Executable

然后向导会自动新建CMakeLists.txt和main.cpp:

CMakeLists.txt:

 1cmake_minimum_required(VERSION 3.0.0)
 2project(demo VERSION 0.1.0)
 3
 4include(CTest)
 5enable_testing()
 6
 7add_executable(demo main.cpp)
 8
 9set(CPACK_PROJECT_NAME ${PROJECT_NAME})
10set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})
11include(CPack)

Main.cpp:

1#include <iostream>
2
3int main(int, char**) {
4    std::cout << "Hello, world!\n";
5}

自行创建CMake项目

其实就是自己编写上面的CMakeLists.txt和项目源码

可以从CMakeLists.txt中看到已经添加并启用了CTest

三、创建单元测试

1、添加需要测试的功能,比如建立一个func.h和func.cpp

func.h

1#ifndef __FUNC_H_INCLUDE_
2#define __FUNC_H_INCLUDE_
3
4int Factorial(int n);
5bool IsPrime(int n);
6
7#endif

func.cpp

 1#include "func.h"
 2
 3// Returns n! (the factorial of n).  For negative n, n! is defined to be 1.
 4int Factorial(int n) {
 5  int result = 1;
 6  for (int i = 1; i <= n; i++) {
 7    result *= i;
 8  }
 9
10  return result;
11}
12
13// Returns true if and only if n is a prime number.
14bool IsPrime(int n) {
15  // Trivial case 1: small numbers
16  if (n <= 1) return false;
17
18  // Trivial case 2: even numbers
19  if (n % 2 == 0) return n == 2;
20
21  // Now, we have that n is odd and n >= 3.
22
23  // Try to divide n by every odd number i, starting from 3
24  for (int i = 3;; i += 2) {
25    // We only have to try i up to the square root of n
26    if (i > n / i) break;
27
28    // Now, we have i <= n/i < n.
29    // If n is divisible by i, n is not prime.
30    if (n % i == 0) return false;
31  }
32
33  // n has no integer factor in the range (1, n), and thus is prime.
34  return true;
35}

2、创建测试目录test,并在目录中添加CMakeLists.txt、tmain.cpp和单元测试代码test.cpp。

test/CMakeLists.txt

1add_executable(t tmain.cpp test.cpp ../func.cpp)
2target_link_libraries(t PRIVATE gtest)
3
4add_test(NAME t COMMAND t)

tmain.cpp

1#include <gtest/gtest.h>
2
3int main()
4{
5    testing::InitGoogleTest();
6    return RUN_ALL_TESTS();
7}

test.cpp

 1#include <gtest/gtest.h>
 2#include "../func.h"
 3
 4
 5// Tests Factorial().
 6
 7// Tests factorial of negative numbers.
 8TEST(FactorialTest, Negative) {
 9  // This test is named "Negative", and belongs to the "FactorialTest"
10  // test case.
11  EXPECT_EQ(1, Factorial(-5));
12  EXPECT_EQ(1, Factorial(-1));
13  EXPECT_GT(Factorial(-10), 0);
14
15  // <TechnicalDetails>
16  //
17  // EXPECT_EQ(expected, actual) is the same as
18  //
19  //   EXPECT_TRUE((expected) == (actual))
20  //
21  // except that it will print both the expected value and the actual
22  // value when the assertion fails.  This is very helpful for
23  // debugging.  Therefore in this case EXPECT_EQ is preferred.
24  //
25  // On the other hand, EXPECT_TRUE accepts any Boolean expression,
26  // and is thus more general.
27  //
28  // </TechnicalDetails>
29}
30
31// Tests factorial of 0.
32TEST(FactorialTest, Zero) { EXPECT_EQ(1, Factorial(0)); }
33
34// Tests factorial of positive numbers.
35TEST(FactorialTest, Positive) {
36  EXPECT_EQ(1, Factorial(1));
37  EXPECT_EQ(2, Factorial(2));
38  EXPECT_EQ(6, Factorial(3));
39  EXPECT_EQ(40320, Factorial(8));
40}
41
42// Tests IsPrime()
43
44// Tests negative input.
45TEST(IsPrimeTest, Negative) {
46  // This test belongs to the IsPrimeTest test case.
47
48  EXPECT_FALSE(IsPrime(-1));
49  EXPECT_FALSE(IsPrime(-2));
50  EXPECT_FALSE(IsPrime(INT_MIN));
51}
52
53// Tests some trivial cases.
54TEST(IsPrimeTest, Trivial) {
55  EXPECT_FALSE(IsPrime(0));
56  EXPECT_FALSE(IsPrime(1));
57  EXPECT_TRUE(IsPrime(2));
58  EXPECT_TRUE(IsPrime(3));
59}
60
61// Tests positive input.
62TEST(IsPrimeTest, Positive) {
63  EXPECT_FALSE(IsPrime(4));
64  EXPECT_TRUE(IsPrime(5));
65  EXPECT_FALSE(IsPrime(6));
66  EXPECT_TRUE(IsPrime(23));
67}

项目的目录结构如下:

3、修改根目录CMakeLists.txt

需要在CMakeLists.txt中引用test目录,添加如下命令:

add_subdirectory(test)

4、运行测试

如果MinGW中安装有gtest则可以在VSCode中执行Run CTest了:

输出如下:

当然,也可以在VSCode中选择测试目标t直接运行测试:

有没发现使用ctest并不能像直接运行测试目标t那样显示出详细的测试项目,那是因为在CMakeLists.txt中是使用的通用方法add_test(NAME t COMMAND t)添加的测试,其实CMake是直接支持gtest的,只需要把add_test(NAME t COMMAND t)换成下面两句即可:

1include(GoogleTest)
2gtest_add_tests(TARGET t)

可以看到各测试项目的情况了:

而且运行Run CTest后,VSCode中也会提示有几个测试用例和通过情况:

使用gtest_add_tests有一个问题:一旦测试用例改变,它就需要重新执行cmake,否则无法知道改变后的测试用例。 所以CMake添加了gtest_discover_tests指令,它通过调用编译后的执行程序并添加参数--gtest_list_tests来获取测试用例的,所以不需要重新执行CMake。

既然如此,那前面的tmain.cpp需要作修改以接收参数:

tmain.cpp

1#include <gtest/gtest.h>
2
3int main(int argc, char *argv[])
4{
5	testing::InitGoogleTest(&argc, argv);
6	return RUN_ALL_TESTS();
7}

此时把gtest_add_tests(TARGET t)替换成gtest_discover_tests(t)即可,修改后的CMakeLists.txt完整源码:

1add_executable(t tmain.cpp test.cpp ../func.cpp)
2target_link_libraries(t PRIVATE gtest)
3
4include(GoogleTest)
5gtest_discover_tests(t)

其实,gtest本身是带有一个main函数库的,只需要包链接gtest_main即可,不需要自己写tmain.cpp中的内容。可以把tmain.cpp删除,使用下面的CMakeLists.txt即可:

1add_executable(t test.cpp ../func.cpp)
2target_link_libraries(t PRIVATE gtest gtest_main)
3
4include(GoogleTest)
5gtest_discover_tests(t)

四、让CMake自动下载、编译依赖

前面有提到,要运行示例,必须要求安装了gtest,可以写入CMakeLists.txt中,使用CMake的find_package命令来查找,本例是需要GTest,添加find_package(GTest REQUIRED),并且是必须要安装有,所有后面添加了REQUIRED参数,注意必须是大写。

如果找不到GTest则会报错:

这种方式要求在MinGW中安装有GTest,可以使用MinGW命令:pacman -S mingw-w64-x86_64-gtest来安装。

当然更友好的方式是如果系统没有安装则自己下载源码进行编译引用,在根目录的CMakeLists.txt中添加:

1cmake_policy(SET CMP0135 NEW)
2find_package(GTest)
3if(NOT GTest_FOUND)
4message("GTest not found, download it...")
5include(FetchContent)
6FetchContent_Declare(googletest URL https://github.com/google/googletest/archive/refs/heads/main.zip)
7FetchContent_MakeAvailable(googletest)
8endif()

这里find_package没有添加REQUIRED参数来强制要求,只是检测,后面判断检测结果GTest_FOUND,如果没有找到则从指定URL下载(FetchContent_Declare)并编译(FetchContent_MakeAvailable),由于使用URL下载需要添加cmake_policy(SET CMP0135 NEW),不然会报警告:

1[cmake]   The DOWNLOAD_EXTRACT_TIMESTAMP option was not given and policy CMP0135 is
2[cmake]   not set.  The policy's OLD behavior will be used.  When using a URL
3[cmake]   download, the timestamps of extracted files should preferably be that of
4[cmake]   the time of extraction, otherwise code that depends on the extracted
5[cmake]   contents might not be rebuilt if the URL changes.  The OLD behavior
6[cmake]   preserves the timestamps from the archive instead, but this is usually not
7[cmake]   what you want.  Update your project to the NEW behavior or specify the
8[cmake]   DOWNLOAD_EXTRACT_TIMESTAMP option with a value of true to avoid this
9[cmake]   robustness issue.

FetchContent_Declare也可以使用GIT_REPOSITORY从Git克隆下来,但是这种方式如果网络不好则比较慢。

注意为了使用这些高级指令,最好是安装最新的CMake版本,FetchContent最低要求3.11:

1cmake_minimum_required(VERSION 3.11.0)

写得非常详细,如果觉得对你有帮助,欢迎点赞收藏!