Il codice testabile è un codice migliore?

100

Sto tentando di prendere l'abitudine di scrivere regolarmente test unitari con il mio codice, ma ho letto che prima è importante scrivere codice verificabile . Questa domanda tocca i principi SOLID della scrittura di codice testabile, ma voglio sapere se quei principi di progettazione sono vantaggiosi (o almeno non dannosi) senza pianificare la scrittura di test. Per chiarire - Capisco l'importanza di scrivere test; questa non è una domanda sulla loro utilità.

Per illustrare la mia confusione, in il pezzo che ha ispirato questa domanda, lo scrittore fornisce un esempio di una funzione che controlla l'ora corrente e restituisce un valore in base al tempo. L'autore fa riferimento a questo come codice errato perché produce i dati (il tempo) che utilizza internamente, rendendo così difficile il test. Per me, però, sembra un eccesso nel passare il tempo come argomento. Ad un certo punto il valore deve essere inizializzato, e perché non più vicino al consumo? Inoltre, lo scopo del metodo nella mia mente è di restituire un valore basato sul tempo corrente , rendendolo un parametro che implichi che questo scopo può / deve essere cambiato. Questa e altre domande mi portano a chiedermi se il codice verificabile fosse anche il codice "migliore".

La scrittura del codice verificabile è ancora buona pratica anche in assenza di test?

Il codice verificabile è effettivamente più stabile? è stato suggerito come un duplicato Tuttavia, questa domanda riguarda la "stabilità" del codice, ma mi sto chiedendo in modo più ampio se il codice è superiore anche per altri motivi, come la leggibilità, le prestazioni, l'accoppiamento e così via.

    
posta WannabeCoder 01.07.2015 - 16:59
fonte

12 risposte

112

Riguardo alla definizione comune di unit test, direi di no. Ho visto il semplice codice reso complicato a causa della necessità di ruotarlo per adattarlo al framework di test (ad esempio interfacce e IoC ovunque rendendo le cose difficili da seguire attraverso strati di chiamate di interfaccia e dati che dovrebbero essere ovviamente trasmessi dalla magia). Data la scelta tra codice facile da capire o codice facile da testare, vado sempre con il codice manutenibile.

Questo non significa non testare, ma per adattarsi agli strumenti, non il contrario. Esistono altri modi per testare (ma il codice difficile da comprendere è sempre un codice errato). Ad esempio, puoi creare test unitari meno rigorosi (ad es. Martin Fowler l'atteggiamento di un'unità in genere classe, non un metodo), oppure puoi invece lanciare il tuo programma con test di integrazione automatici. Questo potrebbe non essere bello come il tuo framework di test si illumina di tick verdi, ma siamo alla ricerca di codice testato, non della gamification del processo, giusto?

Puoi rendere il tuo codice facile da mantenere ed essere ancora valido per i test unitari definendo delle buone interfacce tra di loro e quindi scrivendo dei test che esercitano l'interfaccia pubblica del componente; oppure si potrebbe ottenere un framework di test migliore (uno che sostituisce le funzioni in fase di runtime per deriderle, piuttosto che richiedere che il codice venga compilato con i mock in atto). Un framework di test unitario migliore consente di sostituire la funzionalità di sistema GetCurrentTime () con la propria, in fase di esecuzione, quindi non è necessario introdurre involucri artificiali a questo solo per adattarsi allo strumento di test.

    
risposta data 01.07.2015 - 17:13
fonte
68

Is writing testable code still good practice even in the absence of tests?

Per prima cosa, l'assenza di test è un problema molto più grande di quanto il tuo codice sia testabile o meno. Non avere test unitari significa che non hai finito con il tuo codice / funzione.

In questo modo, non direi che è importante scrivere codice testabile - è importante scrivere codice flessibile . Il codice inflessibile è difficile da testare, quindi ci sono molte sovrapposizioni nell'approccio e ciò che le persone chiamano.

Quindi, per me, c'è sempre una serie di priorità nella scrittura del codice:

  1. Falla funzionare : se il codice non esegue ciò che deve fare, non ha alcun valore.
  2. Rendilo gestibile : se il codice non è gestibile, smetterà rapidamente di funzionare.
  3. Rendi flessibile - se il codice non è flessibile, smetterà di funzionare quando gli affari arrivano inevitabilmente e chiede se il codice può fare XYZ.
  4. Rendi veloce - oltre un livello accettabile di base, le prestazioni sono semplicemente utili.

