Siamo in un progetto che ha un bel po 'di codice e, mentre in generale abbiamo una buona copertura di test e le cose funzionano abbastanza bene, ho notato che i nostri test sono diventati sempre più complessi nel tempo fino a un punto in cui i test sono più complessi del codice che testano.
Come sfondo, stiamo usando Spock per testare e fare ampio uso delle funzioni di test di simulazione e di interazione, che potrebbero essere parte del problema. In generale, abbiamo optato per i test white-box, quindi il nostro codice di prova conosce principalmente il funzionamento interno delle classi sotto test (che potrebbe essere un altro fattore che contribuisce al problema).
Consentitemi di darvi un esempio semplificato di un test meno stellare su questo metodo:
class SomeServiceImpl {
SomeDao someDao;
public void store(SomeInputForm form) {
// some checks of the form here.
SomeEntity entity = new SomeEntity();
// copy data from form to entity here.
someDao.store(someEntity);
}
}
E il test:
SomeServiceImpl underTest
SomeDao someDao
def setup() {
underTest = new SomeServiceImpl()
someDao = Mock(SomeDao)
underTest.someDao = someDao
}
def "storing something yields that stuff ends up in the database"() {
setup:
def form = new SomeInputForm(... with some data ...)
SomeEntity result = null
when:
underTest.store( form )
then:
1 * someDao.store( { result = it } _ as SomeEntity )
// additional checks to verify that the entity has the same data as the form
result.someProperty == form
}
L'intenzione di questo test è di verificare che alcuni dati che arrivano tramite un modulo siano memorizzati all'interno del database. Prendiamo in giro il DAO, quindi non abbiamo bisogno di un vero DB qui. Mentre il test funziona sicuramente ha diversi problemi:
- Sa molto bene come funziona il servizio internamente. Non appena cambi l'implementazione del servizio, devi cambiare l'implementazione del test, anche se il risultato finale sarebbe lo stesso.
- È difficile rilevare l'effetto collaterale del metodo store (vedi il complicato codice per estrarre l'entità generata).
- Se hai più collaboratori dovrai fare un sacco di derisioni e impilarle tutte insieme.
- Il test ripete sostanzialmente il codice del servizio nella sezione
then
.
Attualmente sto sperimentando un modo più funzionale di cose in cui hai solo funzioni che ricevono input e producono output, ad es. così:
class SomeServiceImpl {
void verify(SomeInputForm form) {
// verify and throw exception if a problem is there
}
SomeEntity createEntityFromForm(SomeInputForm form) {
// do the conversion here
}
void store(SomeEntity entity) {
dao.store(entity);
}
}
Mentre questo risolve parte del problema di test, perché ora posso testare i metodi verify
e createEntityFromForm
senza alcun collaboratore e senza conoscere l'effettiva implementazione, ci deve ancora essere un luogo in cui sono chiamati questi tre metodi nell'ordine corretto:
service.verify(form);
Entity en = service.createEntityFromForm(form);
service.store(form);
Che effettivamente sposta il problema da qualche altra parte (e introduce un sacco di margine di errore in quanto è necessario chiamare i metodi nell'ordine corretto ora).
Quindi mi chiedo se c'è un modo migliore di organizzare il codice in modo diverso per rendere i test meno fragili e meno whitebox pur rilevando gli effetti collaterali (o anche un modo in cui non avresti bisogno di rilevare l'effetto collaterale, ma comunque sapere che le cose sono correttamente salvate nel database).