Wie kann ich Unit-Test-Methoden verwenden, die statische Methoden verwenden?

8

Nehmen wir an, ich habe in C # eine Erweiterungsmethode für byteArrays geschrieben, die sie wie folgt in Hex-Strings codiert:

public static class Extensions
{
    public static string ToHex(this byte[] binary)
    {
        const string chars = "0123456789abcdef";
        var resultBuilder = new StringBuilder();
        foreach(var b in binary)
        {
            resultBuilder.Append(chars[(b >> 4) & 0xf]).Append(chars[b & 0xf]);
        }
        return resultBuilder.ToString();
    }
}

Ich könnte die obige Methode mit NUnit wie folgt testen :

[Test]
public void TestToHex_Works()
{
    var bytes = new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef };
    Assert.AreEqual("0123456789abcdef", bytes.ToHex());
}

Wenn ich das Extensions.ToHexInnere meines Projekts verwende, nehmen wir in der Foo.DoMethode Folgendes an:

public class Foo
{
    public bool Do(byte[] payload)
    {
        var data = "ES=" + payload.ToHex() + "ff";
        // ...
        return data.Length > 5;
    }
    // ...
}

Dann Foo.Dohängen alle Willensprüfungen vom Erfolg ab TestToHex_Works.

Bei Verwendung freier Funktionen in C ++ ist das Ergebnis dasselbe: Tests, bei denen Testmethoden, die freie Funktionen verwenden, getestet werden, hängen vom Erfolg der Tests freier Funktionen ab.

Wie kann ich mit solchen Situationen umgehen? Kann ich diese Testabhängigkeiten irgendwie auflösen? Gibt es eine bessere Möglichkeit, die obigen Codefragmente zu testen?

Akira
quelle
4
Then all tests of Foo.Do will depend on the success of TestToHex_works-- Damit? Sie haben keine Klassen, die vom Erfolg anderer Klassen abhängen?
Robert Harvey
5
Ich habe diese Besessenheit von freien / statischen Funktionen und ihrer sogenannten Nichtprüfbarkeit nie ganz verstanden. Wenn eine freie Funktion frei von Nebenwirkungen ist, ist es die einfachste Sache auf dem Planeten, zu testen und zu beweisen, dass sie funktioniert. Sie haben dies in Ihrer eigenen Frage sehr effektiv demonstriert. Wie testen Sie gewöhnliche, nebenwirkungsfreie Methoden (die nicht vom Klassenstatus abhängig sind) in Objektinstanzen? Ich weiß, dass Sie einige davon haben.
Robert Harvey
2
Der einzige Nachteil dieses Codes bei Verwendung statischer Funktionen ist, dass Sie nicht einfach etwas anderes als toHex(oder Swap-Implementierungen) verwenden können. Ansonsten ist alles in Ordnung. Ihr Code, der in Hex konvertiert wird, wird getestet. Jetzt gibt es einen anderen Code, der diesen getesteten Code als Dienstprogramm verwendet, um sein eigenes Ziel zu erreichen.
Steve Chamaillard
3
Mir fehlt völlig, was hier das Problem ist. Wenn ToHex nicht funktioniert, ist klar, dass Do auch nicht funktioniert.
Simon B
5
Der Test für Foo.Do () sollte nicht wissen oder sich darum kümmern, dass ToHex () unter dem Deckmantel aufgerufen wird. Dies ist ein Implementierungsdetail.
17 von 26 26.

Antworten:

37

Dann Foo.Dohängen alle Tests vom Erfolg von TestToHex_Works ab.

Ja. Deshalb haben Sie Tests für TextToHex. Wenn diese Tests bestanden werden, erfüllt die Funktion die in diesen Tests definierte Spezifikation. So Foo.Dokann man es sicher anrufen und sich keine Sorgen machen. Es ist bereits abgedeckt.

Sie können eine Schnittstelle hinzufügen, die Methode zu einer Instanzmethode machen und in diese einfügen Foo. Dann könntest du dich lustig machen TextToHex. Aber jetzt müssen Sie einen Mock schreiben, der möglicherweise anders funktioniert. Sie benötigen also einen "Integrationstest", um die beiden zusammenzubringen und sicherzustellen, dass die Teile wirklich zusammenarbeiten. Was hat das erreicht, außer die Dinge komplexer zu machen?

Die Idee, dass Unit-Tests Teile Ihres Codes isoliert von anderen Teilen testen sollten, ist ein Irrtum. Die "Einheit" in einem Einheitentest ist eine isolierte Ausführungseinheit. Wenn zwei Tests gleichzeitig ausgeführt werden können, ohne sich gegenseitig zu beeinflussen, werden sie isoliert ausgeführt, ebenso wie Unit-Tests. Statische Funktionen, die schnell sind, keinen komplexen Aufbau haben und keine Nebenwirkungen wie Ihr Beispiel haben, können daher direkt in Komponententests verwendet werden. Wenn Sie Code haben, der langsam, komplex einzurichten oder Nebenwirkungen hat, sind Mocks hilfreich. Sie sollten jedoch an anderer Stelle vermieden werden.

