Cosa succede con i test dei metodi quando quel metodo diventa privato dopo la riprogettazione in TDD?


29

Diciamo che inizio a sviluppare un gioco di ruolo con personaggi che attaccano altri personaggi e quel genere di cose.

Applicando TDD, realizzo alcuni casi di test per testare la logica all'interno del Character.receiveAttack(Int)metodo. Qualcosa come questo:

@Test
fun healthIsReducedWhenCharacterIsAttacked() {
    val c = Character(100) //arg is the health
    c.receiveAttack(50) //arg is the suffered attack damage
    assertThat(c.health, is(50));
}

Supponiamo di avere 10 metodi di prova receiveAttack. Ora aggiungo un metodo Character.attack(Character)(che chiama receiveAttackmetodo) e dopo alcuni cicli TDD che lo testano, prendo una decisione: Character.receiveAttack(Int)dovrebbe essere private.

Cosa succede con i precedenti 10 casi di test? Devo eliminarli? Devo mantenere il metodo public(non credo)?

Questa domanda non riguarda come testare metodi privati ​​ma come gestirli dopo una riprogettazione quando si applica TDD



10
Se è privato non lo testate, è così facile. Rimuovi e fai ballare
kayess il

6
Probabilmente sto andando contro il grano qui. Ma generalmente evito i metodi privati ​​a tutti i costi. Preferisco più test che meno test. So cosa pensano le persone "Cosa, quindi non hai mai alcun tipo di funzionalità che non vuoi esporre al consumatore?". Sì, ne ho molti che non voglio esporre. Invece, quando ho un metodo privato, invece lo refactoring nella sua classe e uso detta classe dalla classe originale. La nuova classe può essere contrassegnata come internalo equivalente della tua lingua per impedirne comunque l'esposizione. In effetti la risposta di Kevin Cline è questo tipo di approccio.
user9993,

3
@ user9993 sembri averlo al contrario. Se è importante che tu abbia più test, l'unico modo per assicurarti di non aver perso nulla di importante è eseguire l'analisi della copertura. E per gli strumenti di copertura non importa affatto se il metodo è privato o pubblico o qualsiasi altra cosa. Spero che rendere pubbliche le cose in qualche modo compensi la mancanza di analisi della copertura dà un falso senso di sicurezza, temo
moscerino

2
@gnat Ma non ho mai detto nulla sul "non avere copertura"? Il mio commento su "Preferisco più test che meno test" avrebbe dovuto renderlo ovvio. Non sono sicuro di cosa stai ottenendo esattamente, ovviamente testerò anche il codice che ho estratto. Questo è il punto.
user9993,

Risposte:


52

In TDD, i test servono come documentazione eseguibile del progetto. Il tuo design è cambiato, quindi ovviamente anche la tua documentazione!

Si noti che, in TDD, l'unico modo in cui il attackmetodo avrebbe potuto apparire è il risultato del superamento di un test fallito. Il che significa che attackviene testato da qualche altro test. Ciò significa che indirettamente receiveAttack è coperto dai attacktest di. Idealmente, qualsiasi modifica a receiveAttackdovrebbe interrompere almeno uno dei attacktest.

E se non lo fa, allora c'è funzionalità receiveAttackche non è più necessaria e non dovrebbe più esistere!

Quindi, poiché receiveAttackè già stato testato attack, non importa se si mantengono o meno i test. Se il framework di test semplifica il test di metodi privati ​​e se si decide di testare metodi privati, è possibile mantenerli. Ma puoi anche eliminarli senza perdere la copertura e la sicurezza del test.


14
Questa è una buona risposta, salvo "Se il framework di test semplifica il test di metodi privati ​​e se decidi di testare metodi privati, puoi mantenerli". I metodi privati ​​sono dettagli di implementazione e non dovrebbero mai, mai essere testati direttamente.
David Arno,

20
@DavidArno: non sono d'accordo, per questo motivo gli interni di un modulo non dovrebbero mai essere testati. Tuttavia, gli interni di un modulo possono essere di grande complessità e quindi avere test unitari per ogni singola funzionalità interna può essere prezioso. I test unitari sono usati per verificare gli invarianti di un pezzo di funzionalità, se un metodo privato ha invarianti (pre-condizioni / post-condizioni), allora un test unitario può essere prezioso.
Matthieu M.,

8
" con questo ragionamento gli interni di un modulo non dovrebbero mai essere testati ". Questi interni non dovrebbero mai essere testati direttamente . Tutti i test dovrebbero testare solo le API pubbliche. Se un elemento interno non è raggiungibile tramite un'API pubblica, eliminalo perché non fa nulla.
David Arno,

