Prueba unitaria de C ++ y patrones de inyección simulados utilizando rasgos

Hola de nuevo Queda menos de una semana antes del inicio de clases en el grupo en el curso "Desarrollador C ++" . En este sentido, continuamos compartiendo material útil traducido específicamente para estudiantes de este curso.



La prueba unitaria de su código con plantillas se recuerda a sí misma de vez en cuando. (Está probando sus plantillas, ¿verdad?) Algunas plantillas son fáciles de probar. Algunos no lo son. A veces hay una falta de claridad con respecto a la implementación del código simulado (código auxiliar) en la plantilla probada. He observado varias razones por las cuales incrustar código se vuelve complicado.

A continuación, proporcioné algunos ejemplos con una complejidad aproximadamente creciente de implementación de código.

  1. La plantilla toma un argumento de tipo y un objeto del mismo tipo por referencia en el constructor.
  2. La plantilla toma un argumento de tipo. Hace una copia del argumento constructor o simplemente no lo acepta.
  3. Una plantilla toma un argumento de tipo y crea varias plantillas interconectadas sin funciones virtuales.

Comencemos con uno simple.

La plantilla toma un argumento de tipo y un objeto del mismo tipo por referencia en el constructor


Este caso parece simple, porque la prueba unitaria simplemente crea una instancia de la plantilla de prueba con el tipo de código auxiliar. Se puede verificar alguna declaración para la clase simulada. Y eso es todo.

Naturalmente, las pruebas con un solo argumento de tipo no dicen nada sobre el resto del número infinito de tipos que se pueden pasar a la plantilla. Una forma elegante de decir lo mismo: los patrones están conectados por un cuantificador de generalidad, por lo que es posible que tengamos que ser un poco más perspicaces para realizar más pruebas científicas. Más sobre esto más tarde.

Por ejemplo:

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


La plantilla toma un argumento de tipo. Hace una copia del argumento constructor o simplemente no lo acepta


En este caso, el acceso al objeto dentro de la plantilla puede no ser posible debido a los derechos de acceso. Puedes usar clases de 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 tiene acceso al cuerpo de TemplateUnderTest y puede verificar las declaraciones en la copia interna de MockT.

Una plantilla toma un argumento de tipo y crea varias plantillas interconectadas sin funciones virtuales.


Para este caso, veré un ejemplo del mundo real: Google RPC asíncrono .

En C ++, gRPC asíncrono tiene algo llamado CallData, que, como su nombre lo indica, almacena datos relacionados con una llamada RPC . La plantilla CallData puede manejar varios tipos diferentes de RPC. Por lo tanto, es natural que sea implementado precisamente por la plantilla.

Un CallData genérico acepta dos argumentos de tipo: Solicitud y Respuesta. Puede verse así:

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

La prueba unitaria para la plantilla CallData debe verificar el comportamiento de HandleRequest y HandleResponse. Estas funciones invocan varias funciones miembro. Por lo tanto, verificar el estado de su llamada es primordial para el estado de CallData. Sin embargo, hay trucos.

  1. Algunos tipos del espacio de nombres grpc se crean internamente y no se pasan a través del constructor. ServerAsyncResponseWriter y ServerContext , por ejemplo.
  2. grpc :: ServerCompletionQueue se pasa al constructor como argumento, pero no tiene funciones virtuales. Solo un destructor virtual.
  3. grpc :: ServerContext se crea internamente y no tiene funciones virtuales.

La pregunta es cómo probar CallData sin usar gRPC completo en las pruebas. ¿Cómo simular ServerCompletionQueue? ¿Cómo simular ServerAsyncResponseWriter, que en sí mismo es una plantilla? y así sucesivamente ...

Sin funciones virtuales, sustituir el comportamiento del usuario se convierte en una tarea compleja. Los tipos codificados, como grpc :: ServerAsyncResponseWriter, no se pueden modelar porque están, hmm, codificados y no implementados.

Tiene poco sentido pasarlos como argumentos de constructor. Incluso si hace esto, puede que no tenga sentido, ya que pueden ser clases finales o simplemente no tener funciones virtuales.

Entonces, ¿qué hacemos?

Solución: rasgos




En lugar de incorporar un comportamiento personalizado heredando de un tipo genérico (como se hace en la programación orientada a objetos), INSERTE EL TIPO. Usamos rasgos para esto. Nos especializamos en rasgos de diferentes maneras dependiendo de qué tipo de código sea: un código de producción o un código de prueba de unidad.

Considere CallDataTraits

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

Esta es la plantilla principal para el rasgo utilizado para el código de producción. Vamos a usarlo en un 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 }; 

Mirando el código anterior, está claro que el código de la aplicación todavía usa tipos del espacio de nombres grpc. Sin embargo, podemos reemplazar fácilmente los tipos de grpc con tipos ficticios. Ver abajo.

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

Los rasgos nos permitieron elegir los tipos implementados en CallData, dependiendo de la situación. Este método no requiere rendimiento adicional, ya que no se crearon funciones virtuales innecesarias para agregar funcionalidad. Esta técnica también se puede utilizar en las clases finales.

¿Cómo te gusta el material? Escribir comentarios Y nos vemos en la puerta abierta ;-)

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


All Articles