2012-09-17 11 views
6

Nel nostro recente progetto, Sonar si è lamentato di una copertura di test debole. Abbiamo notato che non considerava i test di integrazione di default. Oltre al fatto che è possibile configurare Sonar, quindi li considererà (plug-in JaCoCo), stavamo discutendo la domanda nel nostro team se c'è davvero bisogno di scrivere Test unitari, quando si coprono tutti i livelli del servizio e del database con test di integrazione Comunque.Devo scrivere Test unitari per le operazioni CRUD quando ho già test di integrazione?

Ciò che intendo con i test di integrazione è che tutti i nostri test vengono eseguiti su un'istanza Oracle dedicata dello stesso tipo utilizzata nella produzione. Non prendiamo in giro nulla. Se un servizio dipende da un altro servizio, utilizziamo il servizio reale. I dati di cui abbiamo bisogno prima di eseguire un test, costruiamo attraverso alcune classi di fabbrica che utilizzano i nostri servizi/repository (DAO).

Quindi dal mio punto di vista - scrivere test di integrazione per semplici operazioni CRUD, specialmente quando si usano framework come Spring Data/Hibernate, non è un grande sforzo. A volte è anche più facile, perché non pensi a cosa e come deridere.

Quindi perché dovrei scrivere i test di unità per le mie operazioni CRUD che sono meno affidabili come i test di integrazione che posso scrivere?

L'unico punto che vedo è che i test di integrazione richiederanno più tempo per essere eseguiti, più grande sarà il progetto. Quindi non vuoi eseguirli tutti prima del check-in. Ma non sono così sicuro se questo è così grave, se si ha un ambiente CI con Jenkins/Hudson che farà il lavoro.

Quindi, qualsiasi opinione o suggerimento è molto apprezzato!

risposta

10

Se la maggior parte dei tuoi servizi passa semplicemente ai tuoi daos, e i tuoi dao fanno poco, ma invocano i metodi su Spring HibernateTemplate o JdbcTemplate, allora sei corretto che i test di unità non provano in realtà nulla che i tuoi test di integrazione già dimostrino. Tuttavia, i test unitari in atto sono preziosi per tutti i soliti motivi.

Dal test di unità di prova solo singole classi, eseguire in memoria senza disco o rete di accesso, e mai realmente testare più classi che lavorano insieme, che normalmente vanno in questo modo: i test di unità

  • servizio deridere il Tao.
  • I test dell'unità Dao simulano il driver del database (o il modello di primavera) o utilizzano un database incorporato (super facile in Spring 3).

Per unit test del servizio che passa solo attraverso il dao, si può deridere in questo modo:

@Before 
public void setUp() { 
    service = new EventServiceImpl(); 
    dao = mock(EventDao.class); 
    service.EventDao = dao; 
} 

@Test 
public void creationDelegatesToDao() { 
    service.createEvent(sampleEvent); 
    verify(dao).createEvent(sampleEvent); 
} 

@Test(expected=EventExistsException.class) 
public void creationPropagatesExistExceptions() { 
    doThrow(new EventExistsException()).when(dao).createEvent(sampleEvent); 
    service.createEvent(sampleEvent); 
} 

@Test 
public void updatesDelegateToDao() { 
    service.updateEvent(sampleEvent); 
    verify(dao).updateEvent(sampleEvent); 
} 

@Test 
public void findingDelgatesToDao() { 
    when(dao.findEventById(7)).thenReturn(sampleEvent); 
    assertThat(service.findEventById(7), equalTo(sampleEvent)); 

    service.findEvents("Alice", 1, 5); 
    verify(dao).findEventsByName("Alice", 1, 5); 

    service.findEvents(null, 10, 50); 
    verify(dao).findAllEvents(10, 50); 
} 

@Test 
public void deletionDelegatesToDao() { 
    service.deleteEvent(sampleEvent); 
    verify(dao).deleteEvent(sampleEvent); 
} 

Ma è davvero una buona idea? Queste asserzioni di Mockito affermano che è stato chiamato un metodo dao, non che ha fatto ciò che ci si aspettava! Otterrete i vostri numeri di copertura ma vincerete più o meno i vostri test a un'implementazione del dao. Ahia.

Ora questo esempio presuppone che il servizio non abbia una vera logica aziendale. Normalmente i servizi avranno una logica di business in aggiunta alle chiamate dao, e sicuramente dovrai testarli.

Ora, per i test di unità daos, mi piace utilizzare un database incorporato.

private EmbeddedDatabase database; 
private EventDaoJdbcImpl eventDao = new EventDaoJdbcImpl(); 

@Before 
public void setUp() { 
    database = new EmbeddedDatabaseBuilder() 
      .setType(EmbeddedDatabaseType.H2) 
      .addScript("schema.sql") 
      .addScript("init.sql") 
      .build(); 
    eventDao.jdbcTemplate = new JdbcTemplate(database); 
} 

@Test 
public void creatingIncrementsSize() { 
    Event e = new Event(9, "Company Softball Game"); 

    int initialCount = eventDao.findNumberOfEvents(); 
    eventDao.createEvent(e); 
    assertThat(eventDao.findNumberOfEvents(), is(initialCount + 1)); 
} 

@Test 
public void deletingDecrementsSize() { 
    Event e = new Event(1, "Poker Night"); 

    int initialCount = eventDao.findNumberOfEvents(); 
    eventDao.deleteEvent(e); 
    assertThat(eventDao.findNumberOfEvents(), is(initialCount - 1)); 
} 

