codice DRY, test DRY?

3

A un certo punto ho dovuto creare una classe "Class1" e quella classe ha bisogno di un metodo "metodo". Quindi ho il seguente:

Class1MethodTest: A total of N tests that check the behavior of Class1.method
Class1 method: A full implementation of the method

Ma un po 'più tardi ho bisogno di una classe diversa "Class2" per avere un metodo "metodo" completamente simile. Ora ho diversi approcci:

Approccio 1

Class1MethodTest: A total of N tests
Class1 method: Full implementation
Class2MethodTest: Another set of identical tests
Class2 method: Another full implementation

Pro: semplice stupido

Contro: non SECCO

Almeno questo è il primo tentativo e potrei anche scrivere questo prima di fare qualsiasi refactoring, giusto?

Metodo 2:

_hidden_private_implementation_function: Full implementation of required method
Class1MethodTest: A total of N tests
Class1 method: Call hidden_private_whatever
Class2MethodTest: Another set of identical tests
Class2 method: Also call hidden_private_stuff

Pro: codice DRY, ancora stupido

Contro: i test non sono ASCIUTTI, "Interfaccia test, non implementazione"

Approccio 3:

MethodTest: A total of N tests
TotallyPublicCommonMethod: Full implementation of required method
Class1MethodTest: Just one test to verify that Class1 method calls the Public one
Class1 method: Call public common method
Class2MethodTest: One more test
Class2 method: Also call common method

Pro: ASCIUTTO, semplice stupido

Contro: .. altro che "Stai testando l'implementazione, non l'interfaccia"?

Approccio 4:

Questo è dove diventa un po 'esotico. Python mi permette di ASCIUGARE direttamente "Approach 3":

_hidden_private_implementation_function: Full implementation of required method
makeTestForClass(cls): return a total of N tests for class cls
Class1MethodTest = makeTestForClass(Class1)
Class1 method: Call hidden_private_whatever
Class2MethodTest = makeTestForClass(Class2)
Class2 method: Also call hidden_private_stuff

Pro: DRY, "Non testare l'implementazione"

Contro: non così semplice. Troppo difficile da modificare se decido di cambiare qualcosa in Class1.method, ma non in Class2.method

Posso pensare ad un paio di altri approcci, ma quelli non sono molto diversi da questi sopra.

In questo momento ho un codice che assomiglia a "Approach 1" e sto pensando a quale dei due dovrei andare per rendere tutto più pulito e migliore.

    
posta aragaer 13.04.2014 - 10:00
fonte

5 risposte

3

Generalmente scrivo abbastanza prove per darmi fiducia che la mia implementazione sia corretta, ma non di più. Quanti test questo dipende dal problema in questione.

Se ti senti molto insicuro sulla corretta implementazione del comportamento, probabilmente finirai per scrivere una serie completa di test per ogni endpoint e refactoring alla fine: andando da Approccio 1 a Approccio 2, come te ' li ho etichettati. Kent Beck chiama questo tipo di programmazione "guidare in una marcia bassa".

Se il tuo problema non è così complesso, potresti voler cambiare una marcia o due. Ricorda, TDD parla di fare "la cosa più semplice possibile" ad ogni passo. Se vedi che il modo più semplice per eseguire il tuo prossimo passaggio di prova è semplicemente chiamare il metodo che hai già implementato, puoi tornare a casa presto.

Se riscontri un errore o un errore imprevisto del test, sei libero di rallentare, scrivere più test e guidare di nuovo con una marcia più bassa. TDD spesso si sente in questo modo: aggiustando costantemente il tuo ritmo per abbinare i tuoi livelli di sicurezza e paura. È uno dei temi del libro di Kent Beck.

Ricorda che non miri alla copertura al 100% di ogni metodo pubblico; quel tanto che basta per darti la certezza che il tuo codice sia corretto.

    
risposta data 13.04.2014 - 10:29
fonte
7

Hai ragione quando dici che puoi utilizzare il primo approccio prima del refactoring. Personalmente, non sono d'accordo con questo approccio, questa è la regola del terzo da Refactoring di Martin Fowler (pagina 58):

Here's a guideline Don Roberts gave me: The first time you do something, you just do it. The second time you do something similar, you wince at the duplication, but you do the duplicate thing anyway. The third time you do something similar, you refactor.

