Possiamo aggiungere metodi che sono più facili per l'asserzione nei test e che vengono utilizzati solo nei test quando TDD?

5

Fammi fare un semplice esempio di parcheggio.

Un parcheggiatore può gestire diversi parcheggi, e quando arriva una macchina parcheggia la macchina in un parcheggio e può anche aiutare a parcheggiare la macchina.

Quindi, fondamentalmente nella mia mente, ci sarà una classe ParkingBoy come questa:

class ParkingBoy {
    public ParkingBoy(List<ParkingLot> parkingLots) {}
    public Ticket park(Car car) {}
    public Car unpark(Ticket ticket) {}
}

Ma nella pratica di TDD, progettiamo l'API dal punto di vista dell'utente e non presumiamo quale sia la classe effettiva e quali metodi ha. Lascia che siano i test a guidarli.

Quindi ho diviso il requisito in diverse piccole attività:

1. A parking boy can manage parking lots
2. A parking boy can park a car to any avaiable parking lot, and return a ticket
3. A parking boy can unpark the car for a ticket

Quando si esegue la prima attività, il mio test è:

class ParkingBoySpec {
    @Test public void should_manage_parking_lots() {
        List<ParkingLot> parkingLots = list(parkingLot1, parkingLot2);
        ParkingBoy boy = new ParkingBoy(parkingLots);
        assertThat(boy.getParkingLots()).containsExactly(pakrkingLot1, parkingLot2);
    }    
}

Ma il mio amico ha domande sul metodo getParkingLots .

A suo parere, questo metodo viene usato solo per l'asserzione del test e non verrà utilizzato in nessuna parte dell'implementazione, quindi non dovremmo fornirlo. E anche, in realtà non ci importa dei parcheggi gestiti dal ragazzo, quello a cui teniamo è che può parcheggiare e parcheggiare la macchina. Quindi dovremmo rimuovere la prima attività e iniziare dall'attività 2 ( park ).

La mia opinione è:

  1. Poiché scriviamo test prima dell'implementazione in TDD, in realtà non sappiamo se verrà utilizzato getParkingLots nell'implementazione successiva.
  2. Sebbene lo scopo principale del parking boy sia di park e unpark di un'auto, ma voglio iniziare da una semplice attività
  3. Il test è l '"utente" del codice, quindi quando il test pensa se c'è un getParkingLots , è un buon motivo per fornire un getParkingLots in ParkingBoy

Come pensi questa domanda?

    
posta Freewind 17.12.2015 - 05:47
fonte

5 risposte

10

A parking boy can manage several parking lots, and when a car comes, he will park the car into some parking lot and can also help to un-park the car. [...]

So I first split the requirement into several small tasks:

  1. A parking boy can manage parking lots
  2. A parking boy can park a car to any avaiable parking lot, and return a ticket
  3. A parking boy can unpark the car for a ticket

Secondo me, la causa principale del problema è il modo in cui si divide il requisito in compiti più piccoli.
Ad esempio, cosa significa che un parcheggiatore "gestisce" parcheggi? Molto probabilmente significa che il ragazzo del parcheggio conosce molti parcheggi per parcheggiare / disabitare auto, ma significa anche che può fornirti informazioni su quei lotti?

Quando si decompongono i requisiti relativi al parcheggio in compiti più piccoli, si dovrebbe cercare di mantenere tali attività strettamente correlate alle attività che il software finale probabilmente vorrà fare con il parcheggio.
Se il prodotto finale si preoccupa solo della possibilità di parcheggiare / disabitare le auto, una ripartizione come questa sarebbe più adatta:

  1. Un parcheggiatore può parcheggiare l'auto fino a qualsiasi parcheggio disponibile che il bambino conosce e restituire un biglietto
  2. Un parcheggiatore si rifiuterà di parcheggiare l'auto se non ci sono partite disponibili a conoscenza del ragazzo
  3. Un parcheggiatore può disboscare l'auto quando viene presentato un biglietto

Il salto più grande che devi fare qui è che il primo compito / test richiede due interfacce: uno per dire al parcheggiatore dei parcheggi (potrebbe essere il costruttore) e un altro per parcheggiare una macchina. Se ciò ti infastidisce, puoi costruire il test in più fasi: prima verifica, avendo errori di compilazione, che devi fornire un elenco di parcheggi e poi aggiungere la chiamata per parcheggiare l'auto allo stesso test.

    
risposta data 17.12.2015 - 09:04
fonte
8
  1. A parking boy can manage parking lots

