Come gestire i test unitari con i collaboratori e gli effetti collaterali?

2

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

    
posta kork 28.07.2016 - 12:03
fonte

1 risposta

1

Al tuo primo punto elenco e all'ultimo punto elenco sono più o meno la stessa cosa e mi occuperò di questi ultimi.

Al secondo punto, non è compito di questo test rilevare gli effetti collaterali del metodo del negozio di SomeDao. Questo test sta testando SomeServiceImpl, non Some SomeDao, non importa che cosa abbia SomeDao. Un altro dispositivo di prova dovrebbe testarlo. L'unica cosa che questo test doveva verificare era se si chiamava il negozio. Altrimenti questo è un test di integrazione.

Terzo punto - sì, questo è uno svantaggio del test unitario, ma più un sistema è complicato e più considerazioni dobbiamo fare. Ma questa non è una brutta cosa. Ci costringe a pensare a come un pezzo di codice interagisce con altri pezzi. Ci dà un'idea precisa del punto di interazione tra di loro e valutiamo come ci aspettiamo che le cose funzionino.

Ora la vera carne. Mentre non so che considererei le aspettative "ripetendo" il codice, ma questa è probabilmente la più grande critica legittima ai test unitari e alle strutture di derisione; la conoscenza che spesso i test devono avere sul metodo specifico che stanno testando.

Ma non è la fine del mondo. Sì, se cambi completamente il modo in cui accedi ai dati, il test dell'unità dovrà cambiare drasticamente, ma è improbabile. Se il tuo DAO ha un'interfaccia adeguata e rispetta il principio aperto / chiuso, questo è uno scenario improbabile. Quindi probabilmente otterrai solo regressioni o dovrai scrivere più test per nuove funzionalità piuttosto che dover refactoring per funzionalità modificate.

Quindi direi semplicemente che le dipendenze del servizio sono ben astratte, fai del tuo meglio per renderle chiuse per la modifica (+ apri per l'estensione) e ti troverai nella forma più adatta che puoi.

    
risposta data 02.08.2016 - 00:47
fonte

Leggi altre domande sui tag