使用特性对C ++和模拟注入模式进行单元测试

你好! 在“ C ++ Developer”课程的班级上课之前,还剩下不到一周的时间。 在这方面,我们将继续分享专门为该课程的学生翻译的有用材料。



使用模板对代码进行单元测试有时会让人想起自己。 (您正在测试模板,对吗?)某些模板易于测试。 有些不是。 有时,在测试的模板中,关于模拟代码(存根)的实现缺乏清晰性。 我已经观察到嵌入代码变得复杂的几个原因。

下面,我给出了一些示例,这些示例的代码实现复杂度不断提高。

  1. 模板在构造函数中通过引用接受类型实参和相同类型的对象。
  2. 模板带有类型参数。 复制构造函数参数或根本不接受它。
  3. 模板采用类型自变量,并创建多个没有虚拟功能的互连模板。

让我们从一个简单的开始。

模板在构造函数中通过引用接受类型实参和相同类型的对象


这种情况似乎很简单,因为单元测试只是使用存根类型创建测试模板的实例。 可以检查某些语句以获取模拟类。 仅此而已。

自然地,仅使用一个类型参数进行测试不会对可以传递给模板的其他无限数量的类型产生任何影响。 用相同的方式优雅地表达一句话:模式由一般性的量词联系在一起,因此对于更科学的测试,我们可能必须变得更有见识。 稍后再详细介绍。

例如:

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)); } }; 


模板带有类型参数。 复制构造函数参数或根本不接受它


在这种情况下,由于访问权限,可能无法访问模板内的对象。 您可以使用friend类。

 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可以访问TemplateUnderTest的主体,并且可以检查MockT的内部副本上的语句。

模板采用类型参数,并创建多个没有虚拟功能的互连模板


对于这种情况,我将看一个真实的示例: Asynchronous Google RPC

在C ++中,异步gRPC有一个称为CallData的东西,顾名思义,它存储与RPC调用相关的数据 。 CallData模板可以处理几种不同类型的RPC。 因此很自然地,它是由模板精确实现的。

通用CallData接受两个类型参数:Request和Response。 它看起来可能像这样:

 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 }; 

CallData模板的单元测试应检查HandleRequest和HandleResponse的行为。 这些函数调用许多成员函数。 因此,验证其呼叫的运行状况对CallData的运行状况至关重要。 但是,有一些技巧。

  1. grpc命名空间中的某些类型是在内部创建的,不会通过构造函数传递。 例如, ServerAsyncResponseWriterServerContext
  2. grpc :: ServerCompletionQueue作为参数传递给构造函数,但没有虚函数。 只有虚拟析构函数。
  3. grpc :: ServerContext是在内部创建的,没有虚拟功能。

问题是如何在不使用完整gRPC的情况下测试CallData? 如何模拟ServerCompletionQueue? 如何模拟本身就是模板的ServerAsyncResponseWriter? 等等...

没有虚拟功能,替代用户行为将成为一项艰巨的任务。 无法对硬编码类型(例如grpc :: ServerAsyncResponseWriter)进行建模,因为它们是hmm硬编码的,并且未实现。

将它们作为构造函数参数传递几乎没有任何意义。 即使您这样做,也可能没有意义,因为它们可能是最终类,或者根本没有虚函数。

那我们该怎么办?

解决方案:特性




INSERT THE TYPE而不是通过从通用类型继承来嵌入自定义行为(如在面向对象的编程中所做的那样)。 为此,我们使用特征。 我们根据特征代码的种类以不同的方式专门研究特征:生产代码或单元测试代码。

考虑CallDataTraits

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

这是用于生产代码的特征的主要模板。 让我们在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 }; 

查看上面的代码,很明显,应用程序代码仍然使用grpc命名空间中的类型。 但是,我们可以轻松地用虚拟类型替换grpc类型。 见下文。

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

特性使我们可以根据情况选择在CallData中实现的类型。 此方法不需要额外的性能,因为没有创建不必要的虚拟功能来添加功能。 此技术也可以在最终课程中使用。

您如何看待材料? 写评论。 见到你在敞开的门;-)

Source: https://habr.com/ru/post/zh-CN457110/


All Articles