Resta il fatto che la duplicazione nel codice è fastidiosa. Da qui, devi pensare al contesto esatto della duplicazione:

  • Entrambe le classi sono correlate (ad esempio sia Cat che Dog possono essere alimentate, e prende gli stessi passaggi per nutrirli entrambi, tranne il cibo che cambierà), nel qual caso, creare un'eredità (nel mio esempio, una classe genitore Pet ),

  • Oppure due classi non sono correlate e si basano semplicemente sullo stesso metodo. Ad esempio, la classe WebRequest può contare su GetSlug ¹ per trovare lo slug della richiesta corrente; Customer può anche fare affidamento su GetSlug per normalizzare il nome del cliente per ulteriori scopi di ricerca. Tuttavia, WebRequest e Customer non sono del tutto correlati e non ha senso creare un oggetto comune per tali classi.

    Qui la soluzione sarebbe chiamare un metodo comune da queste due classi, istanziando la terza classe che contiene questo metodo. Ad esempio, sia RequestUri che CustomerName possono diventare oggetti che implementano l'interfaccia ISlug .

Nei test unitari, le cose sono diverse. Se ti ritrovi a duplicare un sacco di codice, indica che i tuoi test di unità contengono troppa logica. Pertanto, dovrebbero essere abbreviati.

Ad esempio:

@Test
public void ensureCatHungryByDefault()
{
    var cat = new Cat();
    assertTrue(cat.Hungry);
}

@Test
public void ensureCatFed()
{
    var cat = new Cat();
    var food = new Whiskas(100.Gramm);
    cat.feed(food);
    assertFalse(cat.Hungry);
}

@Test
public void ensureCatThrowsOnNull()
{
    var cat = new Cat();
    exception.expect(IllegalArgumentException.class);
    cat.Feed(null);
}

e

@Test
public void ensureDogHungryByDefault()
{
    var dog = new Dog();
    assertTrue(dog.Hungry);
}

@Test
public void ensureDogFed()
{
    var dog = new Dog();
    var food = new Pedigree(600.Gramm);
    dog.feed(food);
    assertFalse(dog.Hungry);
}

@Test
public void ensureDogThrowsOnNull()
{
    var dog = new Dog();
    exception.expect(IllegalArgumentException.class);
    dog.Feed(null);
}

contengono alcune duplicazioni, ma è accettabile. I due difetti di duplicazione sono la duplicazione della logica e la duplicazione dei dati. Non esiste una logica aziendale all'interno dei test unitari. Per quanto riguarda i dati, i dati relativi a un cane sono diversi dai dati relativi a un gatto, quindi riteniamo che non vi sia alcuna duplicazione.

Tenendo separati i test di cani e gatti, ottieni sia la leggibilità che la flessibilità:

  • Ottieni maggiore leggibilità, perché se in seguito rompi il codice, puoi facilmente capire che la modifica ha rotto solo Cat , solo Dog o entrambi.

  • Ottieni flessibilità, perché puoi lavorare sulla modifica della classe Cat e affrontare i test unitari corrispondenti, senza dover affrontare altri animali domestici. Se i test fossero comuni a tutti gli animali domestici, potrebbe aver creato una difficoltà quando la tua logica di business cambia, come tutti i gatti mutano, e dovrebbe essere alimentata in modo molto diverso da ora.

¹ Una lumaca è una trasformazione del testo che consiste nel rimuovere tutti i segni diacritici e sostituire tutti i caratteri speciali o spazi bianchi consecutivi per trattini. Esempio: Il était une fois un... hérissonil-etait-une-fois-un-herrison . Questa tecnica viene solitamente utilizzata negli URI e può anche essere utilizzata per scopi di ricerca in cui la ricerca di herisson deve includere anche hérisson nei risultati di ricerca.

    
risposta data 13.04.2014 - 10:23
fonte
2

TL; DR: l'estrazione di funzionalità comuni va bene finché il più strong accoppiamento è garantito dal codice coesione .

Il refactoring si verifica spesso in aree che hanno già una strong coesione e, di conseguenza, dovrebbe anche avere un strong accoppiamento. Quando estrai funzionalità comuni in alcune strutture condivise, aumenti l'accoppiamento. Quando il codice è duplicato, in genere significa che il pezzo duplicato può essere modificato senza influenzare l'altro, ad es. sono disaccoppiati. Se 2 o più parti di codice ora delegano a un pezzo di codice condiviso, ora sono accoppiati a quel codice condiviso. Se il codice condiviso cambia, allora tutto il codice che delega al codice condiviso è interessato, es. sono strongmente accoppiati.

