Anwendungscode mit Unit-Tests verbinden

8

Ich arbeite an einem Projekt, in dem wir ein neues Modul implementieren und testen müssen. Ich hatte eine ziemlich klare Architektur im Sinn, also schrieb ich schnell die Hauptklassen und -methoden auf und dann begannen wir, Unit-Tests zu schreiben.

Während des Schreibens der Tests mussten wir einige Änderungen am ursprünglichen Code vornehmen, wie z

  • Private Methoden öffentlich machen, um sie zu testen
  • Hinzufügen zusätzlicher Methoden für den Zugriff auf private Variablen
  • Hinzufügen zusätzlicher Methoden zum Einfügen von Scheinobjekten, die verwendet werden sollten, wenn der Code in einem Komponententest ausgeführt wird.

Irgendwie habe ich das Gefühl, dass dies Symptome sind, dass wir etwas falsch machen, z

  1. Das ursprüngliche Design war falsch (einige Funktionen sollten von Anfang an öffentlich sein).
  2. Der Code wurde nicht richtig für die Schnittstelle zu Unit-Tests entwickelt (möglicherweise aufgrund der Tatsache, dass wir mit dem Design der Unit-Tests begonnen haben, als bereits einige Klassen entworfen wurden).
  3. Wir implementieren Unit-Tests falsch (z. B. sollten Unit-Tests nur die öffentlichen Methoden einer API direkt testen / ansprechen, nicht die privaten).
  4. eine Mischung aus den oben genannten drei Punkten und vielleicht einigen zusätzlichen Themen, über die ich nicht nachgedacht habe.

Da ich einige Erfahrungen mit Unit-Tests habe, aber weit davon entfernt bin, ein Guru zu sein, wäre ich sehr daran interessiert, Ihre Gedanken zu diesen Themen zu lesen.

Neben den oben genannten allgemeinen Fragen habe ich einige spezifischere technische Fragen:

Frage 1. Ist es sinnvoll, eine private Methode m einer Klasse A direkt zu testen und sogar öffentlich zu machen, um sie zu testen? Oder sollte ich annehmen, dass m indirekt durch Unit-Tests getestet wird, die andere öffentliche Methoden abdecken, die m aufrufen?

Frage 2. Wenn eine Instanz der Klasse A eine Instanz der Klasse B enthält (zusammengesetzte Aggregation), ist es sinnvoll, B zu verspotten, um A zu testen? Meine erste Idee war, dass ich B nicht verspotten sollte, weil die B-Instanz Teil der A-Instanz ist, aber dann begann ich daran zu zweifeln. Mein Argument gegen das Verspotten von B ist das gleiche wie für 1: B ist privat für A und wird nur für seine Implementierung verwendet, daher scheint das Verspotten von B so, als würde ich private Details von A wie in (1) offenlegen. Aber vielleicht deuten diese Probleme auf einen Konstruktionsfehler hin: Vielleicht sollten wir keine zusammengesetzte Aggregation verwenden, sondern eine einfache Assoziation von A nach B.

Frage 3. Wenn wir uns im obigen Beispiel dazu entschließen, B zu verspotten, wie injizieren wir die B-Instanz in A? Hier sind einige Ideen, die wir hatten:

  • Fügen Sie die B-Instanz als Argument in den A-Konstruktor ein, anstatt die B-Instanz im A-Konstruktor zu erstellen.
  • Übergeben Sie eine BFactory-Schnittstelle als Argument an den A-Konstruktor und lassen Sie A die Factory verwenden, um seine private B-Instanz zu erstellen.
  • Verwenden Sie einen BFactory-Singleton, der für A privat ist. Verwenden Sie eine statische Methode A :: setBFactory (), um den Singleton festzulegen. Wenn A die B-Instanz erstellen möchte, verwendet es den werkseitigen Singleton, wenn dieser festgelegt ist (das Testszenario), und erstellt B direkt, wenn der Singleton nicht festgelegt ist (das Produktionscode-Szenario).

Die ersten beiden Alternativen scheinen mir sauberer zu sein, aber sie erfordern das Ändern der Signatur des A-Konstruktors: Das Ändern einer API, um sie testbarer zu machen, erscheint mir unangenehm. Ist dies eine gängige Praxis?

Der dritte hat den Vorteil, dass die Signatur des Konstruktors nicht geändert werden muss (die Änderung an der API ist weniger invasiv), aber vor dem Start des Tests die statische Methode setBFactory () aufgerufen werden muss, die IMO-fehleranfällig ist ( implizite Abhängigkeit von einem Methodenaufruf, damit die Tests ordnungsgemäß funktionieren). Ich weiß also nicht, welches wir wählen sollen.

