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:
- Il test dovrebbe descrivere il comportamento completo dell'unità sotto test.
- 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.