@Test 
public void createdEventCanBeFound() { 
    eventDao.createEvent(new Event(9, "Company Softball Game")); 
    Event e = eventDao.findEventById(9); 
    assertThat(e.getId(), is(9)); 
    assertThat(e.getName(), is("Company Softball Game")); 
} 

@Test 
public void updatesToCreatedEventCanBeRead() { 
    eventDao.createEvent(new Event(9, "Company Softball Game")); 
    Event e = eventDao.findEventById(9); 
    e.setName("Cricket Game"); 
    eventDao.updateEvent(e); 
    e = eventDao.findEventById(9); 
    assertThat(e.getId(), is(9)); 
    assertThat(e.getName(), is("Cricket Game")); 
} 

@Test(expected=EventExistsException.class) 
public void creatingDuplicateEventThrowsException() { 
    eventDao.createEvent(new Event(1, "Id1WasAlreadyUsed")); 
} 

@Test(expected=NoSuchEventException.class) 
public void updatingNonExistentEventThrowsException() { 
    eventDao.updateEvent(new Event(1000, "Unknown")); 
} 

@Test(expected=NoSuchEventException.class) 
public void deletingNonExistentEventThrowsException() { 
    eventDao.deleteEvent(new Event(1000, "Unknown")); 
} 

@Test(expected=NoSuchEventException.class) 
public void findingNonExistentEventThrowsException() { 
    eventDao.findEventById(1000); 
} 

@Test 
public void countOfInitialDataSetIsAsExpected() { 
    assertThat(eventDao.findNumberOfEvents(), is(8)); 
} 

Lo chiamo ancora un test di unità anche se molte persone potrebbero chiamarlo un test di integrazione.Il database incorporato risiede in memoria e viene richiamato e rimosso quando vengono eseguiti i test. Ma questo si basa sul fatto che il database incorporato ha lo stesso aspetto del database di produzione. Sarà così? Altrimenti, tutto quel lavoro sarebbe stato piuttosto inutile. Se è così, allora, come dici tu, questi test stanno facendo qualcosa di diverso rispetto ai test di integrazione. Ma posso eseguirli su richiesta con mvn test e ho fiducia nel refactoring.

Pertanto, scrivo comunque questi test unitari e raggiungo i miei obiettivi di copertura. Quando scrivo test di integrazione, asserisco che una richiesta HTTP restituisce la risposta HTTP prevista. Sì, si susseguono i test unitari, ma ciao, quando pratichi TDD hai comunque quei test unitari scritti prima della tua effettiva implementazione dao.

Se si scrivono unit test dopo il dao, ovviamente non sono divertenti da scrivere. La letteratura TDD è piena di avvertimenti su come scrivere test dopo che il tuo codice sembra funzionare e nessuno vuole farlo.

TL; DR: i test di integrazione precedono i test dell'unità e in tal senso i test di unità non aggiungono valore di test reale. Tuttavia, quando si dispone di una suite di test unitaria ad alta copertura, si ha fiducia nel refactoring. Ma ovviamente se il dao sta banalmente chiamando il modello di accesso ai dati di Spring, allora potresti non essere il refactoring. Ma non lo sai mai. E, infine, se i test delle unità vengono scritti prima in stile TDD, li avrete comunque.

+0

Ray, grazie mille le tue spiegazioni dettagliate ed esempi! Credo che attenersimo all'approccio per la scrittura dei test di integrazione. Andrei anche così lontano per dire che i test unitari per i servizi CRUD di un'applicazione web sono carini, ma ciò che conta davvero sono i test di integrazione. e perché non farlo nel modo TDD, ma scrivere test di integrazione invece di test unitari prima di implementare il servizio? – fischermatte

+0

Questo andrà bene; la polizia del test unitario non busserà alla tua porta. È vero che i test di integrazione copriranno i test unitari.Personalmente mi godo la sensazione di avere l'unità testata lì (fiducia nel refactoring, una sensazione di completezza e tutto il resto) ma sono solo io. Se sei felice di ottenere la tua copertura attraverso test di integrazione rispetto ai test unitari, e i tuoi test di integrazione sono rapidi test di integrazione generati dagli sviluppatori in opposizione ai test di produzione-integrazione-accettazione-produzione generati dal QA, quindi interessanti. :) –

1

È davvero necessario testare singolarmente ciascun livello se si prevede di far sì che i livelli siano esposti ad altri componenti fuori dal progetto. Per un'app Web, l'unico modo in cui il livello del repository può essere richiamato è dal livello di servizi e l'unico modo in cui il livello di servizio può essere richiamato è il livello controller. In questo modo i test possono iniziare e terminare al livello controller. Per le attività in background, queste sono invocate nel livello di servizio, quindi è necessario testarle qui.

Testare con un vero database è piuttosto veloce in questi giorni, quindi non rallenta troppo i test, se progettate il vostro setup/abbattete bene. Tuttavia, se ci sono altre dipendenze che potrebbero essere lente o problematiche, queste dovrebbero essere derise/stubbate.

Questo approccio vi darà:

  • buona copertura
  • realistici Test
  • minimo sforzo
  • quantità minima di refectoring sforzo

Tuttavia, il test strati in isolamento consente al tuo team di lavorare in modo più concorrente, in modo che un dev possa fare il repository e un altro possa fare il servizio Una sola funzionalità, e produce un lavoro indipendente testato.

Ci sarà sempre una doppia copertura quando i test di selenio/funzionali sono incorporati in quanto non si può fare affidamento solo su questi perché sono troppo lenti per essere eseguiti. Tuttavia, i test funzionali non devono necessariamente coprire tutto il codice, la funzionalità di base da sola può essere sufficiente, tanto più che il codice è stato coperto da test di unità/integrazione.

Problemi correlati