Utilizzo di metodi non sottoposti a test all'interno di un test unitario per un metodo diverso?

2

Sto costruendo un generatore di Sudoku. Ho una classe di bordo con un numero di metodi:

public class Board {
    public Board() { /* Creates an empty board */ }
    public bool ValidateRow(int row) { /* Checks for errors in row */ }
    public bool ValidateColumn(int column) { /* Checks for errors in column */ }
    ...
}

Sto seguendo TDD e, come tale, ho una suite completa di test per tutti questi metodi. Vorrei aggiungere due nuovi metodi a questa classe:

public static Board GenerateFilled() { /* Creates a solved board /* }
public bool ValidateBoard() { /* Checks for any error on the board /* }

Sono alle prese con come scrivere i miei test per questi metodi. Il mio primo pensiero fu:

[TestMethod]
public void GenerateFilled_GeneratesAValidSolvedBoard() {
    var board = Board.GenerateFilled();
    Assert.IsTrue(board.ValidateBoard());
}

ma mi sono reso conto che ho scritto lo stesso test per ValidateBoard:

[TestMethod]
public void ValidateBoard_NoErrors_ReturnsTrue() {
    var board = Board.GenerateFilled();
    Assert.IsTrue(board.ValidateBoard());
}

Questo test si basa su GenerateFilled e ValidateBoard che funzionano correttamente, sebbene il metodo in prova cambi. Ho trovato i seguenti modi per evitare questo problema:

  1. Duplica la logica del metodo non sottoposto a test nel test. Usa quella logica per convalidare il mio metodo sotto test invece di chiamare l'altro metodo.
  2. Lascia il test GenerateFilled così com'è e utilizza i dati di esempio codificati per testare ValidateBoard invece di chiamare GenerateFilled.

Non sono un fan dell'opzione 1 perché renderà noiosi i test accurati se cambiano piccole modifiche alla logica nei metodi duplicati.

Non mi piace l'opzione 2, anche se si basa sul fatto che il mio caso di dati di esempio è generale di tutti i casi, che è meno probabile che sia vero più grande è il set di dati.

Suppongo che nel peggiore dei casi ciò causi solo errori in entrambi i metodi, causando il fallimento di entrambi i test, ma è un po 'un odore. Qualcuno si è imbattuto in uno scenario simile e ha trovato una soluzione migliore rispetto ai precedenti?

    
posta Shaun Hamman 02.02.2018 - 21:03
fonte

3 risposte

4

Durante la rotta TDD, non dovresti provare ad aggiungere due metodi pubblici contemporaneamente. Invece, sceglierne uno, e scorrere tra "testare un po ', codificare un po', refactoring" fino a quando la qualità dei metodi è abbastanza alta. Solo quando hai finito, vai al prossimo metodo pubblico. Se lo fai uno per uno, diventa piuttosto ovvio come costruire i test.

Per il caso illustrato, penso che sia probabilmente il più semplice iniziare con ValidateBoard prima. L'ordine conta qui, dal momento che quando scriverò un metodo come GenerateFilled , per cui presumo tu non sappia come apparirà esattamente la tavola prodotta, avrai bisogno di un validatore universale per la creazione test sufficientemente robusti contro una modifica anticipata della logica di generazione.

Quindi, per testare ValidateBoard , non hai praticamente altra scelta per fornire dati di test hardcoded o scrivere alcune logiche di generazione semplici per creare schede valide o non valide. La funzione GenerateFilled non è disponibile in quel momento e, anche se lo fosse, non produrrebbe dati di test specifici, come schede non valide o incomplete. Questi sono dati che dovrai testare una funzione come ValidateBoard completamente. Quindi, dopo alcuni cicli TDD, finirai per avere abbastanza fiducia in ValidateBoard per non contenere più errori gravi. Durante questo passaggio, ValidateBoard è esclusivamente l'oggetto in prova.

Quindi, quando inizi a scrivere test per GenerateFilled , puoi utilizzare il già collaudato metodo ValidateBoard nei tuoi test. In questo passaggio, GenerateFilled è l'oggetto in prova e dovresti nominare il test di conseguenza, come GenerateFilled_GeneratesAValidSolvedBoard , e non ValidateBoard_NoErrors_ReturnsTrue . Replicare nuovamente la logica di ValidateBoard nel test, solo per rendere il test "più isolato", non ti porta alcun vantaggio reale, solo il codice duplicato.

Se la logica di ValidateBoard è duplicata nei test, IMHO ha senso solo se

  • c'è un'implementazione breve, semplice (ma forse lenta), che si può usare per i test. Dovrebbe essere così semplice che il rischio di avere un bug è basso.

  • la funzione "reale" ValidateBoard sarà una versione molto complessa (ad esempio a causa dell'ottimizzazione), ma anche più incline agli errori a causa della maggiore complessità.

Se incontri questo caso, la semplice implementazione può essere eseguita nel codice di test e utilizzata per testare GenerateFilled e ValidateBoard entrambi.

    
risposta data 02.02.2018 - 23:39
fonte
1

I'm following TDD, and as such have a full suite of tests for all these methods

C'è un conflitto in questa affermazione. O hai adottato un approccio TDD e hai una serie di test e del codice refactored che consente di superare tali test, oppure hai seguito ciecamente l'approccio di test "metodo delle unità significa" e hai pensato a un metodo, quindi hai scritto dei test per quel metodo .

TDD non si tratta di "identificare la necessità di un metodo, pensare a un test per questo ...". Si tratta di "pensare a un test, fallire, farlo passare, refactoring". Se quel test significa modificare un metodo esistente, crearne uno nuovo, mettere insieme 20 metodi o eseguire l'intera app all'interno di un ambiente di unità non fa differenza.

Quindi piuttosto che dire:

I would like to add two new methods to this class:

invece, affermalo in modo TDD:

I want to create a solved board

I want to validate the whole board

Ora hai due requisiti e puoi ottenere codice di scrittura per farli, uno alla volta, superare una serie di test. È molto probabile che dovrai scrivere GenerateFilled() e ValidateBoard() per soddisfare il primo requisito. Così sia, se lo fai.

E, mentre sviluppi l'app, non aver paura di riscrivere completamente i test per consentire loro di rappresentare meglio la crescente funzionalità. Potresti voler esaminare Recycling TDD per avere un'idea di quanto sia potente questo strumento TDD essere.

    
risposta data 02.02.2018 - 23:55
fonte
0

I'm building a Sudoku generator.

Potresti essere interessato a Apprendere da Sudoku Solvers ;

I'm following TDD

Non con questi metodi, non lo sei.

Se facessi TDD, la prima cosa che faresti è capire l'API che vuoi testare. In altre parole, come dovrebbe apparire il codice client. Per un risolutore di Sudoku, potresti iniziare con qualcosa come

var solvedPuzzle = findTheSolution(unsolvedPuzzle)

In alternativa, potresti realizzare che non tutti i puzzle non risolti hanno soluzioni e che la soluzione potrebbe non essere univoca, indirizzandoti forse verso un'API come

var solvedPuzzles = findAllSolutions(unsolvedPuzzle)

Parte del punto dell'esercitazione TDD è sperimentare come scrivere codice client per capire se l'API è utilizzabile o meno.

I'm struggling with how to write my tests for these methods.

Vedo due cose che potrebbero causarti problemi.

Uno è che stai testando il tuo codice all'interno della propria algebra. Hai sostanzialmente scritto

var x = MyCode.doTheRightThing()
Assert.assertTrue(x.didTheRightThing())

Ovviamente il codice pensa di aver fatto la cosa giusta; il trucco è verificarlo in modo indipendente.

L'altro è che il tuo test si basa su un effetto collaterale nascosto. Se rifattiamo un po 'il tuo codice originale, otteniamo qualcosa che assomiglia a

var verified = Board.GenerateFilled().ValidateBoard()
Assert.assertTrue(verified)

Il che significa che l'API per questa "funzione" è simile a

var verified = systemUnderTest( /* takes no arguments */ )

