Konstanten zwischen Tests und Produktionscode duplizieren?

20

Ist es gut oder schlecht, Daten zwischen Tests und echtem Code zu duplizieren? Angenommen, ich habe eine Python-Klasse FooSaver, die Dateien mit bestimmten Namen in einem bestimmten Verzeichnis speichert:

class FooSaver(object):
  def __init__(self, out_dir):
    self.out_dir = out_dir

  def _save_foo_named(self, type_, name):
    to_save = None
    if type_ == FOOTYPE_A:
      to_save = make_footype_a()
    elif type == FOOTYPE_B:
      to_save = make_footype_b()
    # etc, repeated
    with open(self.out_dir + name, "w") as f:
      f.write(str(to_save))

  def save_type_a(self):
    self._save_foo_named(a, "a.foo_file")

  def save_type_b(self):
    self._save_foo_named(b, "b.foo_file")

Jetzt möchte ich in meinem Test sicherstellen, dass alle diese Dateien erstellt wurden. Deshalb möchte ich Folgendes sagen:

foo = FooSaver("/tmp/special_name")
foo.save_type_a()
foo.save_type_b()

self.assertTrue(os.path.isfile("/tmp/special_name/a.foo_file"))
self.assertTrue(os.path.isfile("/tmp/special_name/b.foo_file"))

Obwohl dies die Dateinamen an zwei Stellen dupliziert, finde ich es gut: Es zwingt mich, genau das aufzuschreiben, was ich am anderen Ende erwarten würde, es schützt mich zusätzlich vor Tippfehlern und lässt mich im Allgemeinen zuversichtlich sein, dass die Dinge funktionieren genau wie ich es erwarte. Ich weiß , dass , wenn ich ändern , a.foo_fileum type_a.foo_filein der Zukunft werde ich einige Suche und Ersetzen in meinen Tests zu tun haben, aber ich glaube nicht , das ist zu große Sache. Wenn ich vergesse, den Test im Gegenzug zu aktualisieren, um sicherzugehen, dass mein Verständnis des Codes und der Tests synchron sind, möchte ich lieber einige Fehlalarme erhalten.

Ein Kollege hält diese Vervielfältigung für schlecht und empfahl, dass ich beide Seiten in etwa so umgestalte:

class FooSaver(object):
  A_FILENAME = "a.foo_file"
  B_FILENAME = "b.foo_file"

  # as before...

  def save_type_a(self):
    self._save_foo_named(a, self.A_FILENAME)

  def save_type_b(self):
    self._save_foo_named(b, self.B_FILENAME)

und im Test:

self.assertTrue(os.path.isfile("/tmp/special_name/" + FooSaver.A_FILENAME))
self.assertTrue(os.path.isfile("/tmp/special_name/" + FooSaver.B_FILENAME))

Ich mag das nicht, weil es mich nicht sicher macht, ob der Code das tut, was ich erwartet hatte. Ich habe den out_dir + nameSchritt gerade sowohl auf der Produktionsseite als auch auf der Testseite dupliziert . Es wird keinen Fehler in meinem Verständnis der Funktionsweise +von Zeichenfolgen aufdecken und keine Tippfehler erkennen.

Andererseits ist es deutlich weniger spröde als das zweimalige Ausschreiben dieser Zeichenfolgen, und es scheint mir ein wenig falsch, Daten über zwei solche Dateien zu duplizieren.

Gibt es hier einen klaren Präzedenzfall? Ist es in Ordnung, Konstanten über Tests und Produktionscode zu duplizieren, oder ist es zu spröde?

Patrick Collins
quelle

Antworten:

16

Ich denke, es hängt davon ab, was Sie testen wollen, was der Vertrag der Klasse beinhaltet.

Wenn der Vertrag der Klasse genau der ist, der FooSavergeneriert wird, a.foo_fileund sich b.foo_filean einem bestimmten Ort befindet, sollten Sie dies direkt testen, dh die Konstanten in den Tests duplizieren.

Wenn der Vertrag der Klasse jedoch lautet, dass zwei Dateien in einem temporären Bereich erstellt werden, deren Namen leicht geändert werden können, insbesondere zur Laufzeit, müssen Sie generischer testen, wahrscheinlich mit Konstanten, die aus den Tests herausgerechnet wurden.

