Sollen Integrationstests alle Komponententests wiederholen?

36

Angenommen, ich habe eine Funktion (in Ruby geschrieben, sollte aber für alle verständlich sein):

def am_I_old_enough?(name = 'filip')
   person = Person::API.new(name)
   if person.male?
      return person.age > 21
   else
      return person.age > 18
   end
end

Im Unit-Test würde ich vier Tests erstellen, um alle Szenarien abzudecken. Jeder verwendet ein verspottetes Person::APIObjekt mit den Methoden stubbed male?und age.

Jetzt geht es darum, Integrationstests zu schreiben. Ich gehe davon aus, dass die Person :: API nicht mehr verspottet werden sollte. Ich würde also genau dieselben vier Testfälle erstellen, ohne jedoch das Person :: API-Objekt zu verspotten. Ist das korrekt?

Wenn ja, wozu sollten Unit-Tests überhaupt geschrieben werden, wenn ich nur Integrationstests schreiben könnte, die mir mehr Sicherheit geben (wenn ich an realen Objekten arbeite, nicht an Stubs oder Mocks)?

Filip Bartuzi
quelle
3
Nun, einer der Punkte ist, dass Sie durch Verspotten / Unit-Testen Probleme mit Ihrem Code eingrenzen können. Wenn ein Integrationstest fehlschlägt, wissen Sie nicht, wessen Code fehlerhaft ist, Ihrer oder der API.
Chris Wohlert
9
Nur vier Tests? Sie haben sechs Grenzalter, die Sie testen sollten: 17, 18, 19, 20, 21, 22 ...;)
David Arno
22
@FilipBartuzi, ich nehme an, die Methode prüft, ob ein Mann zum Beispiel über 21 ist. Wie derzeit geschrieben, tut es das nicht, es ist nur wahr, wenn sie 22+ sind. "Over 21" bedeutet auf Englisch "21+". Es gibt also einen Fehler in Ihrem Code. Solche Fehler werden durch Testen von Grenzwerten erfasst, dh 20, 21, 22 für einen Mann, 17, 18, 19 für eine Frau in diesem Fall. Es sind also mindestens sechs Tests erforderlich.
David Arno
6
Ganz zu schweigen von den Fällen 0 und -1. Was bedeutet es für eine Person, -1 Jahre alt zu sein? Was sollte Ihr Code tun, wenn Ihre API etwas Unsinniges zurückgibt?
RubberDuck
9
Dies ist viel einfacher zu testen, wenn Sie ein Personenobjekt als Parameter übergeben.
JeffO

Antworten:

72

Nein, Integrationstests sollten nicht nur die Abdeckung von Unit-Tests duplizieren. Sie können etwas Deckung duplizieren, aber das ist nicht der Punkt.

Der Zweck eines Komponententests besteht darin, sicherzustellen, dass ein bestimmtes kleines Stück Funktionalität genau und vollständig wie beabsichtigt funktioniert. Ein Einheitentest für am_i_old_enoughwürde Daten mit unterschiedlichem Alter testen, mit Sicherheit diejenigen, die sich nahe der Schwelle befinden, möglicherweise alle auftretenden menschlichen Altersgruppen. Nachdem Sie diesen Test geschrieben haben, sollte die Integrität von am_i_old_enoughnie wieder in Frage gestellt werden.

Der Zweck eines Integrationstests besteht darin, zu überprüfen, ob das gesamte System oder eine Kombination einer beträchtlichen Anzahl von Komponenten bei gemeinsamer Verwendung das Richtige tut . Der Kunde kümmert sich nicht um eine bestimmte von Ihnen geschriebene Utility-Funktion, er kümmert sich darum, dass seine Web-App ordnungsgemäß gegen den Zugriff Minderjähriger geschützt ist, da sonst die Aufsichtsbehörden ihre Ärsche haben.

Das Überprüfen des Alters des Benutzers ist ein kleiner Teil dieser Funktionalität, aber der Integrationstest überprüft nicht, ob Ihre Dienstprogrammfunktion den richtigen Schwellenwert verwendet. Es wird geprüft, ob der Anrufer auf der Grundlage dieses Schwellenwerts die richtige Entscheidung trifft, ob die Dienstprogrammfunktion überhaupt aufgerufen wird, ob andere Bedingungen für den Zugriff erfüllt sind usw.

Der Grund, warum wir beide Arten von Tests benötigen, ist, dass es eine kombinatorische Explosion möglicher Szenarien für den Pfad durch eine Codebasis gibt, den die Ausführung möglicherweise benötigt. Wenn die Utility-Funktion über 100 mögliche Eingaben verfügt und es Hunderte von Utility-Funktionen gibt, sind viele, viele Millionen Testfälle erforderlich, um zu überprüfen, ob in allen Fällen das Richtige passiert . Durch einfaches Überprüfen aller Fälle in sehr kleinen Bereichen und anschließendes Überprüfen gemeinsamer, relevanter oder wahrscheinlicher Kombinationen dieser Bereiche, wobei vorausgesetzt wird, dass diese kleinen Bereiche bereits korrekt sind, wie durch Komponententests belegt , können wir eine ziemlich sichere Einschätzung erhalten, dass das System dies tut was es soll, ohne in alternativen szenarien zu ertrinken zu testen.