Questo è tutto a posto e dandy finché tutto il codice che delega al codice condiviso è strongmente coeso. Se non lo sono, stai creando un brutto accoppiamento strong.

Che cosa significa tutto questo? Che è bene estrarre e refactoring nei test purché non si stia creando un accoppiamento strong che non dovrebbe esserci. Per dirla in altro modo. Se devi separare metodi che sono incidentalmente simili, non dovrebbero essere refactored per usare il codice condiviso perché non dovrebbero essere accoppiati insieme. Se, d'altra parte, i metodi separati sono simili perché sono correlati in qualche modo significativo (coesivo), l'estrazione del codice condiviso va bene.

    
risposta data 04.05.2014 - 23:45
fonte
1

Il mio principio guida qui è che il codice di prova deve soddisfare lo stesso livello di qualità del codice di implementazione. Se il tuo codice di test non può raggiungere quel livello, allora qualcosa non va nella progettazione dell'implementazione.

Nel contesto specifico di un metodo che è ora comune a due classi, proverei a reimpostarlo in una classe comune che eredita sia Class1 che Class2 . Come notato da @MainMa, se avevi Cat e ora hai Dog , rifatta il loro metodo feed nella classe Pet .

Se ritieni che ciò non sia corretto, poiché Class1 e Class2 concettualmente non hanno nulla in comune, devi definire una nuova classe che gli altri usano per raggiungere il loro scopo . Come ti chiedi in un commento, se il metodo di refactoring è save_to_database , potresti dover introdurre una classe chiamata Database , che le classi Cat e Screwdriver utilizzano internamente per mantenere il loro stato.

Quindi la tua domanda si trasforma in test dell'interfaccia di questa nuova classe, il cui unico scopo è quello di memorizzare le cose, indipendentemente dal fatto che provengano da un gatto o da un cacciavite.

Il primo approccio dà qualcosa di simile a questo:

class Cat:
    def feed(self):
        """
            >>> cat = new Cat()
            >>> cat.feed()
        """
        pass

# turns into

class Pet:
    def feed(self):
        """
            >>> pet = new Pet()
            >>> pet.feed()
        """
        pass

class Dog(Pet):
    pass

class Cat(Pet):
    pass

Il secondo approccio, che credo sia più pertinente per te, sarebbe simile a questo:

class Customer:
    def save(self):
        """
            >>> customer = new Customer()
            >>> customer.save()
            >>> another_db_conn = db.connect()
            >>> another_db_conn.read(customer.id) == customer.name
            True
        """
        conn = db.connect()
        conn.write(self.name)

# turns into

class Database:
    def save(self, data):
        """
            >>> db = new Database()
            >>> db.save("whatever")
            >>> another_db_conn = db.connect()
            >>> another_db_conn.read() == "whatever"
            True
        """
        conn = db.connect()
        conn.write(self.name)

class Customer:
    def __init__(self, database):
        self.db = database

    def save(self):
        self.db.save(self.name)

class Whale:
    def __init__(self, database):
        self.db = database

     def save(self):
         self.db.save("the whales!")

Avviso Ho usato qui i doctest per mostrare l'implementazione e i test mescolati insieme. Potresti preferire l'uso di unittests o altro. Inoltre, l'esempio Database è troppo generico e così com'è la classe non aggiunge valore: dovrebbe fare qualcosa di interessante sul data che ottiene e che il comportamento è ciò che vuoi testare; ma questo è solo un esempio.

Se ritieni che la definizione di nuove classi da utilizzare possa "aprire" il tuo pacchetto con un'API pubblica molto più di quanto desideri, ricorda che puoi sempre definire classi come pacchetto-privato (in Java), o semplicemente non esportarli dal tuo pacchetto (in Python). I test definiti all'interno del pacchetto saranno comunque in grado di accedervi.

    
risposta data 14.04.2014 - 11:51
fonte
0

Il tuo modulo di test potrebbe contenere un'interfaccia comune per entrambe le classi e gli adattatori corrispondenti.

Quindi puoi raggruppare la classe sottoposta a test e passarla al codice di test comune.

    
risposta data 14.04.2014 - 04:11
fonte

Leggi altre domande sui tag