Sie sollten sich also mit Ihrem Kollegen über die wahre Natur und den Vertrag der Klasse aus einer übergeordneten Perspektive des Domain-Designs streiten. Wenn Sie nicht einverstanden sind, würde ich sagen, dass dies ein Problem des Verständnisses und der Abstraktionsebene der Klasse selbst ist, anstatt sie zu testen.

Es ist auch sinnvoll, festzustellen, dass sich der Vertrag der Klasse während des Refactorings ändert, um beispielsweise die Abstraktionsstufe im Laufe der Zeit zu erhöhen. Zuerst kann es sich um die beiden spezifischen Dateien an einem bestimmten temporären Speicherort handeln, aber im Laufe der Zeit ist möglicherweise eine zusätzliche Abstraktion erforderlich. Ändern Sie in diesem Fall die Tests, um sie mit dem Vertrag der Klasse synchron zu halten. Es ist nicht notwendig, den Vertrag der Klasse sofort zu überarbeiten, nur weil Sie ihn testen (YAGNI).

Wenn der Vertrag einer Klasse nicht genau definiert ist, kann das Testen dazu führen, dass wir die Art der Klasse in Frage stellen, dies aber auch tun würden. Ich würde sagen, dass Sie den Vertrag der Klasse nicht aktualisieren sollten, nur weil Sie ihn testen. Sie sollten den Vertrag der Klasse aus anderen Gründen aktualisieren, z. B. weil es sich um eine schwache Abstraktion für die Domäne handelt. Wenn dies nicht der Fall ist, testen Sie sie so, wie sie ist.

Erik Eidt
quelle
4

Was @Erik vorschlug, um sicherzustellen, dass Sie genau wissen, was Sie testen, sollte auf jeden Fall Ihre erste Überlegung sein.

Aber sollte Ihre Entscheidung Sie dazu führen, die Konstanten herauszurechnen, so dass der interessante Teil Ihrer Frage (Umschreibung) "Warum sollte ich doppelte Konstanten gegen doppelten Code eintauschen?" (In Bezug auf die Stelle, an der von "Duplizieren des Namensschritts out_dir +" die Rede ist.)

Ich glaube , dass (Modulo Erik Kommentare) die meisten Situationen tun profitieren von Entfernen von doppelten Konstanten. Sie müssen dies jedoch so tun, dass Code nicht dupliziert wird. In Ihrem speziellen Beispiel ist dies einfach. Behandeln Sie einen Pfad nicht als "rohe" Zeichenfolgen in Ihrem Produktionscode, sondern als Pfad. Dies ist eine robustere Methode zum Verknüpfen von Pfadkomponenten als die Verkettung von Zeichenfolgen:

os.path.join(self.out_dir, name)

In Ihrem Testcode würde ich dagegen so etwas empfehlen. Hier zeigt die Betonung, dass Sie einen Pfad haben und einen Blattdateinamen "einstecken":

self.assertTrue(os.path.isfile("/tmp/special_name/{0}".format(FooSaver.A_FILENAME)))

Das heißt, durch eine sorgfältigere Auswahl der Sprachelemente können Sie automatisch eine Codeduplizierung vermeiden. (Nicht die ganze Zeit, aber meiner Erfahrung nach sehr oft.)

Michael Sorens
quelle
1

Ich stimme der Antwort von Erik Eidt zu, aber es gibt eine dritte Option: Die Konstante im Test ausstreichen, sodass der Test auch dann erfolgreich ist, wenn Sie den Wert der Konstante im Produktionscode ändern.

(siehe Stubbing einer Konstante in Python unittest )

foo = FooSaver("/tmp/special_name")
foo.save_type_a()
foo.save_type_b()

with mock.patch.object(FooSaver, 'A_FILENAME', 'unique_to_your_test_a'):
  self.assertTrue(os.path.isfile("/tmp/special_name/unique_to_your_test_a"))
with mock.patch.object(FooSaver, 'B_FILENAME', 'unique_to_your_test_b'):
  self.assertTrue(os.path.isfile("/tmp/special_name/unique_to_your_test_b"))

Und wenn Dinge wie diese tun würde ich in der Regel sicher, eine geistige Gesundheit Test zu machen , wo ich die Tests ohne laufen withAussage und stellen Sie sicher , ich sehe „‚a.foo_file‘! =‚Unique_to_your_test_a‘“, dann die Put - withAnweisung zurück im Test so geht es wieder vorbei.

alexanderbird
quelle