Reputation: 17266
I am trying to split an existing CMake project into multiple libraries such that code which is only required for unit tests is segregated and not linked against production code.
I have written a relatively short MWE to illustate the intent.
First, the directory structure for the project:
build/ # empty
core_library/
CMakeLists.txt
core.cpp
core.hpp
some_library/
CMakeLists.txt
some_library.cpp
some_library.hpp
test_only_library/
CMakeLists.txt
test_only_library.cpp
test_only_library.hpp
build.sh
CMakeLists.txt
main.cpp
tests.cpp
Sorry it's a bit long, but let's go through each of these.
core_library
Important to know:
target_function
, which is intended to imitate a core C++ library function.test_only_library
.add_library(
core_library
STATIC
core.cpp
)
target_include_directories(
core_library
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)
The header:
#ifndef CORE_HPP
#define CORE_HPP
#include <string>
// See source for doc
int target_function(int object, std::string dest);
#endif
The source:
#include "core.hpp"
#include <string>
#include <format>
#include <print>
// This is the function that I want to provide a mock implementation for.
// Imagine that it comes from some core C++ library, not some local library
// which I have written.
//
// If helpful, imagine this was the `recv` (or similar) function.
//
int target_function(int object, std::string dest) {
std::println("target_function imitates a core function in the C++ standard library");
// simulate doing some work
const auto value = dest.size();
if(value == 0) {
return -1;
}
if(value > 1000) {
return -1;
}
dest.append(std::to_string(object));
return value;
}
some_library
Important to know:
template<typename T> my_function(T, std::string)
.my_function(int, std::string)
, my_function(std::string, std::string)
. The former is the "default" one used by production code, the latter is a mock implementation for unit testing.If you imaging swapping target_function
for recv
, then it may become more obvious what I am trying to do here. The reason why I didn't present that as a MWE is twofold. The code would become significant more complex. It would introduce "noise", with function calls to setup sockets which are otherwise irrelevant to the problem.
add_library(
some_library
STATIC
some_library.cpp
)
target_include_directories(
some_library
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(
some_library
PUBLIC
core_library
)
The header:
#ifndef SOME_LIBRARY_HPP
#define SOME_LIBRARY_HPP
#include <string>
#include <format>
#include <print>
#include "core.hpp"
// Template function, must go in header file, not source file.
template <typename T>
int my_function(
T object,
std::string dest
) {
std::println("simulate doing some work");
std::println("current string length is {}", dest.size());
int value = target_function(object, dest);
if(value < 0) {
std::println("an error occured");
}
std::println("the string length is now {}", dest.size());
std::println("remember: DRY !");
return value;
}
#endif
The source. (This one is effectively empty.)
#include "some_library.hpp"
test_only_library
Important to know:
target_function
.If you were to imagine replacing target_function
with recv
, then the objective may become more obvious.
add_library(
test_only_library
STATIC
test_only_library.cpp
)
target_include_directories(
test_only_library
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(
test_only_library
PUBLIC
core_library
)
The header:
#ifndef TEST_ONLY_LIBRARY_HPP
#define TEST_ONLY_LIBRARY_HPP
#include <string>
// See source for doc
int target_function(std::string object, std::string dest);
#endif
The source:
#include "test_only_library.hpp"
#include <string>
#include <format>
#include <print>
// This is the mock implementation which I have created.
// It lives inside a library which is only linked against the unit testing
// executable. This prevents test code from propagating into the production
// executable.
//
// If helpful, imaging that this was the `recv` (or similar) function.
//
int target_function(std::string object, std::string dest) {
std::println("mocked version of target_function");
std::println("target_function imitates a core function in the C++ standard library");
// no work to do (at least not as much as the real/original function)
dest.append(object);
return 1;
}
build.sh
Not important, just provided to demo how to build.
mkdir build
cd build
cmake .
cmake --build .
CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(example_project)
set(
CMAKE_CXX_STANDARD 23
)
add_subdirectory(core_library)
add_subdirectory(some_library)
add_subdirectory(test_only_library)
add_executable(
example_exe
main.cpp
)
target_link_libraries(
example_exe
core_library
some_library
)
add_executable(
unit_tests_exe
tests.cpp
)
target_link_libraries(
unit_tests_exe
core_library
some_library
test_only_library
)
main.cpp
This is the code for the production executable.
Notice that my_function(int, std::string)
is the production library function call. It is intended to be analagous to calling my_function
with an int
argument, which is a socket fd.
#include <format>
#include <print>
#include "some_library.hpp"
int main() {
std::println("hello world");
int value_of_type_int = 10;
std::string data;
int value = my_function(value_of_type_int, data);
return 0;
}
tests.cpp
This is the code for the unit tests. (Note that these are not real tests.)
In this instance, my_function(std::string, std::string)
is called.
The analogy with recv
is that rather than calling my_function
with an int
file descriptor, my_function
is called with a std::string
buffer with some pre-prepared data for testing.
#include <format>
#include <print>
#include "some_library.hpp"
#include "test_only_library.hpp"
int main() {
std::println("1 == 1");
std::string value_of_type_string("hello");
std::string data;
int value = my_function(value_of_type_string, data);
if(value != 1) {
std::println("error!");
}
if(data != "hello") {
std::println("error!");
}
std::println("all tests complete");
}
When I attempt to build this I encounter the following error:
[ 90%] Building CXX object CMakeFiles/unit_tests_exe.dir/tests.cpp.o
In file included from /home/user/example/tests.cpp:5:
/home/user/example/some_library/some_library.hpp: In instantiation of ‘int my_function(T, std::string) [with T = std::__cxx11::basic_string<char>; std::string = std::__cxx11::basic_string<char>]’:
/home/user/example/tests.cpp:14:28: required from here
14 | int value = my_function(value_of_type_string, data);
| ~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/user/example/some_library/some_library.hpp:21:33: error: cannot convert ‘std::__cxx11::basic_string<char>’ to ‘int’
21 | int value = target_function(object, dest);
| ^~~~~~
| |
| std::__cxx11::basic_string<char>
In file included from /home/user/example/some_library/some_library.hpp:8:
/home/user/example/core_library/core.hpp:8:25: note: initializing argument 1 of ‘int target_function(int, std::string)’
8 | int target_function(int object, std::string dest);
| ~~~~^~~~~~
gmake[2]: *** [CMakeFiles/unit_tests_exe.dir/build.make:76: CMakeFiles/unit_tests_exe.dir/tests.cpp.o] Error 1
gmake[1]: *** [CMakeFiles/Makefile2:167: CMakeFiles/unit_tests_exe.dir/all] Error 2
gmake: *** [Makefile:91: all] Error 2
I am not totally sure what causes this error. The code will work if I put everything into the tests.cpp
file, however this is obviously not the right solution.
It seems as if the compiler does not know about the function
int target_function(std::string, std::string)
which is provided in the library test_only_library
. This is despite the fact that this library is explicitly linked against the test executable.
My guess is that the reason for the error is that the dependency is configured in a way which somewhat doesn't make complete sense.
To explain further, the library some_libray
depends on test_only_library
if and only if T = std::string
. This happens only when the unit test code is built. T = int
for the "production" code.
This suggests that I would need to link test_only_library
against some_library
, but to be honest this is a guess. Even if this works, it is again not really the best idea, since the production code then is likely to contain the test_only_library
code, which should not be shipped with a production application.
Does anyone have any thoughts as to the right direction in which to proceed here?
Upvotes: -1
Views: 37