Duplicazione di costanti tra test e codice di produzione?


20

È positivo o negativo duplicare i dati tra test e codice reale? Ad esempio, supponiamo di avere una classe Python FooSaverche salva i file con nomi particolari in una determinata directory:

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")

Ora nel mio test vorrei assicurarmi che tutti questi file siano stati creati, quindi voglio dire qualcosa del genere:

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"))

Anche se questo duplica i nomi dei file in due punti, penso che sia buono: mi costringe a scrivere esattamente ciò che mi aspetto di uscire dall'altra parte, aggiunge uno strato di protezione contro gli errori di battitura e in generale mi fa sentire sicuro che le cose stanno funzionando esattamente come mi aspetto. So che se cambio a.foo_fileper type_a.foo_fileil futuro ho intenzione di avere a che fare un po 'di ricerca e sostituzione nel mio test, ma non credo che è troppo grande di un affare. Preferirei avere alcuni falsi positivi se dimentico di aggiornare il test in cambio di assicurarmi che la mia comprensione del codice e i test siano sincronizzati.

Un collega ritiene che questa duplicazione sia negativa e mi ha consigliato di refactificare entrambe le parti in qualcosa del genere:

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)

e nel test:

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

Non mi piace perché non mi rende sicuro che il codice stia facendo quello che mi aspettavo --- ho appena duplicato il out_dir + namepassaggio sia dal lato della produzione che da quello del test. Non scoprirà un errore nella mia comprensione di come +funziona sulle stringhe e non colpirà errori di battitura.

D'altra parte, è chiaramente meno fragile che scrivere quelle stringhe due volte, e mi sembra un po 'sbagliato duplicare i dati su due file come quello.

C'è un chiaro precedente qui? Va bene duplicare le costanti tra i test e il codice di produzione o è troppo fragile?

Risposte:


16

Penso che dipenda da cosa stai cercando di testare, che va a quello che è il contratto della classe.

Se il contratto della classe è esattamente quello che FooSavergenera a.foo_filee b.foo_filein una posizione particolare, allora dovresti testarlo direttamente, cioè duplicare le costanti nei test.

Se, tuttavia, il contratto della classe prevede che generi due file in un'area temporanea, i nomi di ciascuno che possono essere facilmente modificati, specialmente in fase di esecuzione, è necessario eseguire test più generici, probabilmente utilizzando costanti fattorizzate fuori dai test.

Quindi dovresti discutere con il tuo collega sulla vera natura e sul contratto della classe da una prospettiva di progettazione del dominio di livello superiore. Se non puoi essere d'accordo, direi che questo è un problema di comprensione e livello di astrazione della classe stessa, piuttosto che testarlo.

È anche ragionevole scoprire che il contratto della classe cambia durante il refactoring, ad esempio per aumentare nel tempo il suo livello di astrazione. Inizialmente, si tratta del file specifico in una determinata posizione temporanea, ma con il passare del tempo potresti trovare un'ulteriore astrazione garantita. In quel momento, modifica i test per mantenerli sincronizzati con il contratto della classe. Non è necessario costruire subito il contratto della classe solo perché lo stai testando (YAGNI).

Quando il contratto di una classe non è ben definito, il suo test può farci mettere in discussione la natura della classe, ma anche usarla. Direi che non dovresti aggiornare il contratto della classe solo perché lo stai testando; dovresti aggiornare il contratto della classe per altri motivi, come ad esempio, si tratta di un'astrazione debole per il dominio e, in caso contrario, testarlo così com'è.


4

Ciò che @Erik ha suggerito - in termini di accertamento di ciò che stai testando - dovrebbe sicuramente essere il tuo primo punto di considerazione.

Ma la tua decisione dovrebbe portarti alla direzione del factoring delle costanti, che lascia la parte interessante della tua domanda (parafrasando) "Perché dovrei scambiare le costanti duplicate per duplicare il codice?". (In riferimento a dove parli di "duplicat [ing] out_dir + name step".)

Credo che (commenti di Modulo Erik) la maggior parte delle situazioni tragga vantaggio dalla rimozione di costanti duplicate. Ma devi farlo in un modo che non duplica il codice. Nel tuo esempio particolare, questo è facile. Invece di trattare un percorso come stringhe "non elaborate" nel codice di produzione, considera un percorso come un percorso. Questo è un modo più efficace per unire i componenti del percorso rispetto alla concatenazione di stringhe:

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

Nel tuo codice di test, d'altra parte, consiglierei qualcosa di simile. Qui, l'enfasi sta mostrando che hai un percorso e che stai "collegando" un nome di file foglia:

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

Cioè, con una più attenta selezione degli elementi del linguaggio, è possibile evitare automaticamente la duplicazione del codice. (Non sempre, ma molto spesso nella mia esperienza.)


1

Concordo con la risposta di Erik Eidt, ma esiste una terza opzione: stub la costante nel test, quindi il test passa anche se si modifica il valore della costante nel codice di produzione.

(vedi stubbing una costante in pitone 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"))

E quando faccio cose del genere di solito mi assicurerei di fare un test di withintegrità in cui eseguo i test senza la dichiarazione e mi assicuro di vedere "'a.foo_file'! = 'Unique_to_your_test_a'", quindi rimetti la withdichiarazione nel test così passa di nuovo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.