David Arno
quelle
2
Sie machen ein ziemlich gutes Argument für die "Test-First" -Entwicklung. Zum Teil gibt es Verspottungen, weil Code so geschrieben wird, dass es zu schwer zu testen ist. Wenn Sie Ihre Tests zuerst schreiben, zwingen Sie sich, Code zu schreiben, der leichter zu testen ist.
Robert Harvey
3
Ich stimme nur dafür, dass ich empfehle, keine reinen Funktionen zu verspotten. Zu oft gesehen.
Jared Smith
2

Bei Verwendung freier Funktionen in C ++ ist das Ergebnis dasselbe: Tests, bei denen Testmethoden, die freie Funktionen verwenden, getestet werden, hängen vom Erfolg der Tests freier Funktionen ab.

Wie kann ich mit solchen Situationen umgehen? Kann ich diese Testabhängigkeiten irgendwie auflösen? Gibt es eine bessere Möglichkeit, die obigen Codefragmente zu testen?

Nun, ich sehe hier keine Abhängigkeit. Zumindest nicht die Art, die uns zwingt, einen Test vor dem anderen durchzuführen. Die Abhängigkeit, die wir zwischen Tests aufbauen (unabhängig von der Art), ist ein Vertrauen .

Wir erstellen einen Code (Test zuerst oder nicht) und stellen sicher, dass die Tests bestanden werden. Dann sind wir in der Lage, mehr Code zu erstellen. Der gesamte Code, der zuerst darauf aufgebaut ist, basiert auf Vertrauen und Gewissheit. Dies ist mehr oder weniger das, was @DavidArno (sehr gut) in seiner Antwort erklärt.

Ja. Aus diesem Grund haben Sie Tests für X. Wenn diese Tests erfolgreich sind, entspricht die Funktion den in diesen Tests definierten Spezifikationen. So kann Y es sicher anrufen und sich keine Sorgen machen. Es ist bereits abgedeckt.

Unit-Tests sollten in beliebiger Reihenfolge, zu jeder Zeit, in jeder Umgebung und so schnell wie möglich ausgeführt werden. Ob TestToHex_Worksder erste oder der letzte ausgeführt wird, sollte Sie nicht beunruhigen.

Wenn TestToHex_Worksdies aufgrund von Fehlern fehlschlägt ToHex, ToHexenden alle Tests, auf die Sie sich verlassen, mit unterschiedlichen Ergebnissen und schlagen (idealerweise) fehl. Der Schlüssel hier ist das Erkennen dieser unterschiedlichen Ergebnisse. Wir machen es so, dass Unit-Tests deterministisch sind. Wie du es hier tust

var bytes = new byte[] { 0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef };
Assert.AreEqual("0123456789abcdef", bytes.ToHex());

Weitere Unit-Tests, auf die man sich stützt, ToHexsollten ebenfalls deterministisch sein. Wenn alles ToHexgut geht, sollte das Ergebnis das erwartete sein. Wenn Sie eine andere bekommen, ist irgendwo etwas schiefgegangen, und genau das möchten Sie von einem Komponententest, um diese subtilen Änderungen zu erkennen und schnell zu scheitern.

Laiv
quelle
1

Mit einem so minimalen Beispiel ist es ein bisschen schwierig, aber lassen Sie uns noch einmal überlegen, was wir tun:

Nehmen wir an, ich habe in c # eine Erweiterungsmethode für Byte-Arrays geschrieben, die sie wie folgt in Hex-Strings codiert:

Warum haben Sie dazu eine Erweiterungsmethode geschrieben? Sofern Sie keine Bibliothek schreiben, um Byte-Arrays in Zeichenfolgen zu codieren, ist dies wahrscheinlich nicht die Anforderung, die Sie erfüllen möchten. Wie es aussieht, haben Sie hier versucht, die Anforderung "Validieren einer Nutzlast, bei der es sich um ein Byte-Array handelt" zu erfüllen. Die von Ihnen implementierten Komponententests sollten daher "Bei einer gültigen Nutzlast X gibt meine Methode true zurück" und "Gegeben" lauten Bei einer ungültigen Nutzlast Y gibt meine Methode false zurück. "

Wenn Sie dies zum ersten Mal implementieren, können Sie das "Byte-to-Hex" -Ding in der Methode inline ausführen. Und das ist gut so. Später erhalten Sie dann eine andere Anforderung (z. B. "Nutzdaten als Hex-Zeichenfolge anzeigen"), bei der Sie bei der Implementierung feststellen, dass Sie auch ein Byte-Array in eine Hex-Zeichenfolge konvertieren müssen. An diesem Punkt erstellen Sie Ihre Erweiterungsmethode, überarbeiten die alte Methode und rufen sie aus Ihrem neuen Code auf. Ihre Tests für die Validierungsfunktionalität sollten sich nicht ändern. Ihre Tests zur Anzeige der Nutzdaten sollten gleich sein, unabhängig davon, ob der Byte-zu-Hex-Code inline oder statisch war. Die statische Methode ist ein Implementierungsdetail, das Sie beim Schreiben der Tests nicht berücksichtigen sollten. Der Code in der statischen Methode wird von seinen Verbrauchern getestet. Ich würde nicht'

