Devo scrivere test duplicati per i metodi "setDate" e "isValidDate"?

2

Diciamo che ho un metodo chiamato setDate . Ho anche un altro metodo chiamato isValidDate per vedere se una stringa è una stringa di data valida.

Per comodità, setDate utilizza isValidDate internamente come mezzo di convalida, quindi lo sviluppatore non deve eseguire manualmente la convalida. Ma isValidDate è usato anche in altri metodi. Inoltre genererà un'eccezione quando la stringa passata non è una stringa di data valida.

Ho dei test unitari per isValidDate per accertarmi che la logica funzioni correttamente. Ho anche alcuni test unitari per setDate , ma nessuno di essi è sovrapposto a isValidDate test.

Ciò significa che se qualcuno rimuove accidentalmente la chiamata a isValidDate dal metodo setDate , passeranno tutti i test, così come tutti i test per il metodo isValidDate , ma in effetti setDate ora può impostare non valido date.

Ho un difetto di progettazione nella strutturazione del mio codice, o dovrei semplicemente scrivere test duplicati per entrambi i metodi se voglio un po 'di affidabilità in più?

    
posta 53777A 24.10.2016 - 20:39
fonte

2 risposte

7

Suppongo che setDate() assomigli a qualcosa del tipo:

public void setDate(Date d) {
  if (!isValidDate(d))
    throw SomeException(...);
  this.date = d;
}

public Date getDate() { return this.date; }

Sto anche includendo un getDate() dato che i test per l'impostazione e il recupero non possono essere separati.

Ci sono due approcci per gestire la duplicazione del test.

Verifica solo ciò che fa il metodo direttamente.

Ci sono due scuole per quanto riguarda i test unitari:

  1. Il test dovrebbe descrivere il comportamento completo dell'unità sotto test.
  2. Il test dovrebbe coprire solo il valore aggiunto dell'unità in prova e ignorare il lavoro svolto da parti esterne.

Utilizzando il secondo approccio, cosa fa setDate() ? Per le date non valide (determinate con metodi esterni che non verranno testati qui), genera un'eccezione. Altrimenti, possiamo recuperare la stessa data con getDate() .

Questo ci dà esattamente due casi di test. In uno schizzo:

void test_setDate__throwsOnInvalidDates() {
  MyObject obj = new MyObject();
  Date invalidDate = ...;

  try {
    obj.setDate(invalidDate);
    assert(false, "setDate() did not reject the invalid date");
  } catch (SomeException e) {
    assert(true);
  }
}

void test_getDate__canRetrieveValuesFromSetDate() {
  MyObject obj = new MyObject();
  Date d = new Date(...);

  obj.setDate(d);

  assertEquals(obj.getDate(), d);
}

Genera una volta i dati del test, utilizza entrambi i test.

Memorizzando un elenco di date valide e non valide, possiamo testare sia isValidDate() che setDate() senza ripetizione notevole. Come puoi parametrizzare i tuoi test per usare un elenco di casi dipende dal tuo framework, qui inserirò il ciclo all'interno di un test:

Date[] validDates = { ... };
Date[] invalidDates = { ... };

void test_isValidDate__acceptsValidDates() {
  for (Date d : validDates)
    assertTrue(isValidDate(d), d.toString());
}

void test_isValidDate__rejectsInvalidDates() {
  for (Date d : invalidDates)
    assertTrue(!sValidDate(d), d.toString());
}

...

void test_getDate__canRetrieveValuesFromSetDate() {
  for (Date d : validDates) {
    MyObject obj = new MyObject();

    obj.setDate(d);

    assertEquals(obj.getDate(), d);
  }
}

void test_setDate__throwsOnInvalidDates() {
  for (Date d : invalidDates) {
    MyObject obj = ...;
    try {
      obj.setDate(d);
      assertTrue(false, ...);
    } catch (SomeException e) {
      assertTrue(true);
    }
  }
}

In pratica, questi dovrebbero essere casi di test separati invece di loop all'interno di un singolo caso di test, in modo che un test in errore non impedisca il test delle altre date - più dati in cui le date non riescono o riescono possono rendere il debug molto più facile.

Sebbene questo approccio sia immune dal refactoring, fa sì che i test siano più lunghi (un problema per test suite molto grandi) e crea il problema di generare i dati necessari.

Personalmente preferisco il primo approccio - solo testando la funzione aggiunta immediatamente con un metodo. Ciò aiuta a mantenere piccole e significative le suite di test. Ma se ritieni che non sia sufficiente, o se ti aspetti che un programmatore rimuova incurantemente un controllo di convalida, è probabile che la generazione di un elenco riutilizzabile di dati di test sia probabilmente migliore. Ho usato entrambi gli approcci, ed entrambi funzionano bene.

    
risposta data 24.10.2016 - 21:22
fonte
3

I tuoi test non devono sovrapporsi. Se isValidDate è sufficientemente coperto dai test unitari e setDate dipende da isValidDate , allora tutto ciò che rompe isValidDate romperà anche setDate . Presumibilmente, il tuo unit test per isValidDate copre già questi problemi, e se non lo fanno, non è colpa di setDate o di uno dei suoi test, è un bug non corretto.

Il problema è ciò che accade se setDate viene mai refactored in modo che non dipenda più da isValidDate . Ora i tuoi test di unità esistenti non coprono più adeguatamente setDate . La persona che rifiuta setDate per rimuovere la dipendenza da isValidDate deve tenerne conto e aggiungere nuovi test unitari per coprire completamente setDate .

La tua regola empirica per la copertura del test dovrebbe sempre essere "scrivere test sufficienti in modo da essere sicuro che il metodo funzioni correttamente."

    
risposta data 24.10.2016 - 21:22
fonte

Leggi altre domande sui tag