Welcome to the next pikoTutorial !
Building and running a single test file
To present the easiest case, we need to assume some folder structure of our project:
project |- build |- src | |- CMakeLists.txt | |- lib.cpp | |- lib.hpp |- test | |- CMakeLists.txt | |- test_lib.cpp |- thirdparty | |- googletest |- CMakeLists.txt
Here the easiest case means that we have only on library and only one test file for it. Firstly of all, we need to put enable_testing()
to our top level CMakeLists.txt file:
cmake_minimum_required(VERSION 3.15) project(UnitTestsCMake) # this line initializes testing environment and generate test target enable_testing() add_subdirectory(src) add_subdirectory(test) add_subdirectory(thirdparty/googletest)
CMakeLists.txt in src folder is just a one liner defining the library which we can link against in the tests:
add_library(MyLib lib.cpp)
Now the most important one - CMakeLists.txt file in test folder:
# create test target name set(TARGET TestLib) # create test executable add_executable(${TARGET} test_lib.cpp) # link our testd lib MyLib to the test executable declared above target_link_libraries(${TARGET} PRIVATE gtest_main MyLib) # define a test with its name and command to run it - in our simple case, # test name is the same as command to run the test (the executable), but in # command you can pass more information, e.g. the command line arguments add_test(NAME ${TARGET} COMMAND ${TARGET})
After that the only thing left to do is to go to terminal and configure, build and run the unit tests:
cd build cmake .. cmake --build . ctest
If everything went ok, you should see the following output:
Test project /home/pikotutorial/unit_tests_cmake/build Start 1: TestLib 1/1 Test #1: TestLib .......................... Passed 0.00 sec 100% tests passed, 0 tests failed out of 1 Total Test time (real) = 0.00 sec
You can always make this output more verbose by calling ctest
with -V
option:
UpdateCTestConfiguration from :/home/pikotutorial/unit_tests_cmake/build/DartConfiguration.tcl UpdateCTestConfiguration from :/home/pikotutorial/unit_tests_cmake/build/DartConfiguration.tcl Test project /home/pikotutorial/unit_tests_cmake/build Constructing a list of tests Done constructing a list of tests Updating test list for fixtures Added 0 tests to meet fixture requirements Checking test dependency graph... Checking test dependency graph end test 1 Start 1: TestLib 1: Test command: /home/pikotutorial/unit_tests_cmake/build/test/TestLib 1: Working Directory: /home/pikotutorial/unit_tests_cmake/build/test 1: Test timeout computed to be: 10000000 1: Running main() from /home/pikotutorial/unit_tests_cmake/thirdparty/googletest/googletest/src/gtest_main.cc 1: [==========] Running 0 tests from 0 test suites. 1: [==========] 0 tests from 0 test suites ran. (0 ms total) 1: [ PASSED ] 0 tests. 1/1 Test #1: TestLib .......................... Passed 0.01 sec 100% tests passed, 0 tests failed out of 1 Total Test time (real) = 0.01 sec
Creating a test suite
After learning how to define a single executable with unit tests, we can move to slightly more complex example in which there are multiple test files. Look at this project structure:
project |- build |- src |- CMakeLists.txt |- lib.cpp |- lib.hpp |- test |- CMakeLists.txt |- test_lib_1.cpp |- test_lib_2.cpp |- test_lib_3.cpp |- thirdparty |- googletest |-CMakeLists.txt
You could of course repeat 3 times what we did above, but because every test requires 4 lines, as the number of tests increases, your CMakeLists.txt file would quickly start to be very long hard to maintain. This is the reason why it's much better to do this using loop:
# define a list of test names set(LIB_TESTS test_lib_1 test_lib_2 test_lib_3 ) # iterate over every test foreach(TEST_NAME ${LIB_TESTS}) add_executable(${TEST_NAME} ${TEST_NAME}.cpp) target_link_libraries(${TEST_NAME} PRIVATE gtest_main MyLib) add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}) endforeach()
This way, whenever you add a new test file, you just need to append its name to the LIB_TESTS
list and that's it. After that you can run all your tests with a single command ctest
what should give you the following result.
Test project /home/pawbar/Documents/projekty/piko_tutorial/content/sop_24_07/cpp/unit_tests_cmake/build Start 1: test_lib_1 1/3 Test #1: test_lib_1 ....................... Passed 0.01 sec Start 2: test_lib_2 2/3 Test #2: test_lib_2 ....................... Passed 0.01 sec Start 3: test_lib_3 3/3 Test #3: test_lib_3 ....................... Passed 0.01 sec 100% tests passed, 0 tests failed out of 3 Total Test time (real) = 0.03 sec
Creating multiple test suites
Now let's jump into the structure which is the most commonly met in various software projects. Previous examples provided overview on how to use CMake for unit testing, but in real projects you would very rarely meet structures with only one file with tests or only one test suite which. Usually, there are multiple test files organized in multiple test suites. Moreover, during a typical work you want to run unit tests only from the test suite that you're currently working on, not all unit tests every time. Let's then mimic such structure trying at the same time to keep it simple:
project |- build |- src |- CMakeLists.txt |- lib_1.cpp |- lib_1.hpp |- lib_2.cpp |- lib_2.hpp |- test |- CMakeLists.txt |- test_lib_1_1.cpp |- test_lib_1_2.cpp |- test_lib_2_1.cpp |- test_lib_2_2.cpp |- thirdparty |- googletest |-CMakeLists.txt
Again, you could just duplicate the code for creating a single test suite from the previous example, but there are 2 problems with that:
- how to differentiate between the test suites, so that tests from only one can be ran?
- how to avoid code duplication related to repeated loops?
First problem can be solved with labels - each test may have a label assigned and all tests with the same labels create a single test suite:
set(LIB_1_TESTS test_lib_1_1 test_lib_1_2 ) foreach(TEST_NAME ${LIB_1_TESTS}) add_executable(${TEST_NAME} ${TEST_NAME}.cpp) target_link_libraries(${TEST_NAME} PRIVATE gtest_main MyLib1) add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}) set_tests_properties(${TEST_NAME} PROPERTIES LABELS "test_lib_1") endforeach() set(LIB_2_TESTS test_lib_2_1 test_lib_2_2 ) foreach(TEST_NAME ${LIB_2_TESTS}) add_executable(${TEST_NAME} ${TEST_NAME}.cpp) target_link_libraries(${TEST_NAME} PRIVATE gtest_main MyLib2) add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}) set_tests_properties(${TEST_NAME} PROPERTIES LABELS "test_lib_2") endforeach()
After setting labels, to run e.g. only test suite test_lib_1, call:
ctest -L test_lib_1
What will run tests from only 2 files, instead of all of them:
Test project /home/pikotutorial/unit_tests_cmake/build Start 1: test_lib_1_1 1/2 Test #1: test_lib_1_1 ..................... Passed 0.00 sec Start 2: test_lib_1_2 2/2 Test #2: test_lib_1_2 ..................... Passed 0.00 sec 100% tests passed, 0 tests failed out of 2 Label Time Summary: test_lib_1 = 0.00 sec*proc (2 tests) Total Test time (real) = 0.00 sec
To avoid the code duplication, we will create a CMake function:
# define a function creating a single test suite function(create_test_suite TEST_SUITE_NAME TESTS DEPENDENCIES) foreach(TEST_NAME IN LISTS TESTS) add_executable(${TEST_NAME} ${TEST_NAME}.cpp) target_link_libraries(${TEST_NAME} PRIVATE gtest_main ${DEPENDENCIES}) add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}) set_tests_properties(${TEST_NAME} PROPERTIES LABELS ${TEST_SUITE_NAME}) endforeach() endfunction() # define a list of tests in the first test suite set(LIB_1_TESTS test_lib_1_1 test_lib_1_2 ) # define a list of tests in the second test suite set(LIB_2_TESTS test_lib_2_1 test_lib_2_2 ) # use create_test_suite function to define 2 test suites create_test_suite("test_lib_1" "${LIB_1_TESTS}" MyLib1) create_test_suite("test_lib_2" "${LIB_2_TESTS}" MyLib2)
Note for beginners: I didn't want to overcomplicate the folder structure here, but in reality usually you want to organize your test suite into separate test_* folders corresponding to the folders where the source code is located. In such case, every folder should have a dedicated CMakeLists.txt files which defines a test suite for the given folder.
Note for advanced: in big projects built under a single top-level CMake with sub-components owned by a different teams, there's always a discussion about the strategy on avoiding labels duplication. One of them is to simply use paths relative to the project root. For example, a test/test_lib_1/CMakeLists.txt file would define a test suite labeled as test/test_lib_1. Thanks to such approach, when someone wants to run all the unit tests from this folder, the command is
ctest -L test/test_lib_1
. This is not only clear, but also allows to utilize the Tab autocompletion in the terminal when choosing the test suite because the label corresponds to the folder structure.
Building and running at once
So far so good, but there's still one thing missing for the full convenience. Note that the only thing that ctest
does is running the test binaries that have already been built. Whenever you introduce a change to your code and want to check if tests are still passing, you need to first compile your tests and then run the tests. It would be nice of course to be able to do that in a single command. To achieve that we can add a custom target to the create_test_suite
function:
function(create_test_suite TEST_SUITE_NAME TESTS DEPENDENCIES) foreach(TEST_NAME IN LISTS TESTS) add_executable(${TEST_NAME} ${TEST_NAME}.cpp) target_link_libraries(${TEST_NAME} PRIVATE gtest_main ${DEPENDENCIES}) add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}) set_tests_properties(${TEST_NAME} PROPERTIES LABELS ${TEST_SUITE_NAME}) endforeach() # add a target which runs tests for the given label and depends on the given tests add_custom_target(${TEST_SUITE_NAME} COMMAND ${CMAKE_CTEST_COMMAND} -L ${TEST_SUITE_NAME} DEPENDS ${TESTS} ) endfunction()
Since now, after every code change, if you want to recompile and run test_lib_1 test suite, you can simply use command:
cmake --build . --target test_lib_1
Top comments (0)