简体   繁体   中英

How to unit test private features of library (TDD) with CMake

I want to set up my project with CMake such that it can be developed using TDD, which means including and testing internal headers . But setting up the library correctly with CMake hides these implementation details (and correctly so for external use) from my unit tests.

Given a file and folder structure like this:

Foo
|-- include
|   `-- Foo
|       `-- Foo.h
|-- CmakeLists.txt
|-- src
|   |-- Bar
|   |   |-- Bar.h
|   |   `-- Bar.cpp
|   |-- Baz
|   |   |-- Baz.h
|   |   `-- Baz.cpp
|   |-- Foo.cpp
|   `-- CMakeLists.txt
`-- test
    |-- Bar
    |   `-- BarTest.cpp
    |-- Baz
    |   `-- BazTest.cpp
    |-- FooTest.cpp
    `-- CMakeLists.txt

Foo/CMakeLists.txt

project(Foo)
include(CTest)
add_subdirectory(src)
if(BUILD_TESTING)
    add_subdirectory(test)
endif()

Foo/src/CMakeLists.txt

add_library(Foo)
target_include_directories(Foo
    PUBLIC
        $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
        $<INSTALL_INTERFACE:include>
    PRIVATE
        ${CMAKE_CURRENT_LIST_DIR}
)

target_sources(Foo
    PRIVATE
        Foo.cpp
        Bar/Bar.cpp
        Baz/Baz.cpp
)

Foo/test/CMakeLists.txt

find_package(Catch2 REQUIRED)
include(Catch)

add_executable(FooTest
    FooTest.cpp
    Bar/BarTest.cpp # This file can't #include "Bar/Bar.h"
    Baz/BazTest.cpp # This file can't #include "Baz/Baz.h"
)

target_link_libraries(FooTest
    PRIVATE
        Foo
        Catch2::Catch2
)

The problem now is that FooTest links against Foo in a way that other projects do when they need the Foo library as a dependency. This creates the problem that I can't run unit tests in Bar/BarTest.cpp and Baz/BazTest.cpp which includes the files #include "Bar/Bar.h" and #include "Baz/Baz.h . Ideally in my mind a solution would be able to get an internal shortcut target to Foo which includes everything as PUBLIC which then I can run tests on. But when installed the internal headers are private and only files in include/Foo/ is public.

Solutions I've seen is either to create many sub targets and include just what you need for each test. But this seems cumbersome and fits not very good with package managers such as Conan in a very modular setup.

Other solution is to create a duplicate Foo target where everything is public, but this requires me to write everything twice. Sounds like a dirty way to me.

A final solution I thought about is to create an internal target FooInternal with every headers and sources set to public which then FooTest can link against. And then create the wrapping library as Foo which links against FooInternal as private, but sets the include/Foo folder as public headers. But this requires either Foo to be an interface target, which then will not have any libFoo to export, having me to create custom logic to rename or somehow setup CMake to use the libFooInternal as the correct lib file. Again this sounds dirty and I'm not sure how this would work in practice.

Are there any obvious solutions I am being oblivious about, or does anyone have a good solution to this problem?

edit3: Solution from edit2 will actually not work well with build trees that contains multiple libraries, because it gives targets access to linked targets internal headers. Solution offered by Brad King on Kitwares CMake gitlab that was finally used is

add_library(foo foo.c)
target_include_directories(foo PUBLIC "$<BUILD_INTERFACE:$<$<BOOL:$<TARGET_PROPERTY:FOO_PRIVATE>>:/path/to/foo/private/include>>")
set_property(TARGET foo PROPERTY FOO_PRIVATE 1)

add_executable(test_foo test_foo.c)
target_link_libraries(test_foo foo)
set_property(TARGET test_foo PROPERTY FOO_PRIVATE 1)

source: https://gitlab.kitware.com/cmake/cmake/issues/19048


edit2: The final solution which ended up being ridiculously simple was just to set the include paths to

target_include_directories(Foo
    PUBLIC
        $<INSTALL_INTERFACE:include>
        $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
        $<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}>
)

edit1: After looking at the video steveire linked shows that I have done too little programming and testing lately and too much devops oriented work. I need to be able to test internal headers. Back to the original problem on how to test it correctly with CMake with the given setup. Will update this answer when I find a compelling solution.


Original: After going by Superlokkus' comment and discussion on the cpplang slack, it seems to be best never to test internal headers. If the number of abstractions between the tests that use the public headers and the innermost .cpp file is too large, split it into a standalone library.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM