Search code examples
c++unit-testingcmakectest

Each CTest executable is several MB - how do I make them smaller?


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:

  1. a cross-platform C++ testing framework that avoids all these problems?
  2. a way to compile unit tests in ctest to share a lot of the same code?
  3. directions on how to link C++ classes dynamically?

Solution

    1. 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.

    1. 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.

    1. 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

    • nothing for a static library
    • DLL_EXPORT if the DLL is built
    • DLL_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:

    1. option(BUILD_SHARED_LIBS "Build using shared libraries" ON)
      This is explained in CMake: Step 9: Selecting Static or Shared Libraries.

    2. 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.

    3. 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:

    Snapshot of VS 2019

    The build artefacts of Visual Studio 2019:

    Snapshot of Build binary dir

    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$