it-swarm.com.de

Sollten meine Klassen separate Konstruktoren nur für Unit-Tests haben?

Ich schreibe gerne Klassen mit zwei Konstruktoren: einem primären Konstruktor, der im Produktionscode verwendet wird, und einem Standardkonstruktor nur für Komponententests. Ich mache das, weil der primäre Konstruktor andere konkrete Abhängigkeiten erstellt, die externe Ressourcen verbrauchen. Ich kann das alles nicht in einem Unit-Test machen.

Meine Klassen sehen also so aus:

public class DoesSomething : BaseClass
{
    private Foo _thingINeed;
    private Bar _otherThingINeed;
    private ExternalResource _another;

    public DoesSomething()
    {
        // Empty constructor for unit tests
    }

    public DoesSomething(string someUrl, string someThingElse, string blarg)
    {
         _thingINeed = new Foo(someUrl);
         _otherThingINeed = Foo.CreateBar(blarg);
         _another = BlargFactory.MakeBlarg(_thingINeed, _otherThingINeed.GetConfigurationValue("important");
    }
}

Diesem Muster folge ich bei vielen meiner Klassen, damit ich Unit-Tests für sie schreiben kann. Ich füge den Kommentar immer in den Standardkonstruktor ein, damit andere erkennen können, wofür er gedacht ist. Ist dies eine gute Möglichkeit, testbare Klassen zu schreiben, oder gibt es einen besseren Ansatz?


Es wurde vorgeschlagen, dass dies ein Duplikat von "Sollte ich einen Konstruktor mit injizierten Abhängigkeiten und einen anderen, der sie instanziiert" haben könnte. Es ist nicht. Bei dieser Frage geht es um eine andere Art, die Abhängigkeiten zu erstellen. Wenn ich den Standard-Unit-Test-Konstruktor verwende, werden die Abhängigkeiten überhaupt nicht erstellt. Dann mache ich die meisten Methoden öffentlich und sie verwenden die Abhängigkeiten nicht. Das sind die, die ich teste.

6
Scott Hannen

Erstellen Sie niemals separate Codepfade für Unit-Tests. Dies macht den Zweck des Komponententests zunichte, da Sie in Ihren Komponententests einen Codepfad testen und in der Produktion einen anderen ausführen.

Aus irgendeinem Grund scheint das nicht ausreichend zu sein, weil ich Ihnen nur Teile Ihrer Frage wiederhole. Sie wissen bereits, dass Sie in Ihren Komponententests unterschiedlichen Code ausführen. Der Kommentar, den Sie in Ihren Konstruktor eingegeben haben, sagt dies aus. Es ist, als würde man fragen, ob es angemessen ist, das Handy eines anderen mitzunehmen, weil es ihm gehört, nicht Ihnen, und Sie es haben möchten.

Würde ein Argument aus dem Konsens helfen? Tu das nicht, weil es sonst niemand tut. Jemand, der es besser weiß, wird schockiert sein. Schlimmer noch, jemand, der es nicht besser weiß, könnte daraus lernen, was schlecht wäre, weil ... ich das Gefühl habe, immer noch im Kreis zu fahren. Aber hoffentlich hat es etwas Gewicht, dass andere Leute sagen, dass Sie das nicht tun sollten.

Vielleicht könnte ich Ihnen zeigen, dass es einen besseren Weg gibt: Abhängigkeitsinversion und Abhängigkeitsinjektion. Abhängigkeitsinversion bedeutet, dass Sie von Abstraktionen (wie Schnittstellen) anstelle von konkreten Implementierungen abhängig sind. Dies ist unmöglich, wenn Ihre Klasse eigene Abhängigkeiten erstellt. Hier kommt die Abhängigkeitsinjektion ins Spiel.

Anstatt Abhängigkeiten zu erstellen, definieren Sie Abstraktionen, die beschreiben, wie Ihre Klasse sie verwenden muss. Wenn Sie beispielsweise mit ExternalResource Dateien herunterladen, können Sie eine Schnittstelle wie die folgende erstellen:

public interface IFileStorage
{
    byte[] GetFile(string filePath);
}

Dann erstellen Sie Ihre Klasse folgendermaßen:

public class DoesSomething : BaseClass
{
    private readonly IFileStorage _fileStorage;

