Frage Wie ist es möglich Unit-Test vor dem Schreiben des Quellcodes zu schreiben?


Da es sich bei dem Komponententest um einen White-Box-Test handelt, wird davon ausgegangen, dass Sie alle Fälle, in denen Ihr Code behandelt werden muss, alle Client-Objekte (das Mock-Objekt im Test) und die richtige Reihenfolge im Voraus kennen müssen Client-Objekte müssen im Code erscheinen (da der Unit-Test das Aufrufen von Mock-Objekten berücksichtigt). Mit anderen Worten, Sie müssen den genauen Algorithmus Ihres Codes genau kennen. Bevor Sie den Algorithmus Ihres Codes genau kennen können, müssen Sie ihn zuerst schreiben!

Aus meiner Sicht sehe ich nicht, wie es möglich ist, den richtigen Komponententest vor dem Schreiben des Quellcodes zu schreiben. Dennoch ist es möglich, zuerst Funktionstests zu schreiben, da Funktionstests eine Art Benutzeranforderung sind. Dein Tipp? Mit bestem Gruß

EIN BEISPIEL FÜR DIESE FRAGE IST:
Wie schreibe ich Testcode vor dem Schreiben des Quellcodes, wenn sie Objektabhängigkeiten sind?


6
2018-01-26 14:09


Ursprung


Antworten:


Mit anderen Worten, Sie müssen den detaillierten Algorithmus Ihres Codes genau kennen.

Nicht ganz. Sie müssen genau das Detail wissen Verhalten von Ihrem Code, wie von außerhalb des Codes selbst beobachtet. Der Algorithmus, der dieses Verhalten erreicht, oder eine Kombination von Algorithmen oder beliebige Ebenen von Abstraktion / Verschachtelung / Berechnungen / etc. sind für die Tests unerheblich. Die Tests sorgen nur dafür, dass das gewünschte Ergebnis erzielt wird.

Der Wert der Tests ist dann, dass sie die Spezifikationen sind, wie sich der Code verhalten soll. Der Code kann also beliebig geändert werden, solange er noch mit den Tests verglichen werden kann. Sie können die Leistung verbessern, die Lesbarkeit und die Unterstützbarkeit verbessern usw. Die Tests stellen sicher, dass das Verhalten unverändert bleibt.

Angenommen, ich möchte eine Funktion schreiben, die zwei Zahlen addiert. Sie wissen vielleicht in Ihrem Kopf, wie Sie es umsetzen werden, aber stellen Sie dieses Wissen für einen Moment beiseite. Sie implementieren es noch nicht. Zuerst implementieren Sie den Test ...

public void CanAddIntegers()
{
    var addend = 1;
    var augend = 1;
    var result = MyMathObject.Add(addend, augend);
    Assert.AreEqual(2, result);
}

Jetzt, wo Sie einen Test haben, können Sie die Methode implementieren ...

public int Add(int addend, int augend)
{
    return ((addend * 2) + (augend * 2)) / 2;
}

Whoa. Moment mal ... Warum um alles in der Welt habe ich das so umgesetzt? Nun, aus der Perspektive des Tests, wen interessiert das? Es geht vorbei. Die Implementierung entspricht den Anforderungen. Und jetzt, wo ich einen Test habe, kann ich den Code sicher umgestalten ...

public int Add(int addend, int augend)
{
    return addend + augend;
}

Das ist ein bisschen vernünftiger. Und der Test besteht noch immer. In der Tat kann ich den Code weiter reduzieren ...

public int Add(int addend, int augend)
{
    return 2;
}

Erraten Sie, was? Der Test besteht noch immer. Es ist der einzige Test, den wir haben, es ist die einzige gegebene Spezifikation, also funktioniert der Code. Daher müssen wir die Tests verbessern, um mehr Fälle abzudecken. Das Schreiben weiterer Tests gibt uns die Spezifikationen, die wir benötigen, um mehr Code zu schreiben.

In der Tat, diese letzte Implementierung sollte waren die ersten, nach die dritte Regel von TDD:

Sie dürfen keinen Produktionscode mehr schreiben, der ausreicht, um den einen fehlgeschlagenen Komponententest zu bestehen.

In einer reinen Uncle-Bob-getriebenen TDD-Welt hätten wir diese letzte Implementierung zuerst geschrieben, dann weitere Tests geschrieben und den Code schrittweise verbessert.

Dies ist bekannt als die Rot, Grün, Refactor Zyklus. Es ist sehr gut illustriert in einem einfachen, etwas weniger konstruierten Beispiel, dem Bowling Spiel. Der Zweck dieser Übung besteht darin, diesen Zyklus zu üben:

  1. Schreiben Sie zuerst einen Test, der ein bestimmtes Verhalten erwartet. Dies ist das rot Teil des Zyklus, weil der Test ohne das Verhalten an Ort und Stelle ausfallen wird.
  2. Als nächstes schreiben Sie Code, um dieses Verhalten zu zeigen. Dies ist das Grün Teil des Zyklus, weil sein Zweck ist, den Test zu bestehen. Und nur um den Test zu bestehen.
  3. Zum Schluss den Code umgestalten und verbessern. Dies ist natürlich die Refaktor Teil des Zyklus.