sbagliato. Il fatto stesso che ci sia "gestire" in quella frase dovrebbe essere un'enorme bandiera rossa. Questa frase non dice assolutamente nulla sul comportamento del ragazzo di parcheggio. Se non c'è un comportamento interessante, non è necessario implementare un test per questo. Avere questi requisiti può essere pericoloso, perché persone diverse immaginano cose diverse leggendo questa frase. In realtà, questo tipo di frase sarebbe una user story di alto livello, che viene divisa in attività specifiche, ad esempio cosa succede se tutti i lotti sono pieni o se il cliente perde un ticket.

Although the main purpose of the parking boy is to park and unpark a car, but I want to start from a simple task

Quindi inizi con "Parking boy può parcheggiare una macchina in un parcheggio e restituire un biglietto." Ciò significherebbe sin dall'inizio che Parking Boy conosce solo un singolo parcheggio. Solo dopo aver ottenuto requisiti come "Parcheggia il ragazzo può parcheggiare più auto" dovresti sapere che ha bisogno di un elenco di parcheggi.

    
risposta data 17.12.2015 - 09:05
fonte
4

Ciò che dovresti chiedere a te stesso è se il semplice fatto che ParkingBoy stia gestendo un set di ParkingLots è qualcosa che dovresti provare. Quello che probabilmente dovresti provare è che effect il fatto che ParkingBoy abbia un set di ParkingLots ha il comportamento della tua classe?

Ecco alcuni approcci che considero spesso (nessuno perfetti), che potrebbero darti la possibilità di testare la tua classe senza richiedere accessi di prova (ma con altri compromessi).

1. Prova gli effetti sullo stato dei tuoi collaboratori.

Invece di verificare se ParkingBoy gestisca direttamente i parcheggi, puoi verificare l'effetto del parcheggio di un'automobile sullo stato del parcheggio. Parcheggio di parcheggioLocali:

public void mustParkCarInManagedParkingLot() {
    ParkingBoy parkingBoy = new ParkingBoy(parkingLot1, parkingLot2);
    parkingBoy.park(car);
    assertThat(parkingLot1.isOccupied(car), equalTo(true));
    assertThat(parkingLot2.isOccupied(car), equalTo(false));
}

Naturalmente, questo presuppone che ParkingBay abbia metodi che ti consentano di interrogare il suo stato. In caso contrario, probabilmente stai semplicemente passando il tempo.

2. Prova gli effetti sull'interfaccia dei tuoi collaboratori

Esiste un altro tipo popolare di approccio di testing unitario in cui non si verifica l'effetto del comportamento sullo stato della classe, ma si verifica effettivamente l'interazione della classe con i suoi collaboratori. Questo è chiamato "test comportamentale" se ricordo correttamente.

Questo approccio è più pratico se usato con un framework di simulazione (io uso Mockito perché è relativamente semplice):

@Mock ParkingLot parkingLot1;
@Mock ParkingLot parkingLot2;

public void parkingCarMustOccupyManagedParkingLot() {

    ParkingBoy parkingBoy = new ParkingBoy(parkingLot1, parkingLot2);
    parkingBoy.park(car);
    verify(parkingLot1).add(car);
    verify(parkingLot2, never()).add(car);
}

Quello che stai facendo qui è testare se il metodo park ha un effetto sul collaboratore ParkingLot tramite una chiamata del metodo comportamentale piuttosto che sullo stato di ispezione (che richiede gli accessor). La cosa bella con il framework di simulazione è che la tua classe di test non dipende dai dettagli di implementazione di ParkingLot stesso - richiede semplicemente che il metodo .add () venga richiamato al momento giusto. Quindi in parte proteggi il tuo test unitario di ParkingBoy contro i cambiamenti in ParkingLot.

Nessuno di questi approcci è perfetto. Il primo richiede ancora che tu acceda a qualche stato, se indirettamente. Il secondo lega il codice di test ad alcuni dettagli di implementazione privati - l'interazione con i collaboratori, ma almeno questo si basa solo sull'API pubblica di quei collaboratori, che di solito ritengo di poter convivere.

    
risposta data 17.12.2015 - 09:13
fonte
2

Congratulazioni. Hai appena scoperto Test della casella bianca .

Come il tuo amico ha correttamente affermato, il consenso generale sul test unitario è che i test unitari dovrebbero testare solo l'API pubblica di una classe, un metodo o un modulo. Ne consegue che, se hai metodi interni, li provi indirettamente, tramite i metodi esterni.