I test unitari aiutano la manutenzione del codice, ma solo fino a un certo punto. Se rendi il codice meno leggibile o più fragile per far funzionare i test unitari, questo diventa controproducente. Il "codice verificabile" è generalmente un codice flessibile, quindi è buono, ma non importante quanto la funzionalità o la manutenibilità. Per qualcosa di simile al tempo corrente, rendere questo flessibile è buono ma danneggia la manutenibilità rendendo il codice più difficile da usare giusto e più complesso. Dal momento che la manutenibilità è più importante, di solito commetto errori verso un approccio più semplice anche se è meno verificabile.

    
risposta data 01.07.2015 - 17:17
fonte
48

Sì, è una buona pratica. Il motivo è che la testabilità non è a scopo di test. È per chiarezza e comprensibilità che porta con sé.

A nessuno importa dei test stessi. È un triste fatto della vita che abbiamo bisogno di grandi suite di test di regressione perché non siamo abbastanza geniali da scrivere codice perfetto senza controllare costantemente il nostro piede. Se potessimo, il concetto di test sarebbe sconosciuto e tutto ciò non sarebbe un problema. Certamente vorrei poterlo fare. Ma l'esperienza ha dimostrato che quasi tutti noi non possiamo farlo, quindi i test che coprono il nostro codice sono una buona cosa anche se ci mettono del tempo a scrivere codice commerciale.

In che modo i test hanno migliorato il nostro codice aziendale indipendentemente dal test stesso? Forzandoci a segmentare le nostre funzionalità in unità che si dimostrano facilmente corrette. Queste unità sono anche più facili da ottenere rispetto a quelle che altrimenti saremmo tentati di scrivere.

Il tuo esempio di tempo è un buon punto. Finché solo ha una funzione che restituisce l'ora corrente, potresti pensare che non ha senso programmarlo. Quanto può essere difficile farlo bene? Ma inevitabilmente il tuo programma usa questa funzione in un altro codice, e tu vorresti sicuramente testare quel codice in condizioni diverse, anche in momenti diversi. Pertanto, è una buona idea essere in grado di manipolare il tempo in cui la funzione ritorna, non perché non si sospetta la chiamata di currentMillis() su una riga, ma perché è necessario verificare i chiamanti di quella chiamata sotto controllo circostanze. Quindi, vedete, avere il codice testabile è utile anche se da solo, non sembra meritare molta attenzione.

    
risposta data 01.07.2015 - 17:09
fonte
12

At some point the value needs to be initialized, and why not closest to consumption?

Perché potrebbe essere necessario riutilizzare quel codice, con un valore diverso da quello generato internamente. La possibilità di inserire il valore che intendi utilizzare come parametro, ti assicura che puoi generare quei valori in base a qualsiasi momento che ti piace, non solo "adesso" (con "ora" che significa quando chiami il codice).

