c++unit-testingprivateprotectedmembers

Testing private class member in C++ without friend


Today I had a discussion with a colleague on whether to test or not to test private members or private state in the class. He almost convinced me why it makes sense. This question does not aim to duplicate already existing StackOverflow questions about the nature and reason of testing private members, like: What is wrong with making a unit test a friend of the class it is testing?

Colleagues suggestion was in my opinion a bit fragile to introduce the friend declaration to the unit test implementation class. In my opinion this is a no-go, because we introduce some dependency of tested code to the test code, whereas test code already depends on tested code => cyclic dependency. Even such innocent things like renaming a test class results in breaking unit tests and enforces code changes in tested code.

I'd like to ask C++ gurus to judge on the other proposal, which relies on the fact that we are allowed to specialize a template function. Just imagine the class:

// tested_class.h

struct tested_class 
{
  tested_class(int i) : i_(i) {}

  //some function which do complex things with i
  // and sometimes return a result

private:
  int i_;
};

I don't like the idea to have a getter for i_ just to make it testable. So my proposal is 'test_backdoor' function template declaration in the class:

// tested_class.h

struct tested_class 
{
  explicit
  tested_class(int i=0) : i_(i) {}

  template<class Ctx>
  static void test_backdoor(Ctx& ctx);

  //some function which do complex things with i
  // and sometimes return a result

private:
  int i_;
};

By adding just this function we can make the class' private members testable. Note, there is no dependency to unit test classes, nor the template function implementation. In this example the unit test implementation uses Boost Test framework.

// tested_class_test.cpp

namespace
{
  struct ctor_test_context
  {
    tested_class& tc_;
    int expected_i;
  };
}

// specialize the template member to do the rest of the test
template<>
void tested_class::test_backdoor<ctor_test_context>(ctor_test_context& ctx)
{
  BOOST_REQUIRE_EQUAL(ctx.expected_i, tc_.i_);
}

BOOST_AUTO_TEST_CASE(tested_class_default_ctor)
{
  tested_class tc;
  ctor_test_context ctx = { tc, 0 };
  tested_class::test_backdoor(ctx);
}

BOOST_AUTO_TEST_CASE(tested_class_value_init_ctor)
{
  tested_class tc(-5);
  ctor_test_context ctx = { tc, -5 };
  tested_class::test_backdoor(ctx);
}

By introducing just a single template declaration, which is not callable at all, we give the test implementer a possibility to forward test logic into a function. The function, acts on type safe contexts and is only visible from inside the particular test compilation unit, due to anonymous type nature of test context. And the best thing is, we can define as many anonymous test contexts as we like and specialize tests on them, without ever touching the tested class.

Sure, the users must know what template specialization is, but is this code really bad or weird or unreadable? Or can I expect from C++ developers to have the knowledge what C++ template specialization is and how it works?

Elaborating on using friend to declare unit test class I don't think this is robust. Imagine boost framework (or may be other test frameworks). It generates for every test case a separate type. But why should I care as long I can write:

BOOST_AUTO_TEST_CASE(tested_class_value_init_ctor)
{
  ...
}

If using friends, I had to declare each test case as a friend then... Or end up introducing some test functionality in some common type (like fixture), declare it as a friend, and forward all test calls to that type... Isn't that weird?

I would like to see your pro and cons practicing this approach.


Solution

  • What will follow is not technically speaking a straight answer to your question as it will still make use of the "friend" functionality but it does not require modification of the tested entity itself and I think it addesses the concern of breaking the encapsulation mentioned in some of the other answers; it does though require writing some boilerplate code.

    The idea behind it is not mine and the implementation is entirely based on a trick presented and explained by litb on his blog(coupled with this Sutter's gotw for just a little bit more context, at least for me) - in short CRTP, friends, ADL and pointers to members (I must confess that to my dismay the ADL part I still don't get it entirely, but I'm relentesly working in figuring it out 100%).

    I tested it with gcc 4.6, clang 3.1 and VS2010 compilers and it works perfectly.

    /* test_tag.h */
    #ifndef TEST_TAG_H_INCLUDED_
    #define TEST_TAG_H_INCLUDED_
    
    template <typename Tag, typename Tag::type M>
    struct Rob
    {
        friend typename Tag::type get(Tag)
        {
            return M;
        }
    };
    
    template <typename Tag, typename Member> 
    struct TagBase
    {
        typedef Member type;
        friend type get(Tag);
    };
    
    
    #endif /* TEST_TAG_H_INCLUDED_ */
    
    /* tested_class.h */
    #ifndef TESTED_CLASS_H_INCLUDED_
    #define TESTED_CLASS_H_INCLUDED_
    
    #include <string>
    
    struct tested_class
    {
        tested_class(int i, const char* descr) : i_(i), descr_(descr) { }
    
    private:
        int i_;
        std::string descr_;
    };
    
    /* with or without the macros or even in a different file */
    #   ifdef TESTING_ENABLED
    #   include "test_tag.h"
    
        struct tested_class_i : TagBase<tested_class_i, int tested_class::*> { };
        struct tested_class_descr : TagBase<tested_class_descr, const std::string tested_class::*> { };
    
        template struct Rob<tested_class_i, &tested_class::i_>;
        template struct Rob<tested_class_descr, &tested_class::descr_>;
    
    #   endif
    
    #endif /* TESTED_CLASS_H_INCLUDED_ */
    
    /* test_access.cpp */
    #include "tested_class.h"
    
    #include <cstdlib>
    #include <iostream>
    #include <sstream>
    
    #define STRINGIZE0(text) #text
    #define STRINGIZE(text) STRINGIZE0(text)
    
    int assert_handler(const char* expr, const char* theFile, int theLine)
    {
        std::stringstream message;
        message << "Assertion " << expr << " failed in " << theFile << " at line " << theLine;
        message << "." << std::endl;
        std::cerr << message.str();
    
        return 1;
    }
    
    #define ASSERT_HALT() exit(__LINE__)
    
    #define ASSERT_EQUALS(lhs, rhs) ((void)(!((lhs) == (rhs)) && assert_handler(STRINGIZE((lhs == rhs)), __FILE__, __LINE__) && (ASSERT_HALT(), 1)))
    
    int main()
    {
        tested_class foo(35, "Some foo!");
    
        // the bind pointer to member by object reference could
        // be further wrapped in some "nice" macros
        std::cout << " Class guts: " << foo.*get(tested_class_i()) << " - " << foo.*get(tested_class_descr()) << std::endl;
        ASSERT_EQUALS(35, foo.*get(tested_class_i()));
        ASSERT_EQUALS("Some foo!", foo.*get(tested_class_descr()));
    
        ASSERT_EQUALS(80, foo.*get(tested_class_i()));
    
        return 0; 
    }