Unit-Test von C ++ - und Mock-Injection-Mustern unter Verwendung von Merkmalen

Hallo nochmal! Es bleibt weniger als eine Woche vor dem Beginn des Unterrichts in der Gruppe im Kurs "C ++ Developer" . In diesem Zusammenhang teilen wir weiterhin nützliches Material, das speziell für Studenten dieses Kurses übersetzt wurde.



Das Unit-Testen Ihres Codes mit Vorlagen erinnert sich von Zeit zu Zeit an sich selbst. (Sie testen Ihre Vorlagen, richtig?) Einige Vorlagen sind einfach zu testen. Einige sind es nicht. Manchmal mangelt es an äußerster Klarheit hinsichtlich der Implementierung von Mock-Code (Stub) in der getesteten Vorlage. Ich habe mehrere Gründe beobachtet, warum das Einbetten von Code kompliziert wird.

Im Folgenden habe ich einige Beispiele mit ungefähr zunehmender Komplexität der Code-Implementierung gegeben.

  1. Die Vorlage verwendet ein Typargument und ein Objekt desselben Typs als Referenz im Konstruktor.
  2. Die Vorlage nimmt ein Typargument an. Erstellt eine Kopie des Konstruktorarguments oder akzeptiert es einfach nicht.
  3. Eine Vorlage verwendet ein Typargument und erstellt mehrere miteinander verbundene Vorlagen ohne virtuelle Funktionen.

Beginnen wir mit einem einfachen.

Die Vorlage verwendet ein Typargument und ein Objekt desselben Typs als Referenz im Konstruktor


Dieser Fall scheint einfach zu sein, da der Komponententest einfach eine Instanz der Testvorlage mit dem Stub-Typ erstellt. Einige Anweisungen können für die Scheinklasse überprüft werden. Und das ist alles.

Das Testen mit nur einem Typargument sagt natürlich nichts über den Rest der unendlichen Anzahl von Typen aus, die an die Vorlage übergeben werden können. Eine elegante Art, dasselbe zu sagen: Muster sind durch einen Quantifizierer der Allgemeinheit verbunden, so dass wir für wissenschaftlichere Tests möglicherweise etwas aufschlussreicher werden müssen. Dazu später mehr.

Zum Beispiel:

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


Die Vorlage nimmt ein Typargument an. Erstellt eine Kopie des Konstruktorarguments oder akzeptiert es einfach nicht


In diesem Fall ist der Zugriff auf das Objekt in der Vorlage aufgrund von Zugriffsrechten möglicherweise nicht möglich. Sie können friend verwenden.

 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 hat Zugriff auf den Hauptteil von TemplateUnderTest und kann Anweisungen in der internen Kopie von MockT überprüfen.

Eine Vorlage verwendet ein Typargument und erstellt mehrere miteinander verbundene Vorlagen ohne virtuelle Funktionen


In diesem Fall sehe ich mir ein Beispiel aus der Praxis an : Asynchroner Google RPC .

In C ++ verfügt async gRPC über CallData, in dem, wie der Name schon sagt, Daten zu einem RPC-Aufruf gespeichert werden . Die CallData-Vorlage kann verschiedene Arten von RPCs verarbeiten. Es ist also selbstverständlich, dass es genau von der Vorlage implementiert wird.

Eine generische CallData akzeptiert zwei Typargumente: Request und Response. Es kann so aussehen:

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

Der Komponententest für die CallData-Vorlage sollte das Verhalten von HandleRequest und HandleResponse überprüfen. Diese Funktionen rufen eine Reihe von Elementfunktionen auf. Daher ist die Überprüfung des Zustands ihres Anrufs für den Zustand von CallData von größter Bedeutung. Es gibt jedoch Tricks.

  1. Einige Typen aus dem grpc-Namespace werden intern erstellt und nicht durch den Konstruktor übergeben. ServerAsyncResponseWriter Beispiel ServerAsyncResponseWriter und ServerContext .
  2. grpc :: ServerCompletionQueue wird als Argument an den Konstruktor übergeben, hat jedoch keine virtuellen Funktionen. Nur ein virtueller Destruktor.
  3. grpc :: ServerContext wird intern erstellt und hat keine virtuellen Funktionen.

Die Frage ist, wie CallData ohne Verwendung von vollständigem gRPC in den Tests getestet werden kann. Wie simuliere ich ServerCompletionQueue? Wie simuliere ich ServerAsyncResponseWriter, der selbst eine Vorlage ist? usw…

Ohne virtuelle Funktionen wird das Ersetzen des Benutzerverhaltens zu einer komplexen Aufgabe. Hardcodierte Typen wie grpc :: ServerAsyncResponseWriter können nicht modelliert werden, da sie hmm, hardcodiert und nicht implementiert sind.

Es macht wenig Sinn, sie als Konstruktorargumente zu übergeben. Selbst wenn Sie dies tun, ist dies möglicherweise nicht sinnvoll, da es sich möglicherweise um Abschlussklassen handelt oder einfach keine virtuellen Funktionen haben.

Was machen wir also?

Lösung: Eigenschaften




Anstatt benutzerdefiniertes Verhalten durch Erben von einem generischen Typ einzubetten (wie dies bei der objektorientierten Programmierung der Fall ist), fügen Sie den Typ ein. Wir verwenden dafür Merkmale. Wir sind auf verschiedene Arten auf Merkmale spezialisiert, je nachdem, um welche Art von Code es sich handelt: einen Produktionscode oder einen Unit-Test-Code.

Betrachten Sie CallDataTraits

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

Dies ist die Hauptvorlage für das Merkmal, das für den Produktionscode verwendet wird. Verwenden wir es in einer 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 }; 

Wenn Sie sich den obigen Code ansehen, ist klar, dass der Anwendungscode weiterhin Typen aus dem grpc-Namespace verwendet. Wir können grpc-Typen jedoch leicht durch Dummy-Typen ersetzen. Siehe unten.

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

Mit den Merkmalen konnten wir je nach Situation die in CallData implementierten Typen auswählen. Diese Methode erfordert keine zusätzliche Leistung, da keine unnötigen virtuellen Funktionen zum Hinzufügen von Funktionen erstellt wurden. Diese Technik kann auch in Abschlussklassen angewendet werden.

Wie gefällt dir das Material? Schreiben Sie Kommentare. Und wir sehen uns an der offenen Tür ;-)

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


All Articles