Wo du feststeckst ist, dass du ständig in der. Bist Refaktor Teil des Zyklus. Sie denken bereits darüber nach, wie Sie den Code verbessern können. Welcher Algorithmus wäre richtig, wie kann er optimiert werden? sollte geschrieben sein. Zu diesem Zweck ist TDD eine Übung in die Geduld. Schreib nicht den besten Code ... noch.

  1. Bestimmen Sie zuerst, was der Code ist sollte tun, und nichts mehr.
  2. Als nächstes schreibe Code, der tut es und nichts mehr.
  3. Schließlich verbessere diesen Code und mache ihn besser.

AKTUALISIEREN

Ich stieß auf etwas, das mich an diese Frage erinnerte, und mir fiel ein Zufall ein. Vielleicht habe ich die Umstände, nach denen du fragst, falsch ausgelegt. Wie verwalten Sie Ihre Abhängigkeiten? Das heißt, welche Art von Abhängigkeitsinjektionsmethodik verwenden Sie? Es hört sich so an, als ob dies die Wurzel des hier diskutierten Problems sein könnte.

So lange ich mich erinnern kann, habe ich so etwas benutzt Gemeinsamer Service Locator (oder, häufiger, selbst entwickelte Implementierungen desselben Konzepts). Und dabei tendiere ich zu einer sehr spezifischen Art der Abhängigkeitsinjektion. Es klingt wie du einen anderen Stil verwendest. Konstruktorinjektion, vielleicht? Ich nehme die Konstruktorinjektion für diese Antwort an.

Sagen wir also, wenn Sie das angeben MyMathObject hat Abhängigkeiten auf MyOtherClass1 und MyOtherClass2. Mithilfe der Konstruktorinjektion wird der Footprint von MyMathObject sieht aus wie das:

public class MyMathObject
{
    public MyMathObject(MyOtherClass1 firstDependency, MyOtherClass2 secondDependency)
    {
        // implementation details
    }

    public int Add(int addend, int augend)
    {
        // implementation details
    }
}

Und so, wie Sie angeben, müssen die Tests die Abhängigkeiten oder Mocks liefern. In der Grundfläche der Klasse gibt es keinen Hinweis auf die tatsächliche Verwendung von MyOtherClass1 oder MyOtherClass2, aber es gibt Hinweise auf die brauchen für Sie. Als Abhängigkeiten werden sie vom Konstrukteur lautstark beworben.

Das wirft die Frage auf, die Sie gestellt haben ... Wie kann man zuerst die Tests schreiben, wenn man das Objekt noch nicht implementiert hat? Auch hier gibt es keinen Hinweis auf die tatsächliche Verwendung nur in der nach außen gerichteten Gestaltung des Objekts. Die Abhängigkeit ist also ein Implementierungsdetail, das bekannt sein muss.

Ansonsten schreibst du das zuerst:

public class MyMathObject
{
    public int Add(int addend, int augend)
    {
        // implementation details
    }
}

Dann würden Sie Ihre Tests dafür schreiben, dann würden Sie es implementieren und die Abhängigkeiten entdecken, dann würden Sie umschreiben Ihre Tests dafür. Darin liegt das Problem.

Das Problem, das Sie gefunden haben, ist jedoch kein Problem der Tests oder der Test Driven Development. Das Problem liegt tatsächlich im Design des Objekts. Trotz der Tatsache dass // implementation details wurden verglast, es gibt immer noch ein Implementierungsdetail, das entkommt. Da ist ein undichte Abstraktion:

public class MyMathObject
{
    public MyMathObject(MyOtherClass1 firstDependency, MyOtherClass2 secondDependency)
    {                   ^---Right here                 ^---And here

        // implementation details
    }

    public int Add(int addend, int augend)
    {
        // implementation details
    }
}

Das Objekt verkapselt und abstrahiert seine Implementierungsdetails nicht ausreichend. Es versucht, und die Verwendung von Dependency-Injektion ist ein wichtiger Schritt dazu. Aber es ist noch nicht vollständig da. Dies liegt daran, dass die Abhängigkeiten, die Implementierungsdetails sind, sind von außen sichtbar und extern bekannt durch andere Objekte. (In diesem Fall das Testobjekt.) Also, um die Abhängigkeiten zu erfüllen und zu machen MyMathObject Arbeit müssen externe Objekte über ihre Implementierungsdetails wissen. Sie alle tun es. Das Testobjekt, alle Produktionscodeobjekte, die es verwenden, alles, was darauf in irgendeiner Weise beruht.

Zu diesem Zweck sollten Sie möglicherweise überlegen, wie die Abhängigkeiten verwaltet werden sollen. Statt etwas wie Konstruktor Injektion oder Setter-Injektion, des Weiteren Invertieren Sie die Verwaltung von Abhängigkeiten und lassen Sie das Objekt intern durch ein weiteres Objekt auflösen.

