user2138149
user2138149

Reputation: 17266

How to build a library for tests with CMake which includes a mocked version of an existing function?

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:

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:

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:

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

Answers (0)

Related Questions