Chris Cooper
quelle
"Dies ist wahrscheinlich nicht die Anforderung, die Sie erfüllen möchten. Wie es aussieht, haben Sie hier versucht, die Anforderung zu erfüllen." - Sie machen viele Annahmen über den Zweck einer Funktion, die am wahrscheinlichsten ist wurde nur als repräsentatives Beispiel ausgewählt und nicht aus einem größeren Programm.
Whatsisname
Die "Einheit" in "Einheitentest" bedeutet keine einzelne Funktion oder Klasse. Es bedeutet eine Funktionseinheit. Sofern Sie nicht speziell eine Bibliothek schreiben, die Bytes in Zeichenfolgen konvertiert, ist dieser Teil ein Implementierungsdetail für ein nützlicheres Verhalten. Dieses nützliche Verhalten ist der Teil, für den Sie Tests durchführen möchten, denn genau darum kümmern Sie sich um die Richtigkeit. In einer idealen Welt möchten Sie in der Lage sein, eine Bibliothek zu finden, die bereits für die Ausführung des Bin-to-Hex-Vorgangs vorhanden ist (oder was auch immer es ist, lassen Sie sich nicht auf die Details ein), tauschen Sie sie gegen Ihre Implementierung aus und ändern Sie keine Tests.
Chris Cooper
Der Teufel steckt im Detail. Unit-Tests "Implementierungsdetails" sind immer noch eine enorm nützliche Sache und nicht zu überfliegen.
Whatsisname
0

Genau. Und dies ist eines der Probleme bei statischen Methoden. Ein weiteres Problem ist, dass OOP in den meisten Situationen eine viel bessere Alternative ist.

Dies ist auch einer der Gründe, warum Dependency Injection verwendet wird.

In Ihrem Fall bevorzugen Sie möglicherweise einen konkreten Konverter , den Sie in eine Klasse einfügen, für die einige Werte in hexadezimal konvertiert werden müssen . Eine von dieser konkreten Klasse implementierte Schnittstelle würde den Vertrag definieren, der zum Konvertieren der Werte erforderlich ist.

Sie können Ihren Code nicht nur einfacher testen, sondern auch Implementierungen später austauschen (da es viele andere Möglichkeiten gibt, Werte in Hexadezimalzahlen umzuwandeln, von denen einige unterschiedliche Ausgaben erzeugen).

Arseni Mourzenko
quelle
2
Wie wird der Code dadurch nicht vom Erfolg der Konvertertests abhängig? Wenn Sie eine Klasse, eine Schnittstelle oder etwas anderes einfügen, funktioniert dies nicht auf magische Weise. Wenn Sie über Nebenwirkungen sprechen würden, dann würde es eine Menge Wert geben, eine Fälschung zu injizieren, anstatt eine Implementierung hart zu codieren, aber dieses Problem gab es sowieso überhaupt nicht.
Steve Chamaillard
1
@ArseniMourzenko Wenn Sie also eine Taschenrechnerklasse einfügen, die eine multiplyMethode implementiert , würden Sie sich darüber lustig machen ? Was bedeutet, dass Sie zum Refactor (und stattdessen zum Verwenden des Operators *) jede einzelne Scheindeklaration in Ihrem Code ändern müssen? Sieht für mich nicht nach einfachem Code aus. Alles, was ich sage, ist toHexsehr einfach und hat überhaupt keine Nebenwirkungen, daher sehe ich keinen Grund, dies zu verspotten oder zu stoppen. Es sind zu viel Kosten für absolut keinen Gewinn. Das heißt nicht, dass wir es nicht injizieren sollten, aber das hängt von der Verwendung ab.
Steve Chamaillard
2
@Ewan, wenn ich einen Code modifiziere und dann " einem Meer von Rot " gegenüberstehe, könnte ich denken, oh nein, wo fange ich an?. Aber vielleicht könnte ich einfach zu dem Code zurückkehren, den ich gerade geändert habe, und mir zuerst die Tests ansehen, um zu sehen, was ich kaputt gemacht habe. : p
David Arno
3
@ArseniMourzenko, wenn DI Ihr Design so viel besser macht, warum sehe ich dann keine Argumente für das Injizieren von Typen wie Stringund intauch? Oder grundlegende Utility-Klassen aus der Standardbibliothek?
Bart van Ingen Schenau
4
Ihre Antwort ignoriert praktische Aspekte vollständig. Es gibt viele Situationen mit statischen Methoden, die schnell, in sich geschlossen und unwahrscheinlich genug sind, um sich jemals zu ändern. Es ist sinnlose Zeitverschwendung, sich die Mühe zu machen, sie zu injizieren oder durch einen anderen Rahmen als einen regulären Funktionsaufruf zu springen Anstrengung.
Whatsisname