Kilian Foth
quelle
6
"Wir können ziemlich sicher einschätzen, dass das System das tut, was es sollte, ohne in alternativen Szenarien zu ertrinken." Vielen Dank. Ich liebe es, wenn sich jemand vernünftig automatisierten Tests nähert.
jpmc26
1
JB Rainsberger hat einen schönen Vortrag über Tests und die kombinatorische Explosion, über die Sie im letzten Absatz schreiben, genannt "Integrierte Tests sind ein Betrug" . Es geht nicht so sehr um Integrationstests, aber trotzdem sehr interessant.
Bart van Nierop
The customer doesn't care about a particular utility function you wrote, they care that their web app is properly secured against access by minors-> Das ist sehr klug, danke! Das Problem ist, wenn Sie für sich selbst projektieren. Es ist schwer, seine Einstellung zwischen Programmierer und Produktmanager zu
trennen
14

Die kurze Antwort lautet "Nein". Der interessantere Teil ist, warum / wie diese Situation entstehen könnte.

Ich denke, die Verwirrung entsteht, weil Sie versuchen, strenge Testpraktiken (Komponententests gegen Integrationstests, Verspotten usw.) für Code einzuhalten, der anscheinend nicht strengen Praktiken entspricht.

Das heißt nicht, dass der Code "falsch" ist oder dass bestimmte Praktiken besser sind als andere. Nur, dass einige der Annahmen der Testpraktiken in dieser Situation möglicherweise nicht zutreffen und es hilfreich sein kann, bei Codierungs- und Testpraktiken ein ähnliches Maß an "Strenge" zu verwenden. oder zumindest anzuerkennen, dass sie möglicherweise nicht ausgeglichen sind, was dazu führt, dass einige Aspekte nicht zutreffen oder überflüssig werden.

Der offensichtlichste Grund ist, dass Ihre Funktion zwei verschiedene Aufgaben ausführt:

  • Nachschlagen Personanhand ihres Namens. Dies erfordert Integrationstests, um sicherzustellen, dass es gefunden werden kannPerson Objekte gefunden werden die vermutlich an anderer Stelle erstellt / gespeichert wurden.
  • Berechnung, ob a Personalt genug ist, basierend auf ihrem Geschlecht. Dies erfordert einen Komponententest, um sicherzustellen, dass die Berechnung wie erwartet ausgeführt wird.

Wenn Sie diese Tasks zu einem Codeblock zusammenfassen, können Sie keinen ohne den anderen ausführen. Wenn Sie die Berechnungen in einer Einheit testen möchten, müssen Sie nach a suchen Person(entweder aus einer realen Datenbank oder aus einem Stub / Mock). Wenn Sie testen möchten, ob die Suche in den Rest des Systems integriert ist, müssen Sie auch eine Altersberechnung durchführen. Was sollen wir mit dieser Berechnung machen? Sollten wir es ignorieren oder überprüfen? Das scheint genau das Problem zu sein, das Sie in Ihrer Frage beschreiben.

Wenn wir uns eine Alternative vorstellen, können wir die Berechnung selbst durchführen:

def is_old_enough?(person)
   if person.male?
      return person.age > 21
   else 
      return person.age > 18
   end
end

Da es sich um eine reine Berechnung handelt, müssen keine Integrationstests durchgeführt werden.

Wir könnten auch versucht sein, die Suchaufgabe separat zu schreiben:

def person_from_name(name = 'filip')
   return Person::API.new(name)
end

In diesem Fall ist die Funktionalität jedoch so Person::API.newähnlich , dass ich sagen würde, dass Sie stattdessen diese verwenden sollten (wenn der Standardname erforderlich ist, sollte er besser an einer anderen Stelle gespeichert werden, z. B. als Klassenattribut).

Wenn Sie Integrationstests für Person::API.new(oder person_from_name) alles schreiben, worüber Sie sich Gedanken machen müssen, ist, ob Sie die erwarteten Ergebnisse zurückerhalten Person. Alle altersbezogenen Berechnungen werden an anderer Stelle durchgeführt, sodass Ihre Integrationstests sie ignorieren können.

Warbo
quelle
11

Ein weiterer Punkt, den ich zu Killians Antwort hinzufügen möchte, ist, dass Komponententests sehr schnell ablaufen, sodass wir Tausende davon haben können. Ein Integrationstest dauert in der Regel länger, da er Webservices, Datenbanken oder andere externe Abhängigkeiten aufruft. Daher können wir für Integrationsszenarien nicht dieselben Tests (1000s) ausführen, da diese zu lange dauern würden.

Außerdem werden Komponententests normalerweise zur Erstellungszeit (auf dem Erstellungscomputer) ausgeführt, und Integrationstests werden nach der Bereitstellung auf einer Umgebung / einem Computer ausgeführt.

In der Regel werden für jeden Build Tausende von Komponententests ausgeführt und anschließend nach jeder Bereitstellung etwa 100 Integrationsprüfungen mit hohem Wert. Wir nehmen möglicherweise nicht jeden Build für die Bereitstellung auf, aber das ist in Ordnung, da für den Build, den wir für die Bereitstellung benötigen, die Integrationstests ausgeführt werden. In der Regel möchten wir diese Tests auf 10 oder 15 Minuten beschränken, da wir die Bereitstellung nicht zu lange aufhalten möchten.

Zusätzlich können wir wöchentlich eine Reihe von Regressionstests durchführen, die mehr Szenarien am Wochenende oder andere Ausfallzeiten abdecken. Diese können länger als 15 Minuten dauern, da weitere Szenarien behandelt werden. In der Regel arbeitet jedoch niemand an Sa / So, sodass wir uns mehr Zeit für die Tests nehmen können.

Jon Raynor
quelle
gilt nicht für dynamische Sprachen (dh ohne Build-Phase)
Filip Bartuzi