# 4.1 創建一個簡單的單元測試
**NOTE**:*此示例代碼可以在 https://github.com/dev-cafe/cmake-cookbook/tree/v1.0/chapter-04/recipe-01 中找到,包含一個C++的示例。該示例在CMake 3.5版(或更高版本)中是有效的,并且已經在GNU/Linux、macOS和Windows上進行過測試。*
CTest是CMake的測試工具,本示例中,我們將使用CTest進行單元測試。為了保持對CMake/CTest的關注,我們的測試代碼會盡可能的簡單。計劃是編寫和測試能夠對整數求和的代碼,示例代碼只會對整數進行累加,不處理浮點數。就像年輕的卡爾?弗里德里希?高斯(Carl Friedrich Gauss),被他的老師測試從1到100求和所有自然數一樣,我們將要求代碼做同樣的事情。為了說明CMake沒有對實際測試的語言進行任何限制,我們不僅使用C++可執行文件測試代碼,還使用Python腳本和shell腳本作為測試代碼。為了簡單起見,我們將不使用任何測試庫來實現,但是我們將在 后面的示例中介紹C++測試框架。
## 準備工作
代碼示例由三個文件組成。實現源文件`sum_integs.cpp`對整數向量進行求和,并返回累加結果:
```c++
#include "sum_integers.hpp"
#include <vector>
int sum_integers(const std::vector<int> integers) {
auto sum = 0;
for (auto i : integers) {
sum += i;
}
return sum;
}
```
這個示例是否是優雅的實現并不重要,接口以`sum_integers`的形式導出。接口在` sum_integers.hpp `文件中聲明,詳情如下:
```c++
#pragma once
#include <vector>
int sum_integers(const std::vector<int> integers);
```
最后,main函數在`main.cpp`中定義,從`argv[]`中收集命令行參數,將它們轉換成整數向量,調用`sum_integers`函數,并將結果打印到輸出中:
```c++
#include "sum_integers.hpp"
#include <iostream>
#include <string>
#include <vector>
// we assume all arguments are integers and we sum them up
// for simplicity we do not verify the type of arguments
int main(int argc, char *argv[]) {
std::vector<int> integers;
for (auto i = 1; i < argc; i++) {
integers.push_back(std::stoi(argv[i]));
}
auto sum = sum_integers(integers);
std::cout << sum << std::endl;
}
```
測試這段代碼使用C++實現(`test.cpp`),Bash shell腳本實現(`test.sh`)和Python腳本實現(`test.py`),只要實現可以返回一個零或非零值,從而CMake可以解釋為成功或失敗。
C++例子(`test.cpp`)中,我們通過調用`sum_integers`來驗證1 + 2 + 3 + 4 + 5 = 15:
```c++
#include "sum_integers.hpp"
#include <vector>
int main() {
auto integers = {1, 2, 3, 4, 5};
if (sum_integers(integers) == 15) {
return 0;
} else {
return 1;
}
}
```
Bash shell腳本調用可執行文件:
```shell
#!/usr/bin/env bash
EXECUTABLE=$1
OUTPUT=$($EXECUTABLE 1 2 3 4)
if [ "$OUTPUT" = "10" ]
then
exit 0
else
exit 1
fi
```
此外,Python腳本調用可執行文件(使用`--executable`命令行參數傳遞),并使用`--short`命令行參數執行:
```python
import subprocess
import argparse
# test script expects the executable as argument
parser = argparse.ArgumentParser()
parser.add_argument('--executable',
help='full path to executable')
parser.add_argument('--short',
default=False,
action='store_true',
help='run a shorter test')
args = parser.parse_args()
def execute_cpp_code(integers):
result = subprocess.check_output([args.executable] + integers)
return int(result)
if args.short:
# we collect [1, 2, ..., 100] as a list of strings
result = execute_cpp_code([str(i) for i in range(1, 101)])
assert result == 5050, 'summing up to 100 failed'
else:
# we collect [1, 2, ..., 1000] as a list of strings
result = execute_cpp_code([str(i) for i in range(1, 1001)])
assert result == 500500, 'summing up to 1000 failed'
```
## 具體實施
現在,我們將逐步描述如何為項目設置測試:
1. 對于這個例子,我們需要C++11支持,可用的Python解釋器,以及Bash shell:
```cmake
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)
project(recipe-01 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
find_package(PythonInterp REQUIRED)
find_program(BASH_EXECUTABLE NAMES bash REQUIRED)
```
2. 然后,定義庫及主要可執行文件的依賴關系,以及測試可執行文件:
```cmake
# example library
add_library(sum_integers sum_integers.cpp)
# main code
add_executable(sum_up main.cpp)
target_link_libraries(sum_up sum_integers)
# testing binary
add_executable(cpp_test test.cpp)
target_link_libraries(cpp_test sum_integers)
```
3. 最后,打開測試功能并定義四個測試。最后兩個測試, 調用相同的Python腳本,先沒有任何命令行參數,再使用`--short`:
```cmake
enable_testing()
add_test(
NAME bash_test
COMMAND ${BASH_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test.sh $<TARGET_FILE:sum_up>
)
add_test(
NAME cpp_test
COMMAND $<TARGET_FILE:cpp_test>
)
add_test(
NAME python_test_long
COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test.py --executable $<TARGET_FILE:sum_up>
)
add_test(
NAME python_test_short
COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test.py --short --executable $<TARGET_FILE:sum_up>
)
```
4. 現在,我們已經準備好配置和構建代碼。先手動進行測試:
```shell
$ mkdir -p build
$ cd build
$ cmake ..
$ cmake --build .
$ ./sum_up 1 2 3 4 5
15
```
5. 然后,我們可以用`ctest`運行測試集:
```shell
$ ctest
Test project /home/user/cmake-recipes/chapter-04/recipe-01/cxx-example/build
Start 1: bash_test
1/4 Test #1: bash_test ........................ Passed 0.01 sec
Start 2: cpp_test
2/4 Test #2: cpp_test ......................... Passed 0.00 sec
Start 3: python_test_long
3/4 Test #3: python_test_long ................. Passed 0.06 sec
Start 4: python_test_short
4/4 Test #4: python_test_short ................ Passed 0.05 sec
100% tests passed, 0 tests failed out of 4
Total Test time (real) = 0.12 sec
```
6. 還應該嘗試中斷實現,以驗證測試集是否能捕捉到更改。
## 工作原理
這里的兩個關鍵命令:
* `enable_testing()`,測試這個目錄和所有子文件夾(因為我們把它放在主`CMakeLists.txt`)。
* `add_test()`,定義了一個新的測試,并設置測試名稱和運行命令。
```cmake
add_test(
NAME cpp_test
COMMAND $<TARGET_FILE:cpp_test>
)
```
上面的例子中,使用了生成器表達式:`$<TARGET_FILE:cpp_test>`。生成器表達式,是在生成**構建系統生成時**的表達式。我們將在第5章第9節中詳細地描述生成器表達式。此時,我們可以聲明` $<TARGET_FILE:cpp_test>`變量,將使用`cpp_test`可執行目標的完整路徑進行替換。
生成器表達式在測試時非常方便,因為不必顯式地將可執行程序的位置和名稱,可以硬編碼到測試中。以一種可移植的方式實現這一點非常麻煩,因為可執行文件和可執行后綴(例如,Windows上是`.exe`后綴)的位置在不同的操作系統、構建類型和生成器之間可能有所不同。使用生成器表達式,我們不必顯式地了解位置和名稱。
也可以將參數傳遞給要運行的`test`命令,例如:
```cmake
add_test(
NAME python_test_short
COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test.py --short --executable $<TARGET_FILE:sum_up>
)
```
這個例子中,我們按順序運行測試,并展示如何縮短總測試時間并行執行測試(第8節),執行測試用例的子集(第9節)。這里,可以自定義測試命令,可以以任何編程語言運行測試集。CTest關心的是,通過命令的返回碼測試用例是否通過。CTest遵循的標準約定是,返回零意味著成功,非零返回意味著失敗。可以返回零或非零的腳本,都可以做測試用例。
既然知道了如何定義和執行測試,那么了解如何診斷測試失敗也很重要。為此,我們可以在代碼中引入一個bug,讓所有測試都失敗:
```shell
Start 1: bash_test
1/4 Test #1: bash_test ........................***Failed 0.01 sec
Start 2: cpp_test
2/4 Test #2: cpp_test .........................***Failed 0.00 sec
Start 3: python_test_long
3/4 Test #3: python_test_long .................***Failed 0.06 sec
Start 4: python_test_short
4/4 Test #4: python_test_short ................***Failed 0.06 sec
0% tests passed, 4 tests failed out of 4
Total Test time (real) = 0.13 sec
The following tests FAILED:
1 - bash_test (Failed)
2 - cpp_test (Failed)
3 - python_test_long (Failed)
4 - python_test_short (Failed)
Errors while running CTest
```
如果我們想了解更多,可以查看文件`test/Temporary/lasttestsfailure.log`。這個文件包含測試命令的完整輸出,并且在分析階段,要查看的第一個地方。使用以下CLI開關,可以從CTest獲得更詳細的測試輸出:
* `--output-on-failure`:將測試程序生成的任何內容打印到屏幕上,以免測試失敗。
* `-v`:將啟用測試的詳細輸出。
* `-vv`:啟用更詳細的輸出。
CTest提供了一個非常方快捷的方式,可以重新運行以前失敗的測試;要使用的CLI開關是`--rerun-failed`,在調試期間非常有用。
## 更多信息
考慮以下定義:
```cmake
add_test(
NAME python_test_long
COMMAND ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test.py --executable $<TARGET_FILE:sum_up>
)
```
前面的定義可以通過顯式指定腳本運行的`WORKING_DIRECTORY`重新表達,如下:
```cmake
add_test(
NAME python_test_long
COMMAND ${PYTHON_EXECUTABLE} test.py --executable $<TARGET_FILE:sum_up>
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
```
測試名稱可以包含`/`字符,按名稱組織相關測試也很有用,例如:
```cmake
add_test(
NAME python/long
COMMAND ${PYTHON_EXECUTABLE} test.py --executable $<TARGET_FILE:sum_up>
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
```
有時候,我們需要為測試腳本設置環境變量。這可以通過`set_tests_properties`實現:
```cmake
set_tests_properties(python_test
PROPERTIES
ENVIRONMENT
ACCOUNT_MODULE_PATH=${CMAKE_CURRENT_SOURCE_DIR}
ACCOUNT_HEADER_FILE=${CMAKE_CURRENT_SOURCE_DIR}/account/account.h
ACCOUNT_LIBRARY_FILE=$<TARGET_FILE:account>
)
```
這種方法在不同的平臺上并不總可行,CMake提供了解決這個問題的方法。下面的代碼片段與上面給出的代碼片段相同,在執行實際的Python測試腳本之前,通過`CMAKE_COMMAND`調用CMake來預先設置環境變量:
```cmake
add_test(
NAME
python_test
COMMAND
${CMAKE_COMMAND} -E env
ACCOUNT_MODULE_PATH=${CMAKE_CURRENT_SOURCE_DIR}
ACCOUNT_HEADER_FILE=${CMAKE_CURRENT_SOURCE_DIR}/account/account.h
ACCOUNT_LIBRARY_FILE=$<TARGET_FILE:account>
${PYTHON_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/account/test.py
)
```
同樣,要注意使用生成器表達式` $<TARGET_FILE:account> `來傳遞庫文件的位置。
我們已經使用`ctest`命令執行測試,CMake還將為生成器創建目標(Unix Makefile生成器為`make test`,Ninja工具為`ninja test`,或者Visual Studio為`RUN_TESTS`)。這意味著,還有另一種(幾乎)可移植的方法來運行測試:
```shell
$ cmake --build . --target test
```
不幸的是,當使用Visual Studio生成器時,我們需要使用`RUN_TESTS`來代替:
```shell
$ cmake --build . --target RUN_TESTS
```
**NOTE**:*`ctest`提供了豐富的命令行參數。其中一些內容將在以后的示例中探討。要獲得完整的列表,需要使用`ctest --help`來查看。命令`cmake --help-manual ctest`會將向屏幕輸出完整的ctest手冊。*
- Introduction
- 前言
- 第0章 配置環境
- 0.1 獲取代碼
- 0.2 Docker鏡像
- 0.3 安裝必要的軟件
- 0.4 測試環境
- 0.5 上報問題并提出改進建議
- 第1章 從可執行文件到庫
- 1.1 將單個源文件編譯為可執行文件
- 1.2 切換生成器
- 1.3 構建和鏈接靜態庫和動態庫
- 1.4 用條件句控制編譯
- 1.5 向用戶顯示選項
- 1.6 指定編譯器
- 1.7 切換構建類型
- 1.8 設置編譯器選項
- 1.9 為語言設定標準
- 1.10 使用控制流
- 第2章 檢測環境
- 2.1 檢測操作系統
- 2.2 處理與平臺相關的源代碼
- 2.3 處理與編譯器相關的源代碼
- 2.4 檢測處理器體系結構
- 2.5 檢測處理器指令集
- 2.6 為Eigen庫使能向量化
- 第3章 檢測外部庫和程序
- 3.1 檢測Python解釋器
- 3.2 檢測Python庫
- 3.3 檢測Python模塊和包
- 3.4 檢測BLAS和LAPACK數學庫
- 3.5 檢測OpenMP的并行環境
- 3.6 檢測MPI的并行環境
- 3.7 檢測Eigen庫
- 3.8 檢測Boost庫
- 3.9 檢測外部庫:Ⅰ. 使用pkg-config
- 3.10 檢測外部庫:Ⅱ. 自定義find模塊
- 第4章 創建和運行測試
- 4.1 創建一個簡單的單元測試
- 4.2 使用Catch2庫進行單元測試
- 4.3 使用Google Test庫進行單元測試
- 4.4 使用Boost Test進行單元測試
- 4.5 使用動態分析來檢測內存缺陷
- 4.6 預期測試失敗
- 4.7 使用超時測試運行時間過長的測試
- 4.8 并行測試
- 4.9 運行測試子集
- 4.10 使用測試固件
- 第5章 配置時和構建時的操作
- 5.1 使用平臺無關的文件操作
- 5.2 配置時運行自定義命令
- 5.3 構建時運行自定義命令:Ⅰ. 使用add_custom_command
- 5.4 構建時運行自定義命令:Ⅱ. 使用add_custom_target
- 5.5 構建時為特定目標運行自定義命令
- 5.6 探究編譯和鏈接命令
- 5.7 探究編譯器標志命令
- 5.8 探究可執行命令
- 5.9 使用生成器表達式微調配置和編譯
- 第6章 生成源碼
- 6.1 配置時生成源碼
- 6.2 使用Python在配置時生成源碼
- 6.3 構建時使用Python生成源碼
- 6.4 記錄項目版本信息以便報告
- 6.5 從文件中記錄項目版本
- 6.6 配置時記錄Git Hash值
- 6.7 構建時記錄Git Hash值
- 第7章 構建項目
- 7.1 使用函數和宏重用代碼
- 7.2 將CMake源代碼分成模塊
- 7.3 編寫函數來測試和設置編譯器標志
- 7.4 用指定參數定義函數或宏
- 7.5 重新定義函數和宏
- 7.6 使用廢棄函數、宏和變量
- 7.7 add_subdirectory的限定范圍
- 7.8 使用target_sources避免全局變量
- 7.9 組織Fortran項目
- 第8章 超級構建模式
- 8.1 使用超級構建模式
- 8.2 使用超級構建管理依賴項:Ⅰ.Boost庫
- 8.3 使用超級構建管理依賴項:Ⅱ.FFTW庫
- 8.4 使用超級構建管理依賴項:Ⅲ.Google Test框架
- 8.5 使用超級構建支持項目
- 第9章 語言混合項目
- 9.1 使用C/C++庫構建Fortran項目
- 9.2 使用Fortran庫構建C/C++項目
- 9.3 使用Cython構建C++和Python項目
- 9.4 使用Boost.Python構建C++和Python項目
- 9.5 使用pybind11構建C++和Python項目
- 9.6 使用Python CFFI混合C,C++,Fortran和Python
- 第10章 編寫安裝程序
- 10.1 安裝項目
- 10.2 生成輸出頭文件
- 10.3 輸出目標
- 10.4 安裝超級構建
- 第11章 打包項目
- 11.1 生成源代碼和二進制包
- 11.2 通過PyPI發布使用CMake/pybind11構建的C++/Python項目
- 11.3 通過PyPI發布使用CMake/CFFI構建C/Fortran/Python項目
- 11.4 以Conda包的形式發布一個簡單的項目
- 11.5 將Conda包作為依賴項發布給項目
- 第12章 構建文檔
- 12.1 使用Doxygen構建文檔
- 12.2 使用Sphinx構建文檔
- 12.3 結合Doxygen和Sphinx
- 第13章 選擇生成器和交叉編譯
- 13.1 使用CMake構建Visual Studio 2017項目
- 13.2 交叉編譯hello world示例
- 13.3 使用OpenMP并行化交叉編譯Windows二進制文件
- 第14章 測試面板
- 14.1 將測試部署到CDash
- 14.2 CDash顯示測試覆蓋率
- 14.3 使用AddressSanifier向CDash報告內存缺陷
- 14.4 使用ThreadSaniiser向CDash報告數據爭用
- 第15章 使用CMake構建已有項目
- 15.1 如何開始遷移項目
- 15.2 生成文件并編寫平臺檢查
- 15.3 檢測所需的鏈接和依賴關系
- 15.4 復制編譯標志
- 15.5 移植測試
- 15.6 移植安裝目標
- 15.7 進一步遷移的措施
- 15.8 項目轉換為CMake的常見問題
- 第16章 可能感興趣的書
- 16.1 留下評論——讓其他讀者知道你的想法