Duplicazione delle costanti tra test e codice di produzione?

19

È bello o sbagliato duplicare i dati tra test e codice reale? Ad esempio, supponiamo di avere una classe Python FooSaver che 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 mi piacerebbe assicurarmi che tutti questi file siano stati creati, quindi voglio dire qualcosa del tipo:

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 di file in due punti, penso che sia buono: mi costringe a scrivere esattamente quello che mi aspetto di uscire dall'altra parte, aggiunge uno strato di protezione contro gli errori di battitura e in genere mi fa sentire sicuro che le cose stanno funzionando esattamente come mi aspetto. So che se cambiò a.foo_file in type_a.foo_file in futuro dovrò fare delle ricerche e sostituirmi nei miei test, ma non penso che sia un grosso problema. Preferirei avere dei falsi positivi se dimentico di aggiornare il test per assicurarmi che la mia comprensione del codice e dei test sia sincronizzata.

Un collaboratore ritiene che questa duplicazione sia cattiva e mi ha raccomandato di rifattorizzare entrambe le parti per qualcosa di simile:

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 passo out_dir + name sia dal lato della produzione che dal lato del test. Non scoprirò un errore nella mia comprensione di come + funzioni sulle stringhe e non accetterà 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 del genere.

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

    
posta Patrick Collins 13.04.2016 - 19:29
fonte

3 risposte

15

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

Se il contratto della classe è esattamente quel FooSaver genera a.foo_file e b.foo_file in una particolare posizione, allora dovresti testarlo direttamente, cioè duplicare le costanti nei test.

Se, tuttavia, il contratto della classe è che genera due file in un'area temporanea, i nomi di ciascuno dei quali sono facilmente modificabili, specialmente in fase di esecuzione, è necessario testare più genericamente, probabilmente usando costanti ricavate dal test.

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

È anche ragionevole trovare il contratto della classe che cambia durante il refactoring, ad esempio perché il suo livello di astrazione aumenti nel tempo. In un primo momento, si tratta dei due file specifici in una particolare posizione temporanea, ma nel tempo è possibile che sia necessaria un'ulteriore astrazione. In questo momento, modifica i test per mantenerli sincronizzati con il contratto della classe. Non c'è bisogno di costruire il contratto della classe fin da subito solo perché lo stai testando (YAGNI).

Quando il contratto di una classe non è ben definito, la sua verifica può farci mettere in dubbio la natura della classe, ma lo faremmo. Direi che non dovresti aggiornare il contratto della classe solo perché lo stai testando; è necessario aggiornare il contratto della classe per altri motivi, ad esempio, è un'astrazione debole per il dominio e, in caso contrario, verificarlo così com'è.

    
risposta data 13.04.2016 - 19:53
fonte
4

Ciò che @Erik ha suggerito - in termini di accertamento che tu sia chiaro su cosa stia testando - dovrebbe certamente essere il tuo primo punto di considerazione.

Ma se la tua decisione ti porta alla direzione di scomporre le costanti, lascia la parte interessante della tua domanda (parafrasi) "Perché dovrei scambiare le costanti duplicanti per la duplicazione del codice?". (In riferimento a dove parli di "duplicat [ing] il punto out_dir + nome".)

Credo che (i commenti di modulo Erik) la maggior parte delle situazioni fanno trarranno vantaggio dalla rimozione di costanti duplicate. Ma devi farlo in un modo che faccia non codice duplicato. Nel tuo esempio particolare, questo è facile. Invece di gestire 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 della stringa:

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 stai "collegando" un nome di file foglia:

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

Cioè, selezionando in modo più accurato gli elementi del linguaggio, è possibile evitare automaticamente la duplicazione del codice. (Non sempre, ma molto spesso nella mia esperienza.)

    
risposta data 14.04.2016 - 21:46
fonte
1

Sono d'accordo con la risposta di Erik Eidt, ma c'è una terza opzione: estrai la costante nel test, quindi il test passa anche se cambi il valore della costante nel codice di produzione.

(vedi stub di una costante 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"))

E quando faccio cose di questo genere, di solito mi assicuro di fare un test di integrità dove eseguo i test senza l'istruzione with e mi assicuro di vedere "'a.foo_file'! = 'unique_to_your_test_a'", quindi inserire l'istruzione with torna nel test in modo che passi di nuovo.

    
risposta data 20.04.2016 - 08:45
fonte

Leggi altre domande sui tag