In che modo TDD verifica che gli oggetti vengano aggiunti a una raccolta se la raccolta è privata?

8

Supponiamo che ho pianificato di scrivere una classe che ha funzionato in questo modo:

public class GameCharacter {
    private Collection<CharacterEffect> _collection;

    public void Add(CharacterEffect e) { ... }
    public void Remove(CharacterEffect e) { ... }
    public void Contains(CharacterEffect e) { ... }
}

Quando aggiunto un effetto fa qualcosa al personaggio e viene quindi aggiunto a _collection. Quando viene rimosso, l'effetto ripristina la modifica al personaggio e viene rimosso da _collection.

È facile verificare se l'effetto è stato applicato al personaggio, ma come faccio a verificare che l'effetto sia stato aggiunto a _collection?

Quale test potrei scrivere per iniziare a costruire questo corso. Potrei scrivere un test in cui Contains restituirebbe true per un determinato effetto in _collection, ma non posso organizzare un caso in cui quella funzione restituirebbe true perché non ho implementato il Aggiungi metodo necessario per posizionare elementi in _collection.

Ok, quindi dato che Contains dipende dall'avere Add funzionante, allora perché non provo a creare Aggiungi prima. Bene, per il mio primo test devo cercare di capire se l'effetto è stato aggiunto alla raccolta. Come potrei farlo? L'unico modo per vedere se un effetto è in _collection è con la funzione Contains .

L'unico modo in cui potrei pensare di testare questo sarebbe usare un FakeCollection che deride Add, Remove e Contains di una vera collezione, ma non voglio che _collection sia influenzato da fonti esterne. Non voglio aggiungere una funzione setEffects (effetti collezione) , perché non voglio che la classe abbia quella funzionalità. L'unica cosa che penso potrebbe funzionare è questa:

public class GameCharacter<C extends Collection> {
    private Collection<CharacterEffect> _collection;

    public GameCharacter() {
        _collection = new C<CharacterEffect>();
    }
}

Ma è solo sciocco farmi dichiarare quale tipo di strutture di dati private si trova su ogni dichiarazione del personaggio.

C'è un modo per testarlo senza rompere i principi di TDD, pur continuando a consentirmi di mantenere privata la mia collezione?

    
posta Joshua Harris 16.11.2012 - 22:19
fonte

5 risposte

12

La confusione che stai avendo qui è perché stai cercando di scrivere un test sui dettagli di implementazione, non sull'effetto desiderato. Pensare invece ai comportamenti desiderati della classe ti porterà probabilmente a un test migliore.

In questo esempio, mi sembra che tu abbia due comportamenti che vuoi testare:

  • Quando aggiunto un effetto fa qualcosa al personaggio
  • Quando viene rimosso, l'effetto ripristina la modifica al carattere

Non sono esattamente sicuro di quale sia l'effetto di un personaggio, ma ecco un test di esempio che fa un'ipotesi abbastanza ragionevole, e si spera che riesca a far valere il mio punto di vista anche se è un po 'fuori moda:

[Test]
public void AddAndRemovingEffectsAltersCharachterAppropriately()
{
    var originalStrength = _character.Strength;
    CharacterEffect strengthPotion = new StrengthPotion();
    _character.Add(strengthPotion);
    Assert.That(_character.Strength, Is.GreaterThan(originalStrength));

    //normally I'd make a second test for this, 
    //but the answer edit box is not a great IDE  :)
    _character.Remove(strengthPotion);
    Assert.That(_character.Strength, Is.EqualTo(originalStrength));
}

Avviso: a questo test non interessa affatto la raccolta. Non è ovvio leggendolo che una raccolta è necessaria. La collezione è solo un dettaglio di implementazione. È sotto l'avviso del test.

Nota a margine: i nomi "aggiungi" e "rimuovi" per i metodi fanno suggeriscono una raccolta e non comunicano realmente lo scopo pertinente del dominio dei metodi. Preferirei qualcosa come Apply o Consume su Add . O forse character.Inventory.Add() se stiamo parlando di attrezzature.

    
risposta data 17.11.2012 - 06:18
fonte
5

