I have a bunch of unit tests I want to migrate from Visual Studio's test framework to something more accessible. I started moving some to ctest, where (as I understand) each test needs to compile to a separate executable.
My cmakelists.txt looks like:
cmake_minimum_required(VERSION 3.10)
project(UnitTests)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_MACOSX_RPATH 1)
include(CTest)
enable_testing()
add_executable(Test1 Test1.cpp)
add_executable(Test2 Test2.cpp)
add_test(First Test1)
add_test(Second Test2)
The problem is that Test1 and Test2 compile to files of 2.7 and 2.6 MB, respectively. This is because each has to have #include "BigClass.h"
, where BigClass
is the big, sprawling project I actually want to test.
I have maybe a few dozen tests I want to migrate, so to compile and run all these tests would likely run into hundreds of megabytes of executables, where most of the space is simply repeating all the code from BigClass
!
I tried to include this class as a library with:
add_library(BigClass SHARED ../path/to/BigClass.cpp)
and then at the end of the file:
target_link_libraries(Test1 PUBLIC BigClass)
but this did not affect the space.
I think what I want is a way to dynamically link to BigClass
(which would be compiled already anyway), so all the tests can share the code. But I'm not sure how to do this and I'm not quite sure how to search for it (searching for dynamically linked #include
reveals that this is not really possible, for example).
Can anyway help me find either:
- a cross-platform C++ testing framework that avoids all these problems?
Ignoring the fact that code recommendations are off-topic…
We are using catch2, and I'm quite happy with this.
- a way to compile unit tests in ctest to share a lot of the same code?
Tests are yet other applications. So, the answer to next question should cover this.
- directions on how to link C++ classes dynamically?
I've made an MCVE where I tried to cover the whole story. Be prepared…
When code is written to become part of a DLL, it has to export all symbols which shall become visible in other DLLs or the executable. The other DLLs and the executable have to import these symbols in turn. There is a general introduction to this topic provided by Microsoft: dllexport, dllimport.
There is a usual macro trickery to ensure that the same headers can be used from inside the DLL (where dllexport
is needed) and outside of DLL (where dllimport
is needed).
This is demonstrated in the BigClass.h
:
#ifndef BIG_CLASS_H
#define BIG_CLASS_H
/* A macro trickery to provide export and import prefixes
* platform dependent for Windows and Linux.
*/
#ifdef _WIN32
// for Windows, Visual C++
#define DLL_EXPORT __declspec(dllexport)
#define DLL_IMPORT __declspec(dllimport)
#else // (not) _WIN32
// for gcc
#define DLL_EXPORT __attribute__((visibility("default")))
#define DLL_IMPORT __attribute__((visibility("default")))
#endif // _WIN32
/* The macro trickery to define how exported symbols shall
* be included.
*/
#ifdef BIG_CLASS_STATIC
#define BIG_CLASS_API
#else // (not) BIG_CLASS_STATIC
#ifdef BUILD_BIG_CLASS
#define BIG_CLASS_API DLL_EXPORT
#else // (not) BUILD_BIG_CLASS
#define BIG_CLASS_API DLL_IMPORT
#endif // BUILD_BIG_CLASS
#endif // BIG_CLASS_STATIC
// the library interface
#include <iostream>
struct BigClass {
const int id = 0;
// inline constructors doesn't need an export guard
BigClass() = default;
// non-inline constructors need an export guard
BIG_CLASS_API BigClass(int id);
// non-inline functins as well
BIG_CLASS_API void print() const;
// no export guard for inline member functions
void printLn() const
{
print();
std::cout << std::endl;
}
};
// functions need to be exported
extern BIG_CLASS_API void print(const BigClass&);
// inline functions don't need to be exported
inline void printLn(const BigClass& big)
{
big.printLn();
}
#endif // BIG_CLASS_H
Thereby, the first macros DLL_EXPORT
and DLL_IMPORT
are introduced to hide the different declaration attributes between Windows (Visual Studio) and Linux (g++).
These definitions can be shared over multiple libraries.
The second set of macros is library specific – hence, the prefix BIG_CLASS_
in all of them.
The BIG_CLASS_STATIC
is a macro which should be defined as argument for the compiler if (and only if) the library is built as static library. It has to be defined as well if the static library has to be linked to an executable.
The BUILD_BIG_CLASS
is a macro which should be defined instead as argument for the compiler if (and only if) the library is built and linked as DLL (or shared object in Linux).
Both macros are considered to prepare a third macro BIG_CLASS_API
which expands to
DLL_EXPORT
if the DLL is builtDLL_IMPORT
if anything else is built intended to use and link the DLL.The corresponding BigClass.cc
:
#include "BigClass.h"
BigClass::BigClass(int id): id(id) { }
void BigClass::print() const
{
std::cout << "BigClass::print(): *this: BigClass { id: " << id << " }";
}
void print(const BigClass& big)
{
std::cout << "print(): "; big.print();
}
There is nothing special about it.
BigClass.h
and BigClass.cc
will be used to build the sample BigClass.dll
.
Additionally, two test applications Test1
and Test2
which shall be linked with BigClass.dll
.
Test1.cc
:
#include <iostream>
#include <BigClass.h>
#define TEST(...) std::cout << #__VA_ARGS__ << ";\n"; __VA_ARGS__
int main()
{
std::cout
<< "Test1:\n"
<< "======\n";
TEST(BigClass big);
TEST(big.print());
std::cout << '\n';
TEST(big.printLn());
TEST(BigClass big1(1));
TEST(big1.print());
std::cout << '\n';
TEST(big1.printLn());
std::cout << "Done." << std::endl;
}
Test2.cc
:
#include <iostream>
#include <BigClass.h>
#define TEST(...) std::cout << #__VA_ARGS__ << ";\n"; __VA_ARGS__
int main()
{
std::cout
<< "Test2:\n"
<< "======\n";
TEST(BigClass big);
TEST(print(big));
std::cout << '\n';
TEST(printLn(big));
TEST(BigClass big2(2));
TEST(print(big2));
std::cout << '\n';
TEST(printLn(big2));
std::cout << "Done." << std::endl;
}
Again, there is nothing special about them.
Disclaimer: The macro TEST()
has nothing to do with unit tests. It's just a kind of demo fiddling.
Finally, the CMakeLists.txt
to put everything together (e.g. in a VS solution BigClassDLL.sln
):
project (BigClassDLL)
cmake_minimum_required(VERSION 3.10.0)
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
if (MSVC)
# Unfortunately neither
# CMAKE_CXX_STANDARD_REQUIRED ON
# nor
# CMAKE_CXX_EXTENSIONS OFF
# force the /permissive- option
# which makes MSVC standard conform.
add_compile_options(/permissive-)
endif()
# enable shared libraries (aka. DLLs)
option(BUILD_SHARED_LIBS "Build using shared libraries" ON)
include_directories("${CMAKE_SOURCE_DIR}")
# build the library
add_library(BigClass
BigClass.cc BigClass.h)
set_target_properties(BigClass
PROPERTIES
PROJECT_LABEL "Big Class DLL"
DEFINE_SYMBOL BUILD_BIG_CLASS)
if (NOT BUILD_SHARED_LIBS)
target_compile_definitions(BigClass PUBLIC BIG_CLASS_STATIC)
endif()
# build the tests
add_executable(Test1 Test1.cc)
target_link_libraries(Test1 BigClass)
add_executable(Test2 Test2.cc)
target_link_libraries(Test2 BigClass)
There are some specific things concerning the dynamic linking:
option(BUILD_SHARED_LIBS "Build using shared libraries" ON)
This is explained in CMake: Step 9: Selecting Static or Shared Libraries.
set_target_properties(BigClass PROPERTIES DEFINE_SYMBOL BUILD_BIG_CLASS)
This ensures that macro BUILD_BIG_CLASS
will exclusively be defined in the command line of the compiler when the library BigClass
is built.
if (NOT BUILD_SHARED_LIBS)
target_compile_definitions(BigClass PUBLIC BIG_CLASS_STATIC)
endif()
The macro BIG_CLASS_STATIC
will be defined in every compiler command line where the library BigClass
is built or used.
Disclaimer: I'm aware of that one CMakeLists.txt
for multiple projects is considered as bad style. For a serious project, libraries should reside in their own directory with their own CMakeLists.txt
file, of course. I put it altogether to keep it as minimal as possible (and also because I can).
How it looks in Visual Studio 2019:
The build artefacts of Visual Studio 2019:
Running Test1.exe
:
Test1:
======
BigClass big;
big.print();
BigClass::print(): *this: BigClass { id: 0 }
big.printLn();
BigClass::print(): *this: BigClass { id: 0 }
BigClass big1(1);
big1.print();
BigClass::print(): *this: BigClass { id: 1 }
big1.printLn();
BigClass::print(): *this: BigClass { id: 1 }
Done.
Running Test2.exe
:
Test2:
======
BigClass big;
print(big);
print(): BigClass::print(): *this: BigClass { id: 0 }
printLn(big);
BigClass::print(): *this: BigClass { id: 0 }
BigClass big2(2);
print(big2);
print(): BigClass::print(): *this: BigClass { id: 2 }
printLn(big2);
BigClass::print(): *this: BigClass { id: 2 }
Done.
Finally (to address the portability), I built the same CMake project in Linux:
Scheff'sCat@debian:/mnt/hgfs/hostD/Entwicklung/tests/C++/dll$ mkdir build-Linux
Scheff'sCat@debian:/mnt/hgfs/hostD/Entwicklung/tests/C++/dll$ cd build-Linux
Scheff'sCat@debian:/mnt/hgfs/hostD/Entwicklung/tests/C++/dll/build-Linux$ ccmake ..
Scheff'sCat@debian:/mnt/hgfs/hostD/Entwicklung/tests/C++/dll/build-Linux$ cmake --build .
Scanning dependencies of target BigClass
[ 16%] Building CXX object CMakeFiles/BigClass.dir/BigClass.cc.o
[ 33%] Linking CXX shared library libBigClass.so
[ 33%] Built target BigClass
Scanning dependencies of target Test2
[ 50%] Building CXX object CMakeFiles/Test2.dir/Test2.cc.o
[ 66%] Linking CXX executable Test2
[ 66%] Built target Test2
Scanning dependencies of target Test1
[ 83%] Building CXX object CMakeFiles/Test1.dir/Test1.cc.o
[100%] Linking CXX executable Test1
[100%] Built target Test1
Scheff'sCat@debian:/mnt/hgfs/hostD/Entwicklung/tests/C++/dll/build-Linux$ ls
CMakeCache.txt CMakeFiles cmake_install.cmake libBigClass.so Makefile Test1 Test2
Scheff'sCat@debian:/mnt/hgfs/hostD/Entwicklung/tests/C++/dll/build-Linux$ ./Test1
Test1:
======
BigClass big;
big.print();
BigClass::print(): *this: BigClass { id: 0 }
big.printLn();
BigClass::print(): *this: BigClass { id: 0 }
BigClass big1(1);
big1.print();
BigClass::print(): *this: BigClass { id: 1 }
big1.printLn();
BigClass::print(): *this: BigClass { id: 1 }
Done.
Scheff'sCat@debian:/mnt/hgfs/hostD/Entwicklung/tests/C++/dll/build-Linux$ ./Test2
Test2:
======
BigClass big;
print(big);
print(): BigClass::print(): *this: BigClass { id: 0 }
printLn(big);
BigClass::print(): *this: BigClass { id: 0 }
BigClass big2(2);
print(big2);
print(): BigClass::print(): *this: BigClass { id: 2 }
printLn(big2);
BigClass::print(): *this: BigClass { id: 2 }
Done.
Scheff'sCat@debian:/mnt/hgfs/hostD/Entwicklung/tests/C++/dll/build-Linux$