28
@DavidArno In base a tale logica, se stai costruendo un eseguibile (piuttosto che una libreria), non dovresti avere affatto unit test. - "Le chiamate di funzione non fanno parte dell'API pubblica! Lo sono solo gli argomenti della riga di comando! Se una funzione interna del tuo programma non è raggiungibile tramite un argomento della riga di comando, eliminalo perché non fa nulla." - Sebbene le funzioni private non facciano parte dell'API pubblica della classe, fanno parte dell'API interna della classe. E mentre non è necessariamente necessario testare l'API interna di una classe, è possibile, usando la stessa logica per testare l'API interna di un eseguibile.
RM

7
@RM, se dovessi creare un eseguibile in modo non modulare, sarei costretto a scegliere tra i fragili test degli interni, o usando solo i test di integrazione usando l'eseguibile e l'I / O di runtime. Pertanto, secondo la mia logica attuale, piuttosto che la tua versione di Strawman, la creerei in modo modulare (ad esempio tramite un set di librerie). Le API pubbliche di tali moduli possono quindi essere testate in modo non fragile.
David Arno,

23

Se il metodo è abbastanza complesso da richiedere test, dovrebbe essere pubblico in alcune classi. Quindi fai il refactoring da:

public class X {
  private int complexity(...) {
    ...
  }
  public void somethingElse() {
    int c = complexity(...);
  }
}

a:

public class Complexity {
  public int calculate(...) {
    ...
  }
}

public class X {
  private Complexity complexity;
  public X(Complexity complexity) { // dependency injection happiness
    this.complexity = complexity;
  }

  public void something() {
    int c = complexity.calculate(...);
  }
}

Sposta il test corrente per X.complexity su ComplexityTest. Quindi scrivi X.qualcosa deridendo la complessità.

Nella mia esperienza, il refactoring verso classi più piccole e metodi più brevi offre enormi vantaggi. Sono più facili da capire, più facili da testare e finiscono per essere riutilizzati più di quanto ci si potrebbe aspettare.


La tua risposta spiega molto più chiaramente l'idea che stavo cercando di spiegare nel mio commento sulla domanda di OP. Bella risposta.
user9993,

3
Grazie per la tua risposta. In realtà, il metodo di ricezione è abbastanza semplice ( this.health = this.health - attackDamage). Forse estrarlo in un'altra classe è una soluzione ingegnerizzata, per il momento.
Héctor,

1
Questo è decisamente eccessivo per l'OP - vuole guidare fino al negozio, non volare sulla luna - ma una buona soluzione nel caso generale.

Se la funzione è così semplice, forse è troppo ingegnoso che in primo luogo è persino definita come una funzione.
David K,

1
potrebbe essere eccessivo oggi, ma tra 6 mesi, quando ci saranno molte modifiche a questo codice, i vantaggi saranno chiari. E in qualsiasi IDE decente in questi giorni sicuramente l'estrazione di un po 'di codice in una classe separata dovrebbe essere un paio di battute al massimo quasi una soluzione troppo ingegnerizzata considerando che nel binario di runtime si ridurrà comunque allo stesso modo.
Stephen Byrne,

6

Supponiamo che io abbia 10 metodi per testare il metodo receiveAttack. Ora aggiungo un metodo Character.attack (Character) (che chiama il metodo receiveAttack) e dopo alcuni cicli TDD che lo verificano, prendo una decisione: Character.receiveAttack (Int) dovrebbe essere privato.

Qualcosa da tenere a mente qui è che la decisione che stai prendendo è quella di rimuovere un metodo dall'API . Le cortesie di retrocompatibilità suggerirebbero

  1. Se non è necessario rimuoverlo, lasciarlo nell'API
  2. Se non è necessario rimuoverlo ancora , poi segnarlo come deprecato e, se possibile, documento quando alla fine della vita accadrà
  3. Se è necessario rimuoverlo, è necessario apportare una modifica sostanziale alla versione

I test vengono rimossi / o sostituiti quando l'API non supporta più il metodo. A quel punto, il metodo privato è un dettaglio di implementazione che dovresti essere in grado di rifattorizzare.

A quel punto, sei tornato alla domanda standard se la tua suite di test dovrebbe accedere direttamente alle implementazioni, piuttosto interagendo semplicemente attraverso l'API pubblica. Un metodo privato è qualcosa che dovremmo essere in grado di sostituire senza che la suite di test si frapponga . Quindi mi aspetto che la coppia di test scompaia, sia che si ritiri, sia che si sposti con l'implementazione in un componente testabile separatamente.


3
La deprecazione non è sempre una preoccupazione. Dalla domanda: "Diciamo che comincio a sviluppare ..." se il software non è stato ancora rilasciato, la deprecazione non è un problema. Inoltre: "un gioco di ruolo" implica che questa non è una libreria riutilizzabile, ma un software binario rivolto agli utenti finali. Mentre alcuni software per utenti finali dispongono di un'API pubblica (ad es. MS Office), la maggior parte no. Anche il software che ha un'API pubblica ha solo una porzione di esso esposti per plugin, scripting (ad esempio giochi con estensione LUA), o altre funzioni. Tuttavia, vale la pena sollevare l'idea per il caso generale descritto dall'OP.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.