Sembra che tu capisca la parte incrementale di TDD, cioè implementare la più piccola funzionalità possibile alla volta. Tuttavia, ci sono casi come quello che stai affrontando che lo rendono estremamente difficile da fare. Nella tua situazione particolare, non mi preoccuperei di provare a implementarli separatamente e implementerei Add e Contains allo stesso tempo.

Il tipico ciclo TDD inizia con: Write your test e make it compile . Nel tuo caso, il tuo test sarebbe così:

var g = new GameCharacter();
var c = new CharacterEffect();
g.Add(c);
Assert.That(g.Contains(c));

Per rendere il tuo test compilato (passaggio 2), devi implementare sia Add che Contains , che ritengo sia perfettamente corretto. TDD dice che dovresti fare il minimo passo possibile e questo sembra essere il più piccolo che puoi fare. Non è necessario impazzire nell'implementare ogni metodo singolarmente o si rischia di colpire ripetutamente lo stesso muro.

    
risposta data 16.11.2012 - 22:47
fonte
4

It's easy to test if the effect was applied to the character, but how do I test that the effect was added to _collection?

Non dovresti.

Devi chiedere perché è importante che un articolo sia aggiunto alla collezione. "Beh, dovrebbe avere un certo effetto sul personaggio." Quindi prova per quell'effetto. Non c'è alcun valore nel test che una raccolta rudimentale funzioni come una raccolta.

Supponiamo che un giorno tu possa modificare il programma per creare effetti di carattere in un modo molto diverso. Se si verifica che la raccolta funzioni, il test diventa inutile quando si elimina la raccolta. Ma se esegui il test degli effetti di carattere, il test continuerà a funzionare con un meccanismo di codice diverso. Questo è come dovrebbe essere; devi verificare che il tuo programma funzioni correttamente, non che il tuo linguaggio di programmazione funzioni correttamente.

Dai un'occhiata a questa risposta, di Kent Beck, su cosa si dovrebbe testare:

Quanto sono profondi i test unitari?

I get paid for code that works, not for tests, so my philosophy is to test as little as possible to reach a given level of confidence...If I don't typically make a kind of mistake (like setting the wrong variables in a constructor), I don't test for it.

A meno che tu non abbia completamente familiarità con il linguaggio di programmazione che stai utilizzando o con una libreria che stai utilizzando, non ha senso testare che una raccolta funziona come una raccolta o che puoi creare una nuova oggetto, o che una variabile viene incrementata quando gli dici di incrementare.

Tutto quello che stai facendo in questi casi è assicurarti che il tuo linguaggio di programmazione funzioni come previsto, che generalmente non è una cosa che dovresti testare a meno che tu non sia quello che progetta la lingua.

    
risposta data 18.11.2012 - 03:09
fonte
0

Non dovresti testare le classi ma le interfacce dove per interfaccia, oltre al significato tipico, potresti anche capire le firme dei metodi pubblici di una classe. Le tue classi dovrebbero aderire ai contratti forti ; dal punto di vista del test (e dal POV di altri clienti) non dovresti preoccuparti di come questi contratti sono rispettati / implementati.

Nel tuo caso particolare c'è un problema con il tuo design di classe; il tuo metodo add fa due cose: in primo luogo modifica CharacterEffect e poi aggiunge quell'oggetto alla raccolta di effetti di carattere. Questa cattiva decisione sul design rende difficile il test!

Se vuoi veramente vedere un campo privato, puoi usare la riflessione, ma non è un modo ortodosso di fare le cose.

    
risposta data 16.11.2012 - 22:38
fonte
0

I tuoi test unitari devono essere implementati in una classe di amici.

Ciò ti consente di attingere liberamente a materiale privato e protetto ai fini del test dell'unità senza che deve farne parte dell'API esterna.

    
risposta data 17.11.2012 - 01:41
fonte