Giorgio
quelle
Ich denke, dass C ++ 's Klassen- / Funktionsfunktion für Freunde von Nutzen sein könnte. Hast du das versucht?
Mert Akcakaya
@Mert: Wir haben es nicht versucht. Frage: Bei Verwendung von friend müssten wir Testcodeklassen als Freund der Hauptcodeklassen deklarieren. Ist das ok? Wir hätten Produktionscode abhängig vom Testcode. Ist das eine gute Idee? Oder war es eine andere Lösung, die Sie sich vorgestellt hatten?
Giorgio
Ich bin kein C ++ - Experte, es kam mir nur als einfache Lösung in den Sinn.
Mert Akcakaya

Antworten:

8

Ich denke, öffentliche Methoden zu testen ist die meiste Zeit genug.

Wenn Ihre privaten Methoden sehr komplex sind, sollten Sie sie als öffentliche Methoden in eine andere Klasse einfügen und als private Aufrufe dieser Methoden in Ihrer ursprünglichen Klasse verwenden. Auf diese Weise können Sie sicherstellen, dass beide Methoden in Ihrer Original- und Dienstprogrammklasse ordnungsgemäß funktionieren.

Bei Designentscheidungen ist es wichtig, sich stark auf private Methoden zu verlassen.

Mert Akcakaya
quelle
Ich stimme Ihnen zu: Wenn Sie das Bedürfnis haben, eine private Methode zu testen, sollte sie möglicherweise gar nicht erst privat sein und in eine separate Dienstprogrammklasse eingeordnet werden, die separat getestet werden sollte.
Giorgio
Das Testen aller öffentlichen Methoden sollte bereits alle privaten Methoden testen (sprich: abdecken ). Sonst, was machen sie dort? :)
Amadeus Hein
1
@Amadeus Heing, Testen öffentlicher Methoden kann nur private Methoden aufrufen, nicht testen.
Mert Akcakaya
2
@Mert Ja, aber im Allgemeinen ist das Testen einer privaten Methode ein Signal dafür, dass etwas anderes im Code nicht stimmt. Weitere Details: Link
Amadeus Hein
1
"Bei Entwurfsentscheidungen ist es wichtig, sich stark auf private Methoden zu verlassen.": In unserem Fall waren die privaten Methoden Dienstprogrammmethoden, die von einer öffentlichen Methode einmal aufgerufen werden. Aber dann haben wir uns gefragt, ob es robuster wäre, sie auch zu testen. Aber wahrscheinlich wäre es, wie viele betonten, sinnvoll, diese Methoden in eine Utility-Klasse zu verschieben und öffentlich zu machen, wenn sie so kritisch sind, dass man sie testen möchte.
Giorgio
5

Zu Frage 1: Das kommt darauf an. In der Regel beginnen Sie mit Komponententests für öffentliche Methoden. Manchmal stoßen Sie auf eine Methode m, die Sie für A privat halten möchten, aber Sie denken, dass es sinnvoll ist, m auch isoliert zu testen. Wenn dies der Fall ist, sollten Sie entweder m öffentlich machen oder die Testklasse zu TestAeiner Freundesklasse machen A. Beachten Sie jedoch, dass das Hinzufügen eines Komponententests zum Testen von m es etwas schwieriger macht, die Signatur oder das Verhalten von m anschließend zu ändern. Wenn Sie dieses "Implementierungsdetail" von A beibehalten möchten, ist es möglicherweise besser, keinen direkten Komponententest hinzuzufügen.

Frage 2: (C ++ - integrierte) zusammengesetzte Aggregation funktioniert nicht gut, wenn es darum geht, eine Instanz zu verspotten. Da die Konstruktion des B implizit im Konstruktor von A erfolgt, haben Sie keine Chance, die Abhängigkeit von außen zu injizieren. Wenn dies ein Problem ist, hängt es von der Art und Weise ab, wie Sie A testen möchten: Wenn Sie der Meinung sind, dass es sinnvoller ist, A isoliert mit einem Mock von B anstelle von B zu testen, verwenden Sie besser eine einfache Zuordnung. Wenn Sie der Meinung sind, dass Sie alle erforderlichen Komponententests für A schreiben können, ohne B zu verspotten, ist ein Composite wahrscheinlich in Ordnung.

Frage 3: Das Ändern einer API, um die Testbarkeit zu verbessern, ist üblich, solange Sie bisher nicht viel Code haben, der sich auf diese API verlässt. Wenn Sie TDD ausführen, müssen Sie Ihre API danach nicht mehr ändern, um die Testbarkeit zu verbessern. Sie beginnen zunächst mit einer API, die auf Testbarkeit ausgelegt ist. Wenn Sie eine API später ändern möchten, um die Testbarkeit zu verbessern, können Probleme auftreten. Daher würde ich mich für die erste oder zweite der von Ihnen beschriebenen Alternativen entscheiden, solange Sie Ihre API problemlos ändern können, und so etwas wie Ihre dritte Alternative (Anmerkung: Dies funktioniert auch ohne das Singleton-Muster) nur als letzten Ausweg verwenden, wenn ich muss Ändern Sie die API unter keinen Umständen.