    public DoesSomething(IFileStorage fileStorage)
    {
        _fileStorage = fileStorage;
    }
}

Jetzt brauchen Sie keinen separaten Konstruktor. Ihre Komponententests können den Konstruktor real aufrufen und einen Mock oder ein Testdouble übergeben. Dies bedeutet eine Implementierung von IFileStorage, die genau das zurückgibt, was Sie möchten. Auf diese Weise können Sie Tests schreiben, wie sich Ihre Klasse verhält, wenn die Datei dies oder jenes oder gar nichts enthält, ohne eine konkrete Implementierung von IFileStorage zu verwenden.

Was ist beispielsweise, wenn Sie sehen möchten, wie sich Ihre Klasse verhält, wenn IFileStorage ein leeres Byte-Array zurückgibt? Sie können ein Framework wie Moq verwenden oder einfach Folgendes tun:

public class FileStorageThatReturnsEmptyArray : IFileStorage
{
    public byte[] GetFile(string filePath)
    {
        return new byte[] {};
    }
}

Dann sieht Ihre Testperson wie folgt aus:

var subject = new DoesSomething(new FileStorageThatReturnsEmptyArray());

Ist das nicht besser Ihre Unit-Tests sind etwas wert, weil sie Ihre Klasse durchgängig testen und es ihr ermöglichen, sich genau so zu verhalten, wie es in der Produktion der Fall ist.

In der Produktion haben Sie zwar eine konkrete Implementierung von IFileStorage anstelle eines Testdoppels. Aber der Punkt ist, dass es für DoesSomething alle gleich ist. Ein IFileStorage ist dasselbe wie ein anderes.

Ist das ein Widerspruch, da ich gerade gesagt habe, dass Sie den "echten" Code testen sollten, aber jetzt empfehle ich die Verwendung von Mocks? Nein. Die Frage betraf Unit-Tests, was bedeutet, dass Sie DoesSomething testen wollten, also würden Sie Mocks verwenden, um diese Klasse zu testen. Sie können zusätzliche Komponententests für andere Klassen oder Integrationstests schreiben, um sie gemeinsam zu testen. Wenn Sie im Rahmen dieser Antwort DoesSomething testen möchten, ist der Punkt, dass Sie sich nicht täuschen möchten, indem Sie eine Nichtproduktionsmethode dieser Klasse testen und denken, dass dies dieselbe ist wie Testen der Produktionsmethode. ("Produktions" - und "Nichtproduktions" -Methoden sind nicht einmal eine Sache.)

Ich hoffe, dass es hilfreich ist, zu zeigen, wie andere Entwickler dieses Problem gelöst haben.

17
Scott Hannen

Ich bin auf dem Zaun, ob diese Antwort anders ist als die von Scott Hannen, aber es gibt einen Unterschied in Terminologie und Absicht.


Mit etwas Inspiration (und Kopieren und Einfügen) von Was ist der Unterschied zwischen einem Mock & Stub? Testen Sie Code, der die an den Konstruktor übergebenen Abhängigkeiten nicht verwendet. Was Sie suchen, ist ein Dummy-Objekt .

Aus Mocks Aren't Stubs von Martin Fowler (Hervorhebung, meine):