Manca una ghiera qui (l'input) per vincolare il comportamento del sistema sotto test.

Using methods that are not under test within a unit test for a different method?

Questa è una cosa perfetta da fare, ma c'è un avvertimento: da qualche parte nella suite di test hai bisogno di un modo per misurare il comportamento del sistema sotto test che è indipendente dal suo calcolo.

Questo test:

Assert.assertTrue(Board.generateValidBoard().ValidateBoard())
Assert.assertTrue(Board.generateInvalidBoard().ValidateBoard())

Non ti dice se funziona , ti dice solo che il codice è internamente coerente .

Per ottenere una conferma indipendente, è necessario un test che assomigli ad una specifica; quando costruiamo una board da questi input, otteniamo una board valida, quando costruiamo una board da quegli input, otteniamo una board non valida.

Quindi il tuo test per ValidateBoard sarà simile a

var validBoard = Board.createBoard( descriptionOfAvalidBoard )
Assert.assertTrue(board.ValidateBoard())

var invalidBoard = Board.createBoard( descriptionOfAnInvalidBoard )
Assert.assertFalse(board.ValidateBoard())

Ora che hai la conferma indipendente che ValidateBoard è conforme alle sue specifiche, puoi rivolgere la tua attenzione al prossimo bit

// This is the thing we are testing
var board = Board.GenerateFilled()

// We have tests up above that demonstrate that this piece works
var isValid = board.ValidateBoard()

// So this checks that we have an implementation that is internally
// consistent with something that we have validated independently.
Assert.assertTrue(isValid)
    
risposta data 04.02.2018 - 14:48
fonte

Leggi altre domande sui tag