Über Ihre Bedenken, dass Sie "es falsch machen" könnten: Jeder große Motor oder jede große Maschine hat Wartungsöffnungen, daher ist es meiner Meinung nach nicht allzu überraschend, dass Software so etwas hinzugefügt werden muss.

Doc Brown
quelle
2
+1 Für den letzten Absatz. Für ein gutes Beispiel, wie die Elektronikwelt testet?
Mattnz
+1: Danke für eine sehr motivierte Antwort. Eines meiner Hauptanliegen ist, dass der Anwendungscode dem Anwendungscode Funktionalität bietet, nicht dem Testcode: Der Testcode sollte den Anwendungscode beachten , ohne Anforderungen zu stellen. Natürlich könnten Sie einige Anforderungen haben, um den Code besser sichtbar zu machen, aber diese sollten wirklich minimal sein. Siehe das zusammengesetzte Beispiel: IMO sollte man eine zusammengesetzte einfache Zuordnung basierend auf den Anforderungen der Anwendungsdomäne und nicht auf der Testbarkeit auswählen. Die Anforderungen der IMO-Biegeanwendung an die Testanforderungen sollten der letzte Ausweg sein.
Giorgio
1
@Giorgio: Ihr Missverständnis hier ist, dass die Verwendung einer Zuordnung gegenüber einem Verbundwerkstoff alles mit Domänenanforderungen zu tun hat - Sie können alle Domänenanforderungen mit beiden Arten von Design erfüllen. Es ist nicht zu erwarten, dass Software nur durch minimale Änderungen getestet werden kann. Wenn Sie es richtig machen, wird es definitiv Ihre Software auf der Designebene beeinflussen.
Doc Brown
@Doc Brown: Nun, wenn A <> - B ein Verbund ist, können Instanzen von B nur existieren, wenn sie von Instanzen von A verwaltet werden und ihre Lebensdauer durch die Lebensdauer von A gesteuert wird. Dies kann eine Domänenanforderung sein. Andererseits bedeutet eine einfache "Verwendungs" -Zuordnung A -> B nicht, dass eine A-Instanz eine B-Instanz verwalten sollte. Vielleicht gab es in unserem Fall wirklich einen Fehler in der Analyse der Anwendungsdomäne (wir sollten eine Assoziation anstelle der Komposition verwenden).
Giorgio
@Giorgio: Möglicherweise müssen Instanzen von A und B dieselbe Lebensdauer haben. Aber wie Sie es erfüllen, liegt bei Ihnen. Es gibt keine Domänenanforderung, die Sie dazu zwingt, dieses Problem mithilfe der in C ++ integrierten Kompositionsform zu lösen. Wenn Sie A und B isoliert testen möchten, müssen Sie - zumindest für Ihren Test - eine Instanz von A ohne B haben und umgekehrt. In diesem Fall verwenden Sie besser einen Laufzeitmechanismus, um die Lebensdauer dieser Objekte zu steuern (z. B. mithilfe intelligenter Zeiger), als einen Kompilierungszeitmechanismus.
Doc Brown
1

Sie sollten nach Dependency Injection und Inversion of Control suchen. Misko Hevery erklärt viel davon in seinem Blog. DI und IoC sind effektiv ein Entwurfsmuster für die Erstellung von Code, der einfach zu testen und zu verspotten ist.

Frage 1: Nein, machen Sie keine privaten Methoden öffentlich, um sie zu testen. Wenn die Methode ausreichend komplex ist, können Sie eine Collaborator- Klasse erstellen , die nur diese Methode enthält, und sie in Ihre andere Klasse einfügen (an den Konstruktor übergeben). 1

Frage 2: Wer konstruiert in diesem Fall A? Wenn Sie eine Factory / Builder-Klasse haben, die A erstellt, kann es nicht schaden, den Mitarbeiter B an den Konstruktor zu übergeben. Wenn sich A in einem anderen Paket / Namespace befindet als der Code, der es verwendet, können Sie den Konstruktor sogar paketprivat machen und so festlegen, dass die Factory / der Builder die einzige Klasse ist, die ihn erstellen kann.

Frage 3: Ich habe dies in Frage 2 beantwortet. Einige zusätzliche Hinweise:

  • Die Verwendung eines Builder- / Factory-Musters ermöglicht es Ihnen, die Abhängigkeitsinjektion so oft durchzuführen, wie Sie möchten, ohne sich darum kümmern zu müssen, dass die Verwendung von Code mithilfe Ihrer Klasse schwierig wird.
  • Es trennt die Objektkonstruktionszeit von der Objektnutzungszeit, was bedeutet, dass Code mit Ihrer API einfacher sein kann.