Rendere testabile il codice in effetti significa creare codice che possa (dall'inizio) essere utilizzato in due diversi scenari (produzione e test).

Fondamentalmente, mentre si può sostenere che non vi è alcun incentivo a rendere il codice testabile in assenza di test, vi è un grande vantaggio nella scrittura di codice riutilizzabile, e i due sono sinonimi.

Plus, the purpose of the method in my mind is to return some value based on the current time, by making it a parameter you imply that this purpose can/should be changed.

Si potrebbe anche sostenere che lo scopo di questo metodo è di restituire un valore basato su un valore temporale e che è necessario generarlo in base a "now". Uno di questi è più flessibile e se ti abitui a scegliere quella variante, nel tempo, la percentuale di riutilizzo del codice aumenterà.

    
risposta data 01.07.2015 - 17:37
fonte
10

Può sembrare sciocco dirlo in questo modo, ma se vuoi essere in grado di testare il tuo codice, allora sì, scrivere codice testabile è meglio. Chiedi:

At some point the value needs to be initialized, and why not closest to consumption?

Proprio perché, nell'esempio a cui ti riferisci, rende quel codice non testabile. A meno che non si esegua solo un sottoinsieme dei test in diversi momenti della giornata. O si ripristina l'orologio di sistema. O qualche altra soluzione. Tutto ciò è peggio che semplicemente rendere flessibile il codice.

Oltre ad essere inflessibile, quel piccolo metodo in questione ha due responsabilità: (1) ottenere il tempo di sistema e poi (2) restituire un valore basato su di esso.

public static string GetTimeOfDay()
{
    DateTime time = DateTime.Now;
    if (time.Hour >= 0 && time.Hour < 6)
    {
        return "Night";
    }
    if (time.Hour >= 6 && time.Hour < 12)
    {
        return "Morning";
    }
    if (time.Hour >= 12 && time.Hour < 18)
    {
        return "Afternoon";
    }
    return "Evening";
}

Ha senso abbattere ulteriormente le responsabilità, in modo che la parte fuori dal tuo controllo ( DateTime.Now ) abbia il minor impatto sul resto del codice. Fare così renderà il codice sopra più semplice, con l'effetto collaterale di essere sistematicamente testabile.

    
risposta data 01.07.2015 - 17:20
fonte
9

Certamente ha un costo, ma alcuni sviluppatori sono così abituati a pagarlo che hanno dimenticato che il costo è lì. Per il tuo esempio, ora hai due unità invece di una, hai richiesto il codice chiamante per inizializzare e gestire un'ulteriore dipendenza, e mentre GetTimeOfDay è più testabile, sei di nuovo nella stessa barca testando la tua nuova IDateTimeProvider . È solo che se hai dei buoni test, i benefici di solito superano i costi.

Inoltre, in una certa misura, la scrittura di un codice verificabile di tipo ti incoraggia ad architettare il tuo codice in un modo più gestibile. Il nuovo codice di gestione delle dipendenze è fastidioso, quindi è necessario raggruppare tutte le funzioni dipendenti dal tempo insieme, se possibile. Ciò può aiutare a mitigare e correggere bug come, ad esempio, quando si carica una pagina proprio su un limite temporale, con alcuni elementi resi utilizzando l'ora precedente e altri con l'ora successiva. Può anche velocizzare il programma evitando chiamate di sistema ripetute per ottenere l'ora corrente.

Naturalmente, questi miglioramenti architettonici dipendono strongmente da qualcuno che ne nota le opportunità e le implementa. Uno dei maggiori pericoli di concentrarsi così tanto sulle unità sta perdendo di vista l'immagine più grande.

Molti framework di test unitari consentono di eseguire il patch di scimmia su un oggetto fittizio in fase di runtime, il che consente di ottenere i vantaggi della testabilità senza tutti i problemi. L'ho persino visto in C ++. Cerca in quella capacità in situazioni in cui sembra che il costo della testabilità non ne valga la pena.

    
risposta data 01.07.2015 - 17:53
fonte
8

È possibile che non tutte le caratteristiche che contribuiscono alla testabilità siano desiderabili al di fuori del contesto di testabilità - ho difficoltà a trovare una giustificazione non correlata al test per il parametro temporale che citi, ad esempio - ma in generale le caratteristiche che contribuire alla testabilità anche contribuire a un buon codice indipendentemente dalla testabilità.

In generale, il codice testabile è un codice malleabile. È in pezzi piccoli, discreti, coesi, in modo che i singoli bit possano essere riutilizzati. È ben organizzato e ben definito (per poter testare alcune funzionalità si presta più attenzione alla denominazione, se non si scrivessero i test il nome per una funzione monouso sarebbe meno importante). Tende ad essere più parametrico (come il tuo esempio di tempo), quindi aperto per l'uso da altri contesti rispetto allo scopo originale. È ASCIUTTO, quindi meno ingombrante e più facile da comprendere.

Sì. È buona norma scrivere codice verificabile, anche indipendentemente dal test.

    
risposta data 01.07.2015 - 17:26
fonte
8

Scrivere un codice verificabile è importante se vuoi essere in grado di dimostrare che il tuo codice funziona effettivamente.

Tendo ad essere d'accordo con i sentimenti negativi di deformare il tuo codice in contorsioni odiose solo per adattarlo a un particolare framework di test.

D'altra parte, tutti qui hanno, ad un certo punto o altro, dovuto occuparsi di quella funzione magica lunga 1.000 linee che è semplicemente atroce da affrontare, virtualmente non può essere toccata senza rompere uno o più oscuri dipendenze non ovvie da qualche altra parte (o da qualche parte dentro di sé, dove la dipendenza è quasi impossibile da visualizzare) ed è praticamente per definizione non verificabile. La nozione (che non è senza merito) che i framework di test sono diventati esagerati non dovrebbe essere presa come una licenza gratuita per scrivere codice di scarsa qualità, non verificabile, secondo me.

Gli ideali di sviluppo basati sui test tendono a spingerti verso la scrittura di procedure a responsabilità unica, ad esempio, e che è sicuramente una buona cosa. Personalmente, dico buy in single-responsibility, un'unica fonte di verità, scope controllato (nessuna strana variabile globale) e riduciamo al minimo le dipendenze fragili, e il tuo sarà controllabile. Testabile da qualche specifico framework di test? Chissà. Ma forse è il framework di test che deve adattarsi a un buon codice, e non viceversa.

Ma per essere chiari, il codice che è così intelligente, o così lungo e / o interdipendente da non essere facilmente compreso da un altro essere umano non è un buon codice. Ed anche, per coincidenza, non è un codice che può essere facilmente testato.

Quindi, avvicinandomi al mio riassunto, il codice controllabile migliore ?

Non lo so, forse no. Le persone qui hanno alcuni punti validi.

Ma credo che il codice migliore sia anche testabile .

E che se parli di software serio da utilizzare in situazioni serie, spedire codice non testato non è la cosa più responsabile che potresti fare con il denaro del tuo datore di lavoro o dei tuoi clienti.

È anche vero che alcuni codici richiedono test più rigorosi di altri codici ed è un po 'sciocco pretendere il contrario. Come ti piacerebbe essere stato un astronauta sulla navetta spaziale se il sistema di menu che ti interfacciava con i sistemi vitali sulla navetta non fosse stato testato? O un dipendente di una centrale nucleare dove non sono stati testati i sistemi software di monitoraggio della temperatura nel reattore? D'altra parte, un po 'di codice che genera un semplice report di sola lettura richiede un container pieno di documentazione e migliaia di test unitari? Spero di no. Sto solo dicendo ...

    
risposta data 02.07.2015 - 04:11
fonte
5

To me, though, it seems like overkill to pass in the time as an argument.

Hai ragione, e con il mocking puoi rendere il codice testabile e evitando di passare il tempo (pun intention undefined). Codice di esempio:

def time_of_day():
    return datetime.datetime.utcnow().strftime('%H:%M:%S')

Ora diciamo che vuoi testare cosa succede durante il secondo intercalare. Come dici tu, per testare questo modo eccessivo dovresti cambiare il codice (di produzione):

def time_of_day(now=None):
    now = now if now is not None else datetime.datetime.utcnow()
    return now.strftime('%H:%M:%S')

Se Python supporta i secondi bisestili il codice di prova dovrebbe essere simile a questo:

def test_handle_leap_second(self):
    actual = time_of_day(
        now=datetime.datetime(year=2015, month=6, day=30, hour=23, minute=59, second=60)
    expected = '23:59:60'
    self.assertEquals(actual, expected)

Puoi testarlo, ma il codice è più complesso del necessario e i test ancora non possono esercitare in modo affidabile il ramo di codice che verrà utilizzato dalla maggior parte del codice di produzione (cioè, non passando un valore per now ). Puoi risolvere il problema utilizzando un simulato . a partire dall'originale codice di produzione:

def time_of_day():
    return datetime.datetime.utcnow().strftime('%H:%M:%S')

Codice test:

@unittest.patch('datetime.datetime.utcnow')
def test_handle_leap_second(self, utcnow_mock):
    utcnow_mock.return_value = datetime.datetime(
        year=2015, month=6, day=30, hour=23, minute=59, second=60)
    actual = time_of_day()
    expected = '23:59:60'
    self.assertEquals(actual, expected)

Questo offre diversi vantaggi:

  • Stai testando time_of_day indipendentemente delle sue dipendenze.
  • Stai testando stesso percorso di codice come codice di produzione.
  • Il codice di produzione è il più semplice possibile.

Da una nota a margine, è auspicabile che i futuri schemi di derisione semplificheranno le cose in questo modo. Ad esempio, dal momento che devi fare riferimento alla funzione mocked come stringa, non puoi facilmente far cambiare IDE automaticamente quando time_of_day inizia a utilizzare un'altra fonte per il tempo.

    
risposta data 04.07.2015 - 15:57
fonte
4

Una qualità di codice ben scritto è che è robusto da modificare . Cioè, quando arriva un cambiamento di requisiti, il cambiamento nel codice dovrebbe essere proporzionale. Questo è un ideale (e non sempre raggiunto), ma scrivere codice testabile ci aiuta a avvicinarci a questo obiettivo.

Perché aiuta a farci avvicinare? In produzione, il nostro codice opera all'interno dell'ambiente di produzione, inclusa l'integrazione e l'interazione con tutto il nostro altro codice. Nei test unitari, spazziamo via gran parte di questo ambiente. Il nostro codice ora è robusto da cambiare perché i test sono una modifica . Usiamo le unità in modi diversi, con input diversi (mock, input sbagliati che potrebbero non essere mai effettivamente trasmessi, ecc.) Rispetto a quelli che useremmo nella produzione.

Questo prepara il nostro codice per il giorno in cui il cambiamento avviene nel nostro sistema. Supponiamo che il nostro calcolo del tempo abbia bisogno di un tempo diverso in base a un fuso orario. Ora abbiamo la possibilità di passare in quel momento e non dover apportare modifiche al codice. Quando non vogliamo passare in un momento e vogliamo usare l'ora corrente, potremmo semplicemente usare un argomento predefinito. Il nostro codice è robusto da modificare perché è testabile.

    
risposta data 01.07.2015 - 17:34
fonte
4

Dalla mia esperienza, una delle decisioni più importanti e più importanti che prendi quando costruisci un programma è come suddividere il codice in unità (dove "unità" è usata nella sua più ampia senso). Se si sta utilizzando un linguaggio OO basato sulla classe, è necessario interrompere tutti i meccanismi interni utilizzati per implementare il programma in un certo numero di classi. Quindi è necessario rompere il codice di ogni classe in un certo numero di metodi. In alcune lingue, la scelta è come suddividere il tuo codice in funzioni. O se fai la cosa SOA, devi decidere quanti servizi costruirai e cosa andrà in ogni servizio.

Il guasto che scegli ha un enorme effetto sull'intero processo. Buone scelte rendono il codice più facile da scrivere e comportano meno errori (anche prima di iniziare i test e il debug). Rendono più facile cambiare e mantenere. È interessante notare che, una volta individuato un buon breakdown, di solito è anche più facile eseguire test piuttosto che uno scarso.

Perché è così? Non penso di poter capire e spiegare tutte le ragioni. Ma una ragione è che una buona ripartizione invariabilmente significa scegliere una "grana" moderata per unità di implementazione. Non vuoi stipare troppe funzionalità e troppa logica in una singola classe / metodo / funzione / modulo / ecc. Questo rende il tuo codice più facile da leggere e più facile da scrivere, ma rende anche più facile il test.

Non è solo quello, però. Un buon design interno significa che il comportamento previsto (input / output / etc) di ciascuna unità di implementazione può essere definito in modo chiaro e preciso. Questo è importante per i test. Un buon design di solito significa che ogni unità di implementazione avrà un numero moderato di dipendenze dagli altri. Ciò facilita la lettura e la comprensione del codice da parte degli altri, ma rende anche più facile il test. Le ragioni vanno avanti; forse altri possono articolare più ragioni che non posso.

Per quanto riguarda l'esempio nella tua domanda, non penso che "il buon design del codice" sia equivalente a dire che tutte le dipendenze esterne (come una dipendenza dall'orologio del sistema) dovrebbero sempre essere "iniettate". Potrebbe essere una buona idea, ma è un problema separato da quello che sto descrivendo qui e non approfondirò i suoi pro e contro.

Per inciso, anche se si effettuano chiamate dirette a funzioni di sistema che restituiscono l'ora corrente, si agisce sul filesystem e così via, questo non significa che non si possa testare il proprio codice in modo isolato. Il trucco consiste nell'usare una versione speciale delle librerie standard che consente di simulare i valori di ritorno delle funzioni di sistema. Non ho mai visto altri menzionare questa tecnica, ma è abbastanza semplice da fare con molte lingue e piattaforme di sviluppo. (Si spera che il tuo runtime linguistico sia open source e facile da compilare.Se l'esecuzione del tuo codice comporta un passaggio link, si spera che sia anche facile controllare quali librerie sono collegate.)

In sintesi, il codice verificabile non è necessariamente un codice "buono", ma il codice "buono" è solitamente verificabile.

    
risposta data 02.07.2015 - 08:28
fonte
1

Se utilizzi i principi SOLID ti sentirai dalla parte giusta, specialmente se estendilo con KISS , DRY e YAGNI .

Un punto mancante per me è il punto della complessità di un metodo. È un semplice metodo getter / setter? Quindi scrivere dei test per soddisfare la tua struttura di test sarebbe una perdita di tempo.

Se si tratta di un metodo più complesso in cui si manipolano i dati e si vuole essere sicuri che funzionerà anche se si deve modificare la logica interna, sarebbe un ottimo invito per un metodo di prova. Molte volte ho dovuto cambiare un pezzo di codice dopo diversi giorni / settimane / mesi, e sono stato davvero felice di avere il test case. Quando ho iniziato a sviluppare il metodo, l'ho testato con il metodo di test, ed ero sicuro che funzionasse. Dopo il cambiamento il mio codice di test principale funzionava ancora. Quindi ero certo che il mio cambiamento non avesse infranto del vecchio codice in produzione.

Un altro aspetto dei test di scrittura è mostrare agli altri sviluppatori come utilizzare il metodo. Molte volte uno sviluppatore cercherà un esempio su come utilizzare un metodo e quale sarà il valore restituito.

Solo i miei due centesimi .

    
risposta data 01.07.2015 - 19:02
fonte

Leggi altre domande sui tag