  • Dummy-Objekte werden herumgereicht, aber nie verwendet. Normalerweise werden sie nur zum Füllen von Parameterlisten verwendet.
  • Gefälschte Objekte haben tatsächlich funktionierende Implementierungen, verwenden jedoch normalerweise eine Verknüpfung, die sie für die Produktion nicht geeignet macht (eine In-Memory-Datenbank ist ein gutes Beispiel).
  • Stubs bieten vordefinierte Antworten auf Anrufe, die während des Tests getätigt wurden, und reagieren normalerweise überhaupt nicht auf etwas außerhalb der für den Test programmierten.
  • Spione sind Stubs, die auch einige Informationen aufzeichnen, die darauf basieren, wie sie genannt wurden. Eine Form davon könnte ein E-Mail-Dienst sein, der aufzeichnet, wie viele Nachrichten gesendet wurden.
  • Mocks sind das, worüber wir hier sprechen: Objekte, die mit Erwartungen vorprogrammiert sind und eine Spezifikation der Anrufe bilden, die sie voraussichtlich erhalten.

Sie haben einen Konstruktor. Es muss ein Objekt übergeben werden. Ihr Test verwendet dieses Objekt nicht. Sie benötigen ein Dummy-Objekt, das dieselbe öffentliche Schnittstelle wie die tatsächliche Abhängigkeit enthält, aber tatsächlich nicht irgendetwas - tatsächlich kann es Ausnahmen auslösen, wenn eine Methode darauf aufgerufen wird.

Ich werde Ihr Codebeispiel verwenden, um den Weg von hier aus zu zeigen.

Erstens haben Sie einen Konstruktor, der mehr Dinge tut, als er eigentlich sollte. Es ist Zeit, dies umzugestalten, um einen neuen Konstruktor zu erstellen, mit dem Sie die realen Objekte übergeben können:

public class DoesSomething : BaseClass
{
    private Foo _thingINeed;
    private Bar _otherThingINeed;
    private ExternalResource _another;

    public DoesSomething(string someUrl, string someThingElse, string blarg)
        : this(new Foo(someUrl), Foo.CreateBar(blarg), BlargFactory.MakeBlarg(_thingINeed, _otherThingINeed.GetConfigurationValue("important"))
    {
    }

    public DoesSomething(Foo thing1, Bar thing2, ExternalResource thing3)
    {
         _thingINeed = thing1;
         _otherThingINeed = thing2;
         _another = thing3;
    }
}

Beachten Sie die Unterschiede:

  1. Es gibt keinen leeren Konstruktor
  2. Die Signatur für den vorhandenen Konstruktor ist weiterhin vorhanden und unterstützt die Abwärtskompatibilität mit vorhandenem Code
  3. Es wurde ein neuer Konstruktor hinzugefügt, der Foo, Bar und ExternalResource enthält
  4. Der alte Konstruktor ruft den neuen Konstruktor auf und stellt sicher, dass die Initialisierungslogik nicht dupliziert wird

Ich gehe davon aus, dass ExternalResource etwas kompliziert zu erstellen ist. Für diese Abhängigkeit benötigen Sie eine Schnittstelle, die sich in einer weiteren Änderung niederschlägt:

public class DoesSomething : BaseClass
{
    // ...

    public DoesSomething(Foo thing1, Bar thing2, IExternalResource thing3)
    {
        // ...
    }
}

public interface IExternalResource
{
    // ...
}

public class ExternalResource : IExternalResource
{
    // ...
}

Dann müssen Sie sicherstellen, dass Foo und Bar keine tatsächliche Arbeit in ihren Konstruktoren ausführen. Wenn Foo und Bar von keiner Instanzmethode dieser Klasse verwendet werden, sollten sie nicht als Felder beibehalten werden! Dies würde die Abhängigkeiten dieser Klasse weiter vereinfachen und es noch einfacher machen, sich beim Testen von Einheiten zu verspotten.

Das lässt Sie also wirklich nur ein leeres, dummes Objekt erstellen:

internal class DummyExternalResource : IExternalResource
{
    public void Something()
    {
        throw new NotImplementedException();
    }
}

Erstellen Sie einfach ein Dummy-Objekt mit allen erforderlichen Methoden von IExternalResource, die nur Ausnahmen auslösen. Dann in Ihren Tests:

// Arrange
var foo = new Foo("fakeFile.text");
var bar = new Bar("fake blarg");
var externalResource = new DummyExternalResource();
var something = new DoesSomething(foo, bar, externalResource);

// Act
something.Bazz();

// Assert
Assert.IsTrue(something.FooBar);

Wenn in Zukunft für eine Codeänderung eine Methode für IExternalDependency aufgerufen werden muss, wird ein fehlgeschlagener Test angezeigt, da eine neue Abhängigkeit für die zu testende Methode eingeführt wurde. Dies sollte Entwickler dazu zwingen, die Testabdeckung dieser Methode neu zu bewerten.

2
Greg Burghardt