1 Dies ist die C # / Java-Antwort. C ++ verfügt möglicherweise über zusätzliche Funktionen, die dies erleichtern

Als Antwort auf Ihre Kommentare:

Was ich damit meinte war, dass Ihr Produktionscode von geändert werden würde (bitte verzeihen Sie mein Pseudo-C ++):

void MyClass::MyUseOfA()
{
  A* a = new A();
  a->SomeMethod();
}

A::A()
{
  m_b = new B();
}

zu:

void MyClass::MyUseOfA()
{
  A* a = AFactory.NewA();
  a->SomeMethod();
}

A* AFactory::NewA()
{
  // Construct dependencies
  B* b = new B();
  return new A(b);
}

A::A(B* b)
{
  m_b = b;
}

Dann kann Ihr Test sein:

void MyTest::TestA()
{
  MockB* b = new MockB();
  b->SetSomethingInteresting(somethingInteresting);

  A* a = new A(b);

  a->DoSomethingInteresting();

  b->DidSomethingInterestingHappen();
}

Auf diese Weise müssen Sie die Factory nicht weitergeben, der Code, der A aufruft, muss nicht wissen, wie A erstellt wird, und der Test kann A benutzerdefiniert erstellen, damit das Verhalten getestet werden kann.

In Ihrem anderen Kommentar haben Sie nach verschachtelten Abhängigkeiten gefragt. Zum Beispiel, wenn Ihre Abhängigkeiten waren:

A -> C -> D -> B.

Die erste Frage ist, ob A C und D verwendet. Wenn nicht, warum sind sie in A enthalten? Angenommen, sie werden verwendet, ist es möglicherweise erforderlich, C in Ihrer Fabrik zu übergeben und Ihren Test eine MockC erstellen zu lassen, die eine MockB zurückgibt, sodass Sie alle möglichen Interaktionen testen können.

Wenn dies langsam kompliziert wird, kann dies ein Zeichen dafür sein, dass Ihr Design möglicherweise zu eng miteinander verbunden ist. Wenn Sie die Kupplung lösen und die Kohäsion hoch halten können, ist diese Art von DI einfacher zu implementieren.

Bringer128
quelle
Zu Antwort 2: Meinen Sie, dass der Produktionscode und der Testcode zwei verschiedene Factory-Implementierungen verwenden würden, um A zu konstruieren, wobei (1) die Produktionscode-Factory die Produktions-B-Instanz injizieren würde und (2) die Testcode-Factory die Instanz injizieren würde B Scheininstanz. Tatsächlich ist die B-Instanz ziemlich tief in einem Kompositionsbaum verschachtelt. Ich müsste die Fabrik durch mehrere Ebenen des Kompositionsbaums weitergeben. Instanzen von A werden von ihrem übergeordneten Objekt (einer anderen Klasse) konstruiert
Giorgio
In Bezug auf Frage 3 ist eines unserer Probleme, wie die B-Factory in A eingefügt werden soll: Verwenden eines Konstruktorarguments im A-Konstruktor, um einen lokalen Verweis auf die Factory festzulegen, oder als Singleton, auf das A zugreift, wenn es die Factory verwenden muss .
Giorgio
@Giorgio Schauen Sie sich mein Update mit Ihren Kommentaren an. Da ich Ihr spezifisches Beispiel nicht kenne, gelten meine allgemeinen Beispiele möglicherweise nicht, aber dies ist der Ansatz, mit dem ich prüfen würde, ob ich das Testproblem vereinfachen kann.
Bringer128
Vielen Dank für Ihr Beispiel (ich finde den Pseudocode OK). Zwei Beobachtungen: (1) Warum eine Fabrik im Produktionscode und einen einfachen Konstruktor im Testcode verwenden? (2) Die Kompositionshierarchie ist C -> D -> A -> B, und der Benutzer von C muss die MockB-Instanz bereitstellen, die von C in A injiziert werden muss.
Giorgio
(1) Die Fabrik soll den DI-Aspekt vor dem Code verbergen, der A verwendet. Es soll verhindert werden, dass die Verwendung von Code für das Hinzufügen von DI komplizierter wird. Genauer gesagt ermöglicht es die Abstraktion des Abhängigkeitsmanagements von A, B, sogar C und D. (2) Das Beispiel, das Sie geben, ist eigentlich kein Einheitentest an sich. Wenn Sie Methoden nur für C aufrufen, ist es viel schwieriger, eine hohe Testabdeckung für A zu erzielen. Bis zu Ihrer Wichtigkeit, aber ein Komponententest sollte nur A, seine Wechselwirkungen mit B und seine Rückgabewerte testen.
Bringer128