Unit Testing C ++ and Mock Injection Patterns Using Traits

Original author: C ++ TRUTHS
  • Transfer
Hello again! Less than a week is left before the start of classes in the group at the course "C ++ Developer" . In this regard, we continue to share useful material translated specifically for students of this course.



Unit testing of your code with templates reminds of itself from time to time. (You are testing your templates, right?) Some templates are easy to test. Some are not. Sometimes there is a lack of ultimate clarity regarding the implementation of mock-code (stub) in the tested template. I have observed several reasons why embedding code becomes complicated.

Below I gave some examples with approximately increasing complexity of code implementation.

  1. The template takes a type argument and an object of the same type by reference in the constructor.
  2. The template takes a type argument. Makes a copy of the constructor argument or simply does not accept it.
  3. A template takes a type argument and creates several interconnected templates without virtual functions.

Let's start with a simple one.

The template takes a type argument and an object of the same type by reference in the constructor


This case seems simple, because the unit test simply creates an instance of the test template with the stub type. Some statement can be checked for the mock class. And that is all.

Naturally, testing with only one type argument says nothing about the rest of the infinite number of types that can be passed to the template. An elegant way to say the same thing: patterns are connected by a quantifier of generality, so we may have to become a little more insightful for more scientific testing. More on this later.

For instance:

template 
class TemplateUnderTest {
  T *t_;
public:
  TemplateUnderTest(T *t) : t_(t) {}
  void SomeMethod() {
    t->DoSomething();
    t->DoSomeOtherThing();
  }
};
struct MockT {
  void DoSomething() { 
    // Some assertions here.
	  }
  void DoSomeOtherThing() { 
    // Some more assertions here.
  }
};
class UnitTest {
  void Test1() {
    MockT mock;
    TemplateUnderTest test(&mock);
    test.SomeMethod();
    assert(DoSomethingWasCalled(mock));
    assert(DoSomeOtherThingWasCalled(mock));
  }
};


The template takes a type argument. Makes a copy of the constructor argument or simply does not accept it


In this case, access to the object inside the template may not be possible due to access rights. You can use friend-classes.

template 
class TemplateUnderTest {
  T t_;
  friend class UnitTest;
public:
  void SomeMethod() {
    t.DoSomething();
    t.DoSomeOtherThing();
  }
};
class UnitTest {
  void Test2() {
    TemplateUnderTest test;
    test.SomeMethod();
    assert(DoSomethingWasCalled(test.t_)); // access guts
    assert(DoSomeOtherThingWasCalled(test.t_)); // access guts
  }
};

UnitTest :: Test2 has access to the body of TemplateUnderTest and can check statements on the internal copy of MockT.

A template takes a type argument and creates several interconnected templates without virtual functions


For this case, I'll look at a real-world example: Asynchronous Google RPC .

In C ++, async gRPC has something called CallData, which, as the name implies, stores data related to an RPC call . The CallData template can handle several different types of RPCs. So it is natural that it is implemented precisely by the template.

A generic CallData accepts two type arguments: Request and Response. It can look like this:

template 
class CallData {
  grpc::ServerCompletionQueue *cq_;
  grpc::ServerContext context_;
  grpc::ServerAsyncResponseWriter responder_;
  // ... some more state
public:
  using RequestType = Request;
  using ResponseType = Response;
  CallData(grpc::ServerCompletionQueue *q)
    : cq_(q),
      responder_(&context_) 
  {}
  void HandleRequest(Request *req); // application-specific code
  Response *GetResponse(); // application-specific code
};

The unit test for the CallData template should check the behavior of HandleRequest and HandleResponse. These functions invoke a number of member functions. Therefore, verifying the health of their call is paramount to the health of CallData. However, there are tricks.

  1. Some types from the grpc namespace are created internally and are not passed through the constructor. ServerAsyncResponseWriterand ServerContextfor example.
  2. grpc :: ServerCompletionQueuepassed to the constructor as an argument, but does not have virtual functions. Only a virtual destructor.
  3. grpc :: ServerContext It is created inside and has no virtual functions.

The question is how to test CallData without using full gRPC in the tests? How to simulate ServerCompletionQueue? How to simulate ServerAsyncResponseWriter, which itself is a template? and so on ...

Without virtual functions, substituting user behavior becomes a complex task. Hardcoded types, such as grpc :: ServerAsyncResponseWriter, cannot be modeled because they are, hmm, hardcoded and not implemented.

There is little sense in passing them as constructor arguments. Even if you do this, it might not make sense, since they may be final classes or simply not have virtual functions.

So what do we do?

Solution: Traits




Instead of embedding custom behavior by inheriting from a generic type (as is done in object-oriented programming), INSERT THE TYPE. We use traits for this. We specialize in traits in different ways depending on what kind of code it is: a production code or a unit testing code.

ConsiderCallDataTraits

template 
class CallDataTraits {
  using ServerCompletionQueue = grpc::ServerCompletionQueue;
  using ServerContext = grpc::ServerContext;
  using ServerAsyncResponseWriter = grpc::ServerAsyncResponseWrite;
};

This is the main template for trait used for production code. Let's use it in a CallDatatemplate.

/// Unit testable CallData
template 
class CallData { 
  typename CallDataTraits::ServerCompletionQueue *cq_;
  typename CallDataTraits::ServerContext context_;
  typename CallDataTraits::ServerAsyncResponseWriter responder_;
  // ... some more state
public:
  using RequestType = Request;
  using ResponseType = Response;
  CallData(typename CallDataTraits::ServerCompletionQueue *q)
    : cq_(q),
      responder_(&context_) 
  {}
  void HandleRequest(Request *req); // application-specific code
  Response *GetResponse(); // application-specific code
};

Looking at the code above, it is clear that the application code still uses types from the grpc namespace. However, we can easily replace grpc types with dummy types. See below.

/// In unit test code
struct TestRequest{};
struct TestResponse{};
struct MockServerCompletionQueue{};
struct MockServerContext{};
struct MockServerAsyncResponseWriter{};
/// We want to unit test this type.
using CallDataUnderTest = CallData;
/// A specialization of CallDataTraits for unit testing purposes only.
template <>
class CallDataTraits {
  using ServerCompletionQueue = MockServerCompletionQueue;
  using ServerContext = MockServerContext;
  using ServerAsyncResponseWriter = MockServerAsyncResponseWrite;
};
MockServerCompletionQueue mock_queue;
CallDataUnderTest cdut(&mock_queue); // Now injected with mock types.

Traits allowed us to choose the types implemented in CallData, depending on the situation. This method does not require additional performance, since no unnecessary virtual functions were created to add functionality. This technique can also be used in final classes.

How do you like the material? Write comments. And see you at the open door ;-)

Also popular now: