📜 ⬆️ ⬇️

Unit testing of C ++ and Mock Injection patterns using Traits

Hello again! Before the start of classes in the group “Developer C ++”, there is less than a week. In this regard, we continue to share useful material translated specifically for the students of this course.



Unit testing of your code with templates from time to time reminds of itself. (You are testing your templates, right?) Some templates are easy to test. Some are not. Sometimes there is a lack of final clarity about the implementation of the mock code (stub) in the test pattern. I have observed several reasons why code injection becomes difficult.
')
Below, I have cited several examples with an approximately increasing complexity of code injection.

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

Let's start with the simple.

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 template under test with a 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 pattern. An elegant way to say the same thing: templates are a quantifier of generality, so we may have to become a little more insightful for more scientific testing. More on this later.

For example:

template <class T> 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<MockT> test(&mock); test.SomeMethod(); assert(DoSomethingWasCalled(mock)); assert(DoSomeOtherThingWasCalled(mock)); } }; 


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


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

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

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

The template accepts a type argument and creates several interrelated templates without virtual functions.


For this case, I'll look at a real-life 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 RPCs of different types. So it is natural that it is implemented exactly by the template.

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

 template <class Request, class Response> class CallData { grpc::ServerCompletionQueue *cq_; grpc::ServerContext context_; grpc::ServerAsyncResponseWriter<Response> 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 cause a number of member functions. Therefore, checking the health of their call is paramount to the health of CallData. However, there is a trick.

  1. Some types from the grpc namespace are created internally and are not passed through the constructor. ServerAsyncResponseWriter and ServerContext , for example.
  2. grpc :: ServerCompletionQueue is passed to the constructor as an argument, but has no virtual functions. Only virtual destructor.
  3. grpc :: ServerContext is created inside and has no virtual functions.

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

Without virtual functions, substitution of user behavior becomes difficult. Mailshare types, such as grpc :: ServerAsyncResponseWriter, cannot be modeled because they are, um, hard-coded and not implemented.

In transferring them as arguments to the constructor, I'm confused. Even if this is done, it may be meaningless, since they may be final-classes or simply not have virtual functions.

So what do we do?

Solution: Traits




Instead of introducing custom behavior by inheriting from a generic type (as is done in object-oriented programming), insert the CAM TYPE. We use traits for this. We specialize treats differently depending on what kind of code it is: production code or unit testing code.

Consider CallDataTraits

 template <class CallData> class CallDataTraits { using ServerCompletionQueue = grpc::ServerCompletionQueue; using ServerContext = grpc::ServerContext; using ServerAsyncResponseWriter = grpc::ServerAsyncResponseWrite<typename CallData::ResponseType>; }; 

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

 /// Unit testable CallData template <class Request, class Response> class CallData { typename CallDataTraits<CallData>::ServerCompletionQueue *cq_; typename CallDataTraits<CallData>::ServerContext context_; typename CallDataTraits<CallData>::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<TestRequest, TestResponse>; /// A specialization of CallDataTraits for unit testing purposes only. template <> class CallDataTraits<CallDataUnderTest> { 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 ;-)

Source: https://habr.com/ru/post/457110/


All Articles