Questo test unitario è troppo stretto per l'implementazione?

4

Credo che l'esistenza di questo test unitario sia giustificata. Tuttavia, per me, questo test unitario sembra molto accoppiato all'implementazione del metodo, anche se non sono sicuro che possa essere migliorato.

class ItemManager {
  void save(Item item) {
    if (item.id == null)  
       create(item)
    else 
       update(item)
  }
}

   @Test
   public void save() throws Exception {
      ItemManager manager = spy(new ItemManager());
      doNothing().when(manager).create(any(Item.class));
      doNothing().when(manager).update(any(Item.class));

      Item item = new Item();
      manager.save(item);
      verify(manager, (times(1))).create(item);

      item.setId("id");
      manager.save(item);
      verify(manager, (times(1))).update(item);
}

Modifica:

Sembra che ci siano due problemi con questo test unitario, l'accoppiamento con l'implementazione e che il test copra due comportamenti. Sono d'accordo che i due comportamenti dovrebbero essere divisi in test separati e capire che l'accoppiamento è indesiderabile, ma non sono ancora sicuro di quale sarebbe il modo appropriato per testarlo.

Sembra che il modo corretto di testare il comportamento di save sia di verificare che si traduca nelle chiamate previste del mio livello di persistenza. Tuttavia, questo sembra molto ridondante con i test unitari dei metodi create e update .

La ridondanza nel test sottostante è appropriata? Qualche suggerimento per migliorare?

 Dao mockDao;

    @Test
    public void create() {
      manager.create(new Item()); 

      verify(mockDao).insert("expected params");
    }

    @Test
    public void update() {
      Item item = new Item();
      item.setId("id");
      manager.update(item); 

      verify(mockDao).update("expected params");
    }

    @Test
    public void saveWithoutId() {
      manager.save(new Item()); 

      verify(mockDao).insert("expected params");
    }

    @Test
    public void saveWithId() {
      Item item = new Item();
      item.setId("id");
      manager.save(item); 

      verify(mockDao).update("expected params");
    }
    
posta user39846 24.05.2017 - 19:03
fonte

3 risposte

2

Vorrei (anche) rispondere: sì.

UnitTest verifica il comportamento osservabile pubblico di un'unità. comportamento osservabile pubblico menas quali valori di ritorno produce e come comunica con le sue dipendenze .

Il tuo test verifica quale dei suoi metodi interni chiama.

Molto probabilmente la tua classe ItemManager comunica con un livello di persistenza. Un buon test dovrebbe verificare la comunicazione tra il tuo codice sotto test (taglio) e questo livello di persistenza:

Un'altra cosa di UnitTest è: ogni metodo in un Test delle unità ha verificato un singe expectation .

Il tuo test verifica due aspettative contemporaneamente per due diversi input.

combinando entrambi gli aspetti il tuo test dovrebbe assomigliare a questo:

   @Mock
   private Connection databaseConnection;
   @Mock
   private PreparedStatement preparedStatement;

   private  Item item = new Item();
   @Before
   public vod setup(){
      doReturn(preparedStatement).when(databaseConnection).createPreparedStatement(anyString());
   }
   @Test
   public void saveItenWithoutIdInserts() throws Exception {
      ItemManager manager = new ItemManager(databaseConnection);

      manager.save(item);
      verify(databaseConnection).createPreparedStatement(ItemManager.INSERT_SQL);
   }
   @Test
   public void saveItenWithIdUpdates() throws Exception {
      ItemManager manager = new ItemManager(databaseConnection);
      item.setId("id");
      item.setName("some name");      
      manager.save(item);
      verify(databaseConnection).createPreparedStatement(ItemManager.UPDATE_SQL);
      verify(preparedStatement).setString(ItemManager.INDEX_OF_ID_IN_UPDATE_SQL,"id");
   }

   @Test
   public void saveCopiesValuesFromItemToPreparedStatement() throws Exception {
      ItemManager manager = new ItemManager(databaseConnection);
      item.setName("some name");      
      manager.save(item);

      verify(preparedStatement).setString(ItemManager.INDEX_OF_NAME_IN_SQL,"some name");
   }

I'm concerned that unit tests of my manager.create and manager.update methods would be redundant with saveItenWithoutIdInserts and saveItenWithIdUpdates. I've edited my question to reflect this, any thoughts? – user39846

