Ich bin neu im Unit-Test und höre ständig die Worte "Scheinobjekte", die viel herumgeworfen werden. Kann jemand in Laienbegriffen erklären, was Scheinobjekte sind und wofür sie normalerweise beim Schreiben von Komponententests verwendet werden?
Ich bin neu im Unit-Test und höre ständig die Worte "Scheinobjekte", die viel herumgeworfen werden. Kann jemand in Laienbegriffen erklären, was Scheinobjekte sind und wofür sie normalerweise beim Schreiben von Komponententests verwendet werden?
Antworten:
Da Sie sagen, dass Sie mit Unit-Tests noch nicht vertraut sind und nach Scheinobjekten in "Laienbegriffen" gefragt haben, werde ich das Beispiel eines Laien ausprobieren.
Stellen Sie sich Unit-Tests für dieses System vor:
cook <- waiter <- customer
Es ist im Allgemeinen leicht vorstellbar, eine Low-Level-Komponente wie die cook
folgende zu testen :
cook <- test driver
Der Testfahrer bestellt einfach verschiedene Gerichte und überprüft, ob der Koch für jede Bestellung das richtige Gericht zurückgibt.
Es ist schwieriger, eine mittlere Komponente wie den Kellner zu testen, die das Verhalten anderer Komponenten nutzt. Ein naiver Tester könnte die Kellnerkomponente genauso testen, wie wir die Kochkomponente getestet haben:
cook <- waiter <- test driver
Der Testfahrer würde verschiedene Gerichte bestellen und sicherstellen, dass der Kellner das richtige Gericht zurückgibt. Leider bedeutet dies, dass dieser Test der Kellnerkomponente vom korrekten Verhalten der Kochkomponente abhängen kann. Diese Abhängigkeit ist noch schlimmer, wenn die Kochkomponente testunfreundliche Eigenschaften aufweist, wie nicht deterministisches Verhalten (das Menü enthält die Überraschung des Küchenchefs als Gericht), viele Abhängigkeiten (der Koch kocht nicht ohne sein gesamtes Personal) oder viele Ressourcen (einige Gerichte erfordern teure Zutaten oder brauchen eine Stunde zum Kochen).
Da dies im Idealfall ein Kellner-Test ist, möchten wir nur den Kellner testen, nicht den Koch. Insbesondere möchten wir sicherstellen, dass der Kellner die Bestellung des Kunden korrekt an den Koch übermittelt und das Essen des Kochs korrekt an den Kunden liefert.
Unit-Test bedeutet, Einheiten unabhängig zu testen. Ein besserer Ansatz wäre es, die zu testende Komponente (den Kellner) mithilfe von Test-Doubles (Dummies, Stubs, Fakes, Mocks) zu isolieren, die Fowler nennt .
-----------------------
| |
v |
test cook <- waiter <- test driver
Hier ist der Testkoch mit dem Testfahrer "in cahoots". Idealerweise ist das zu testende System so ausgelegt, dass der Testkoch leicht ausgetauscht ( eingespritzt ) werden kann, um mit dem Kellner zu arbeiten, ohne den Produktionscode zu ändern (z. B. ohne den Kellnercode zu ändern).
Jetzt kann der Testkoch (Testdoppel) auf verschiedene Arten implementiert werden:
In Fowlers Artikel finden Sie weitere Einzelheiten zu Fakes vs Stubs vs Mocks vs Dummies. Konzentrieren wir uns jedoch zunächst auf einen Scheinkoch.
-----------------------
| |
v |
mock cook <- waiter <- test driver
Ein großer Teil des Unit-Tests der Kellnerkomponente konzentriert sich darauf, wie der Kellner mit der Kochkomponente interagiert. Ein scheinbasierter Ansatz konzentriert sich darauf, die richtige Interaktion vollständig zu spezifizieren und zu erkennen, wann sie schief geht.
Das Scheinobjekt weiß im Voraus, was während des Tests geschehen soll (z. B. welche seiner Methodenaufrufe aufgerufen werden usw.), und das Scheinobjekt weiß, wie es reagieren soll (z. B. welcher Rückgabewert bereitzustellen ist). Der Schein zeigt an, ob sich das, was wirklich passiert, von dem unterscheidet, was passieren soll. Ein benutzerdefiniertes Mock-Objekt könnte für jeden Testfall von Grund auf neu erstellt werden, um das erwartete Verhalten für diesen Testfall auszuführen. Ein Mocking-Framework ist jedoch bestrebt, eine solche Verhaltensspezifikation direkt im Testfall klar und einfach anzuzeigen.
Das Gespräch um einen scheinbasierten Test könnte folgendermaßen aussehen:
Testfahrer zum Verspotten des Kochs : Erwarten Sie eine Hot-Dog-Bestellung und geben Sie ihm diesen Dummy-Hot-Dog als Antwort
Testfahrer ( die sich als Kunde) zum Kellner : Ich würde einen Hotdog mögen bitte
Kellner verspotten kochen : 1 Hotdog bitte
Mock Koch zu Kellner : Reihenfolge: 1 Hot Dog bereit (gibt Dummy - Hotdog zu Kellner)
Kellner zu Testfahrer : Hier ist Ihr Hot Dog (gibt dem Testfahrer einen Dummy-Hot Dog)Testfahrer : TEST ERFOLGREICH!
Aber da unser Kellner neu ist, könnte Folgendes passieren:
Testfahrer zum Verspotten des Kochs : Erwarten Sie eine Hot-Dog-Bestellung und geben Sie ihm diesen Dummy-Hot-Dog als Antwort
Testfahrer (posiert als Kunde) zum Kellner : Ich möchte einen Hot Dog, bitte
Kellner zum Verspotten des Kochs : 1 Hamburger, bitte zum
Verspotten des Kochs stoppt den Test: Mir wurde gesagt, dass ich eine Hot Dog-Bestellung erwarten soll!Testfahrer stellt das Problem fest: TEST FEHLGESCHLAGEN! - Der Kellner hat die Bestellung geändert
oder
Testfahrer zum Verspotten des Kochs : Erwarten Sie eine Hot-Dog-Bestellung und geben Sie ihm diesen Dummy-Hot-Dog als Antwort
Testfahrer ( die sich als Kunde) zum Kellner : Ich würde einen Hotdog mögen bitte
Kellner verspotten kochen : 1 Hotdog bitte
Mock Koch zu Kellner : Reihenfolge: 1 Hot Dog bereit (gibt Dummy - Hotdog zu Kellner)
Kellner zu Testfahrer : Hier sind Ihre Pommes Frites (gibt Pommes Frites aus einer anderen Reihenfolge an den Testfahrer)Testfahrer bemerkt die unerwarteten Pommes Frites: TEST FEHLGESCHLAGEN! Der Kellner gab falsches Gericht zurück
Es mag schwierig sein, den Unterschied zwischen Scheinobjekten und Stubs ohne ein kontrastierendes stubbasiertes Beispiel klar zu erkennen, aber diese Antwort ist schon viel zu lang :-)
Beachten Sie auch, dass dies ein ziemlich vereinfachtes Beispiel ist und dass Mocking-Frameworks einige ziemlich ausgefeilte Spezifikationen des erwarteten Verhaltens von Komponenten ermöglichen, um umfassende Tests zu unterstützen. Für weitere Informationen gibt es viel Material zu Scheinobjekten und Verspottungs-Frameworks.
Ein Scheinobjekt ist ein Objekt, das ein reales Objekt ersetzt. Bei der objektorientierten Programmierung sind Scheinobjekte simulierte Objekte, die das Verhalten realer Objekte auf kontrollierte Weise nachahmen.
Ein Computerprogrammierer erstellt normalerweise ein Scheinobjekt, um das Verhalten eines anderen Objekts zu testen, ähnlich wie ein Autodesigner einen Crashtest-Dummy verwendet, um das dynamische Verhalten eines Menschen bei Fahrzeugaufprallen zu simulieren.
http://en.wikipedia.org/wiki/Mock_object
Mock-Objekte ermöglichen es Ihnen, Testszenarien einzurichten, ohne große, unhandliche Ressourcen wie Datenbanken zu nutzen. Anstatt eine Datenbank zum Testen aufzurufen, können Sie Ihre Datenbank mithilfe eines Scheinobjekts in Ihren Komponententests simulieren. Dies befreit Sie von der Last, eine echte Datenbank einrichten und abbauen zu müssen, nur um eine einzelne Methode in Ihrer Klasse zu testen.
Das Wort "Mock" wird manchmal fälschlicherweise synonym mit "Stub" verwendet. Die Unterschiede zwischen den beiden Wörtern werden hier beschrieben. Im Wesentlichen ist ein Mock ein Stub-Objekt, das auch die Erwartungen (dh "Behauptungen") an das ordnungsgemäße Verhalten des zu testenden Objekts / der zu testenden Methode enthält.
Beispielsweise:
class OrderInteractionTester...
public void testOrderSendsMailIfUnfilled() {
Order order = new Order(TALISKER, 51);
Mock warehouse = mock(Warehouse.class);
Mock mailer = mock(MailService.class);
order.setMailer((MailService) mailer.proxy());
mailer.expects(once()).method("send");
warehouse.expects(once()).method("hasInventory")
.withAnyArguments()
.will(returnValue(false));
order.fill((Warehouse) warehouse.proxy());
}
}
Beachten Sie, dass die Objekte warehouse
und mailer
mock mit den erwarteten Ergebnissen programmiert werden.
Scheinobjekte sind simulierte Objekte, die das Verhalten der realen Objekte nachahmen. Normalerweise schreiben Sie ein Scheinobjekt, wenn:
Ein Mock-Objekt ist eine Art Test-Double . Sie verwenden Mockobjects, um das Protokoll / die Interaktion der zu testenden Klasse mit anderen Klassen zu testen und zu überprüfen.
In der Regel werden Sie die Erwartungen "programmieren" oder "aufzeichnen": Methodenaufrufe, die Ihre Klasse für ein zugrunde liegendes Objekt ausführen soll.
Angenommen, wir testen eine Dienstmethode, um ein Feld in einem Widget zu aktualisieren. Und dass es in Ihrer Architektur ein WidgetDAO gibt, das sich mit der Datenbank befasst. Das Gespräch mit der Datenbank ist langsam und das Einrichten und anschließende Bereinigen ist kompliziert. Daher werden wir das WidgetDao verspotten.
Lassen Sie uns überlegen, was der Dienst tun muss: Er sollte ein Widget aus der Datenbank abrufen, etwas damit tun und es erneut speichern.
In einer Pseudosprache mit einer Pseudo-Mock-Bibliothek hätten wir also so etwas wie:
Widget sampleWidget = new Widget();
WidgetDao mock = createMock(WidgetDao.class);
WidgetService svc = new WidgetService(mock);
// record expected calls on the dao
expect(mock.getById(id)).andReturn(sampleWidget);
expect(mock.save(sampleWidget);
// turn the dao in replay mode
replay(mock);
svc.updateWidgetPrice(id,newPrice);
verify(mock); // verify the expected calls were made
assertEquals(newPrice,sampleWidget.getPrice());
Auf diese Weise können wir die Entwicklung von Klassen, die von anderen Klassen abhängen, problemlos testen.
Ich empfehle einen großartigen Artikel von Martin Fowler, in dem erklärt wird, was genau Mocks sind und wie sie sich von Stubs unterscheiden.
Wenn Sie einen Teil eines Computerprogramms testen, möchten Sie im Idealfall nur das Verhalten dieses bestimmten Teils testen.
Schauen Sie sich zum Beispiel den folgenden Pseudocode aus einem imaginären Teil eines Programms an, das ein anderes Programm verwendet, um etwas aufzurufen:
If theUserIsFred then
Call Printer(HelloFred)
Else
Call Printer(YouAreNotFred)
End
Wenn Sie dies testen, möchten Sie hauptsächlich den Teil testen, der untersucht, ob der Benutzer Fred ist oder nicht. Sie wollen den Printer
Teil der Dinge nicht wirklich testen . Das wäre ein weiterer Test.
Hier kommen Scheinobjekte ins Spiel. Sie geben vor, andere Arten von Dingen zu sein. In diesem Fall würden Sie einen Mock verwenden, Printer
damit er sich wie ein echter Drucker verhält, aber keine unbequemen Dinge wie das Drucken ausführt.
Es gibt verschiedene andere Arten von vorgetäuschten Objekten, die Sie verwenden können und die keine Mocks sind. Die Hauptsache, die Mocks Mocks ausmacht, ist, dass sie mit Verhalten und Erwartungen konfiguriert werden können.
Durch die Erwartungen kann Ihr Mock einen Fehler auslösen, wenn er falsch verwendet wird. Im obigen Beispiel möchten Sie möglicherweise sicherstellen, dass der Drucker im Testfall "Benutzer ist Fred" mit HelloFred aufgerufen wird. Wenn das nicht passiert, kann Ihr Mock Sie warnen.
Verhalten in Mocks bedeutet, dass beispielsweise Ihr Code Folgendes getan hat:
If Call Printer(HelloFred) Returned SaidHello Then
Do Something
End
Jetzt möchten Sie testen, was Ihr Code tut, wenn der Drucker aufgerufen wird und SaidHello zurückgibt, damit Sie den Mock so einrichten können, dass SaidHello zurückgegeben wird, wenn er mit HelloFred aufgerufen wird.
Eine gute Ressource ist Martin Fowlers Post Mocks Aren't Stubs
Mock- und Stub-Objekte sind ein wesentlicher Bestandteil der Unit-Tests. Tatsächlich tragen sie wesentlich dazu bei, dass Sie Einheiten und nicht Gruppen von Einheiten testen .
Auf den Punkt gebracht, verwenden Sie Stubs die brechen SUT (System Under Test) die Abhängigkeit von anderen Objekten und spottet , das zu tun und sicherstellen , dass SUT bestimmte Methoden / Eigenschaften auf die Abhängigkeit genannt. Dies geht auf die Grundprinzipien des Komponententests zurück - dass die Tests leicht lesbar und schnell sein sollten und keine Konfiguration erfordern, was die Verwendung aller realen Klassen implizieren könnte.
Im Allgemeinen können Sie mehr als einen Stub in Ihrem Test haben, aber Sie sollten nur einen Mock haben. Dies liegt daran, dass Mock das Verhalten überprüfen soll und Ihr Test nur eines testen sollte.
Einfaches Szenario mit C # und Moq:
public interface IInput {
object Read();
}
public interface IOutput {
void Write(object data);
}
class SUT {
IInput input;
IOutput output;
public SUT (IInput input, IOutput output) {
this.input = input;
this.output = output;
}
void ReadAndWrite() {
var data = input.Read();
output.Write(data);
}
}
[TestMethod]
public void ReadAndWriteShouldWriteSameObjectAsRead() {
//we want to verify that SUT writes to the output interface
//input is a stub, since we don't record any expectations
Mock<IInput> input = new Mock<IInput>();
//output is a mock, because we want to verify some behavior on it.
Mock<IOutput> output = new Mock<IOutput>();
var data = new object();
input.Setup(i=>i.Read()).Returns(data);
var sut = new SUT(input.Object, output.Object);
//calling verify on a mock object makes the object a mock, with respect to method being verified.
output.Verify(o=>o.Write(data));
}
Im obigen Beispiel habe ich Moq verwendet, um Stubs und Mocks zu demonstrieren. Moq verwendet für beide dieselbe Klasse - Mock<T>
was es etwas verwirrend macht. Unabhängig davon schlägt der Test zur Laufzeit fehl, wenn er output.Write
nicht mit data as parameter
aufgerufen input.Read()
wird , während ein fehlgeschlagener Aufruf nicht fehlschlägt.
Wie eine andere Antwort über einen Link zu " Mocks Aren't Stubs " vorschlägt, sind Mocks eine Form von "Test Double", die anstelle eines realen Objekts verwendet werden kann. Was sie von anderen Formen von Test-Doubles wie Stub-Objekten unterscheidet, ist, dass andere Test-Doubles eine Zustandsüberprüfung (und optional eine Simulation) bieten, während Mocks eine Verhaltensüberprüfung (und optional eine Simulation) bieten.
Mit einem Stub können Sie mehrere Methoden für den Stub in beliebiger Reihenfolge (oder sogar wiederholt) aufrufen und den Erfolg bestimmen, wenn der Stub einen von Ihnen beabsichtigten Wert oder Status erfasst hat. Im Gegensatz dazu erwartet ein Scheinobjekt, dass sehr bestimmte Funktionen in einer bestimmten Reihenfolge und sogar eine bestimmte Anzahl von Malen aufgerufen werden. Der Test mit einem Scheinobjekt wird einfach deshalb als "fehlgeschlagen" betrachtet, weil die Methoden in einer anderen Reihenfolge oder Anzahl aufgerufen wurden - selbst wenn das Scheinobjekt zum Zeitpunkt des Testabschlusses den richtigen Status hatte!
Auf diese Weise werden Scheinobjekte häufig als enger an den SUT-Code gekoppelt betrachtet als Stub-Objekte. Das kann gut oder schlecht sein, je nachdem, was Sie überprüfen möchten.
Ein Teil der Verwendung von Scheinobjekten besteht darin, dass sie nicht wirklich gemäß den Spezifikationen implementiert werden müssen. Sie können nur Dummy-Antworten geben. Wenn Sie beispielsweise die Komponenten A und B implementieren müssen und beide miteinander "aufrufen" (interagieren), können Sie A erst testen, wenn B implementiert ist, und umgekehrt. In der testgetriebenen Entwicklung ist dies ein Problem. Sie erstellen also Scheinobjekte ("Dummy") für A und B, die sehr einfach sind, aber bei der Interaktion eine Art Antwort geben . Auf diese Weise können Sie A mithilfe eines Scheinobjekts für B implementieren und testen.
Für PHP und PHPUNIT wird in PHPUNIT Documentaion gut erklärt. siehe hier phpunit dokumentation
In einfachen Worten ist das Mocking-Objekt nur ein Dummy-Objekt Ihres ursprünglichen Objekts und gibt seinen Rückgabewert zurück. Dieser Rückgabewert kann in der Testklasse verwendet werden
Dies ist eine der Hauptperspektiven von Unit-Tests. Ja, Sie versuchen, Ihre einzelne Codeeinheit zu testen, und Ihre Testergebnisse sollten für das Verhalten anderer Beans oder Objekte nicht relevant sein. Sie sollten sie daher verspotten, indem Sie Mock-Objekte mit einer vereinfachten entsprechenden Antwort verwenden.