Wenn man den oben genannten Service-Locator als Startmuster verwendet, ist es ziemlich einfach, ein Objekt zu erstellen, dessen einziger Zweck (dessen einzige Verantwortung) es ist, Abhängigkeiten aufzulösen. Wenn Sie ein Dependency-Injection-Framework verwenden, ist dieses Objekt im Allgemeinen nur ein Durchgriff für die Funktionalität des Frameworks (aber abstrahiert das Framework selbst ... also eine weniger Abhängigkeit, was eine gute Sache ist). Wenn Sie eine selbst entwickelte Funktionalität verwenden, abstrahiert dieses Objekt diese Funktionalität.

Aber was du am Ende hast, ist so etwas in dir MyMathObject:

private SomeInternalFunction()
{
    var firstDependency = ServiceLocatorObject.Resolve<MyOtherClass1>();
    // implementation details
}

Also jetzt der Fußabdruck von MyMathObject, selbst mit Abhängigkeitsinjektion, ist:

public class MyMathObject
{
    public int Add(int addend, int augend)
    {
        // implementation details
    }
}

Keine undichten Abstraktionen, keine extern bekannten Abhängigkeiten. Tests müssen nicht geändert werden, da die Implementierungsdetails geändert werden. Dies ist ein weiterer Schritt zur Entkoppelung der Tests von den Objekten, die sie testen.


20
2018-01-26 14:27



Natürlich können Sie keinen Test schreiben, wenn Sie nicht wissen, was die "Software" Absicht ist, aber es ist absolut möglich ob das Anforderungen oder Spezifikationen sind detailliert.

Sie können etwas schreiben, das der Anforderung entspricht das wird scheitern; dann die minimale Menge an Arbeit produzieren, um den Test basierend auf der Spezifikation bestehen zu lassen.

Es sei denn, Sie sind eine Art von Genie - dieser erste Schnitt muss Refactoring und Abstraktionen eingeführt, Muster, Wartbarkeit, Leistung und alle möglichen anderen Dinge berücksichtigt werden.

Wenn also die Anforderungen verstanden werden, können Sie zuerst testen - aber der Test wird nicht bestanden, bis die Implementierung zusammenkommt und nur die Implementierung erforderlich ist, um den Test zu bestehen.

In der Realität wird es nicht immer der Realität entsprechen, besonders wenn die Spezifikation schwer zu bekommen ist. Sie müssen aufpassen, diesen Pfad nicht blindlings zu stürmen, wenn Sie nicht das bekommen, was Sie als Entwickler brauchen. Es ist auch oft unmöglich zu erreichen, wenn Code vererbt oder zu "Brownfield" -Projekten hinzugefügt wird. Als Entwickler ist es wichtig, die praktischen Dinge frühzeitig zu erarbeiten.


1
2018-01-26 14:22



Dies ist ein sehr schwieriges Problem für die Leute, wenn sie TDD starten. Ich bin sicher, es gibt viele gute Antworten auf SO und besonders auf programmers.stackexchange.com (diese Frage ist wahrscheinlich besser für dieses Forum geeignet).

Es gibt viele Dinge, die ich Ihnen sagen könnte, um Ihnen zu helfen, aber keine würde so gut funktionieren, wie Sie tatsächlich eine TDD machen. Als einen schnellen Startplatz werde ich Sie verweisen Dieser Artikel über Code Katas, die zu einigen nützlichen TDD-Übungen verlinkt.


0
2018-01-26 14:22



Nicht wirklich. Mit TDD beginnen Sie mit Ihren Testfällen, mit denen Sie Ihren Produktcode erwarten. Es bedeutet nicht, dass es sein sollte Nein Produktcode. Ihre Klassenfunktionen usw. können existieren. Sie beginnen mit fehlgeschlagenen Testfällen und nehmen dann Änderungen an Ihrem Produktcode vor, um sie zu bestehen.

Sehen http://msdn.microsoft.com/en-us/library/aa730844(v=vs.80).aspx#guidelinesfortdd_topic2


0
2018-01-26 14:23



Zunächst einmal: Viele Unit-Tests erfordern keine Mocks, keine Interaktionen mit anderen Objekten; Ihre Frage gilt nicht für diese.

Wenn der Zweck oder der Teil des Zwecks einer neuen Methode Auswirkungen auf ein kollaborierendes Objekt haben soll, dann ist das Teil dessen, was getestet werden muss. Wenn dies nicht der Zweck ist, aber Ihre Implementierung hat Auswirkungen auf einen Mitarbeiter, dann sollten Sie diesen Nebeneffekt nicht testen.

So oder so, es ist einfach zu sehen, dass Sie den Test schreiben können, bevor Sie den Code schreiben. Wenn Ihre Methode ein anderes Objekt beeinflussen soll, sollte der Test dies sagen. Wenn Ihre Methode nicht dazu gedacht ist, muss der Test nichts über die Interaktion der Methode mit anderen Objekten sagen.


0
2018-01-26 17:10