La tua domanda aggiornata ha in effetti test ridondanti . Ma la ragione è che hai comportamento ridondante .

Dovresti prendere una decisione nel modo in cui desideri che i tuoi client utilizzino questo codice: vuoi che chiamino esplicitamente update o insert o semplicemente chiami save ?

Se il secondo update e insert sono non parte dell'interfaccia pubblica e non dovrebbero essere testati esplicitamente. Inoltre, non dovrebbero avere visibilità public .

Ma se vuoi che i client chiamino update e insert sulla loro decisione, hai bisogno di questo test "ridondante" almeno come documentazione su come i client possono usare il tuo codice . Inoltre, dovresti aggiungere altri due test per ciò che accade quando chiamano update con un ID mancante o insert con un ID inaspettatamente.

    
risposta data 24.05.2017 - 21:52
fonte
0

Perché scrivi test unitari in primo luogo? Creo un'unità. L'unità ha speranze ha una specifica. Scrivo test unitari che confermano che la mia unità soddisfa le sue specifiche. Se i miei test unitari sono buoni, allora puoi fare affidamento sul fatto che la mia unità funzioni secondo le specifiche fino a quando i test passano.

Se cambio la mia unità e la rompo, o cambio la mia unità perché le specifiche sono cambiate, allora (a) i miei test unitari si interromperanno e (b) il tuo codice che si basa sulla mia unità che lavora secondo le specifiche che tu sapere si romperà. La cosa bella dei test unitari è che conosci immediatamente il colpevole: il mio codice non funziona secondo le specifiche che ti aspetti. Quindi non perdi tempo a cercare un bug nel tuo codice che non esiste. È carino per te. (Abbiamo anche informazioni affidabili su quali unità funzionano in base alle loro specifiche previste, il che fa sentire tutti molto meglio).

Ora i test unitari ovviamente non mi aiutano se introduco un bug nella mia unità, solo nella misura in cui mi dicono che c'è un bug, ma non dove si trova il bug. Proprio come hai ottenuto aiuto dicendo che "il bug è dovuto all'unità guasta di Gnasher", mi piacerebbe anche un aiuto qui. E posso ottenere questo aiuto scrivendo test per il mio codice interno.

Considera che invece di scrivere "creare" e "aggiornare" me stesso, avrei potuto fare affidamento su qualcun altro che scriveva un'unità che li implementava. In quel caso, ci sarebbero dei test unitari. Ora non dovrebbe importare che non ho scritto un'unità separata, voglio ancora questi test.

Si può dire con ragione che questi non sono test unit perché non testano il comportamento osservabile ma qualcosa di interno. Eppure, sono test utili. E vorrei eseguirli contemporaneamente ai miei test unitari, cioè dopo ogni cambiamento nell'unità. Quindi dovrebbero andare con i test unitari.

Probabilmente è opportuno contrassegnare nel codice sorgente per i tuoi test unitari quali test stanno testando il comportamento osservabile (perché altri sviluppatori considereranno questi test se non sono sicuri che il comportamento osservabile che hai provato ad implementare sia il comportamento osservabile che tutti sono d'accordo), e quali test sono interni (di nessun interesse per gli utenti della vostra unità).

Un argomento potrebbe essere che i test interni potrebbero fallire, ma finché i test delle unità del comportamento osservabili vengono eseguiti, tutto va bene e questi fallimenti dei test interni perdono tempo. Non sarei d'accordo. Se i miei test interni falliscono, e il mio test unitario passa, allora il codice nella mia unità è mal compreso, il che è sempre un problema, oppure i miei test unitari non vanno bene.

    
risposta data 27.05.2017 - 21:49
fonte
-1

IMO, sì. I test unitari di questo tipo mi fanno sempre errori, non è il momento di verificare che l'elemento sia stato effettivamente salvato con successo, non che sia stato chiamato un particolare metodo?

Meglio testare il requisito molto più utile e generalmente applicabile che create() genera un'eccezione su un id non nullo e allo stesso modo che update() genera un'eccezione su un ID nullo.

Quindi, questo test di save () non è necessario. Oppure, se insisti, può essere semplificato a qualcosa di simile

Item item = new Item();
manager.save(item);
item.setId("id");
manager.save(item);
// no exceptions were thrown, we pass
    
risposta data 24.05.2017 - 20:03
fonte

Leggi altre domande sui tag