In altre parole, il test unitario è veramente Test della scatola nera , secondo i puristi.

Tuttavia, c'è un problema con questo: l'API che fornisci all'utente della tua classe non è necessariamente la stessa API con cui vuoi testare. Riesci a ottenere una copertura di prova migliore testando unitariamente i tuoi metodi interni direttamente? Certo che puoi, sembra ovvio.

I puristi diranno, tuttavia, che quando si testano i metodi interni, si sta veramente testando ciò che equivale a un dettaglio di implementazione, non a un comportamento. È il comportamento della tua API esterna che conta, non quello che succede sotto il cofano. A cui vorrei rispondere: i test di integrazione sono probabilmente altrettanto buoni, se non migliori, dei test unitari a tale scopo.

Inoltre, testare metodi privati può essere un problema. Come funziona un metodo di test che non espone un'interfaccia pubblica? Ci sono diversi modi: puoi usare classi di accesso interno, riflessione o classi (in C #) parziali. Nessuno di questi modi è l'ideale.

Quindi cosa fare?

Ultimamente, ho effettuato il refactoring dei miei metodi interni in classi separate. Talvolta si tratta di raccolte di metodi di utilità trasparenti referenzialmente in classi statiche. Questi metodi sono altamente testabili e non richiedono mock; tutto ciò che fai è consegnare loro alcuni valori dei parametri e asserire il loro risultato restituito. A volte sono semplicemente classi di supporto che compongo nella classe principale.

Riconosco che queste tecniche non sono l'ultima moda, ma hanno il merito di eliminare completamente il problema "non testare i tuoi metodi interni", perché nessuno dei metodi è privato. Sono gli unici modi che conosco per ottenere gli ideali Uncle Bob senza compromessi: piccoli metodi testabili che utilizzano codice pulito in classi piccole e ben organizzate.

    
risposta data 17.12.2015 - 06:01
fonte
1

Normalmente, non dovresti aggiungere metodi che hanno solo lo scopo di violare l'incapsulamento esponendo lo stato interno degli oggetti. Il loro bisogno è un buon segno di problemi con il design ed è meglio rivederlo e trovare entità e operazioni mancanti.

Nel tuo esempio questo è esattamente il caso: nel mondo reale di solito c'è un'area identificata come parcheggio e suddivisa in più lotti. La prima domanda che il conducente / parcheggiatore vuole chiedere è "Posso parcheggiare in questo parcheggio?" A volte c'è persino uno schermo con un numero di parcheggi disponibili, che non è una proprietà di un lotto specifico, ma un'interfaccia per il parcheggio nel suo complesso. Ciò significa che è necessario aggiungere un'altra entità: Parcheggio

In questa entità avrai sicuramente bisogno di operazioni per interrogare lo spazio totale e il numero di lotti disponibili (per poter rispondere a domande come "posso parcheggiare" e "quali sono le entrate"), così come l'operazione per ottenere il prossimo lotto disponibile.

interface Parking {
    int getAvailableSpace() { ... } 
    int getSize() { ... } 
    /** returns DTO with number and directions to the taken parking lot */
    Route takeNextFreeLot() { ... } 
    void release(int lot) { ... }
}

Questa interfaccia può quindi essere estesa per consentire parcheggi con caricabatterie elettrici, lotti dedicati per persone con disabilità, lotti per camion ecc. I test per questo saranno molto semplici:

Parking parking = new ParkingBuilding(15);
assert(15, parking.getSize());
assert(15, parking.getAvailableSpace());
int number = parking.takeNextFreeLot().getNumber();
assert(14, parking.getAvailableSpace());
parking.release(number);
assert(15, parking.getAvailableSpace());

I parcheggi quindi lavoreranno con il parcheggio e registreranno i numeri di lotto nei biglietti, ma non funzioneranno con gli oggetti ParkingLot che diventeranno lo stato interno del parcheggio. I test per ParkingBoy avranno il seguente aspetto:

@Mock Parking parking;

public void parkingCarMustOccupyParkingLot() {
    when(parking.takeNextFreeLot()).thenReturn(new Route(1));
    ParkingBoy parkingBoy = new ParkingBoy(parking);
    Ticket ticket = parkingBoy.park(car);
    verify(parking).takeNextFreeLot();
    parkingBoy.getCar(ticket);
    verify(parking).release(1);
}
    
risposta data 17.12.2015 - 13:54
fonte

Leggi altre domande sui tag