Il codice testabile è un codice migliore?


103

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

Per illustrare la mia confusione, nel pezzo che ha ispirato questa domanda, lo scrittore fornisce un esempio di una funzione che controlla l'ora corrente e restituisce un valore a seconda dell'ora. L'autore indica questo come codice errato perché produce i dati (il tempo) che utilizza internamente, rendendo così difficile il test. Per me, tuttavia, sembra eccessivo 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 è quello di restituire un valore basato sul tempo corrente , rendendolo un parametro che implica che questo scopo possa / debba essere modificato. Questa e altre domande mi portano a chiedermi se il codice testabile fosse sinonimo di codice "migliore".

Scrivere codice testabile è ancora una buona pratica anche in assenza di test?


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


24
C'è una proprietà speciale della funzione che richiede di passare il tempo chiamato idempotenza. Tale funzione produrrà lo stesso risultato ogni volta che viene chiamata con un determinato valore di argomento, il che non solo la rende più verificabile, ma più compostabile e più facile da ragionare.
Robert Harvey,

4
Puoi definire "codice migliore"? vuoi dire "mantenibile"? "più facile da usare-senza-IOC-contenitore-magico"?
k3b,

7
Suppongo che non hai mai avuto un test fallito perché ha utilizzato l'ora di sistema effettiva e quindi l'offset del fuso orario è cambiato.
Andy,

5
È meglio del codice non verificabile.
Tulains Córdova,

14
@RobertHarvey Non chiamerei quell'idempotenza, direi che è trasparenza referenziale : se func(X)ritorna "Morning", quindi la sostituzione di tutte le occorrenze di func(X)with "Morning"non cambierà il programma (cioè la chiamata funcnon fa altro che restituire il valore). L'idempotenza implica sia quello func(func(X)) == X(che non è corretto per tipo), sia quello che func(X); func(X);produce gli stessi effetti collaterali di func(X)(ma qui non ci sono effetti collaterali)
Warbo

Risposte:


116

Per quanto riguarda la definizione comune di unit test, direi di no. Ho visto il codice semplice reso contorto a causa della necessità di ruotarlo per adattarlo al framework di test (ad es. Interfacce e IoC ovunque rendendo le cose difficili da seguire attraverso strati di chiamate di interfaccia e dati che dovrebbero essere ovvi passati dalla magia). Data la scelta tra codice che è facile da capire o codice che è facile da testare l'unità, vado sempre con il codice gestibile.

Questo non significa non testare, ma adattarsi agli strumenti adatti a te, non viceversa. Esistono altri modi per testare (ma il codice difficile da capire è sempre un codice errato). Ad esempio, è possibile creare test unitari meno granulari (ad es. L'atteggiamento di Martin Fowler secondo cui un'unità è generalmente una classe, non un metodo), oppure è possibile eseguire il programma con test di integrazione automatizzati. Potrebbe non essere così bello come il tuo framework di test si illumina con segni di spunta verdi, ma stiamo cercando un codice testato, non la gamification del processo, giusto?

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


3
I commenti non sono per una discussione estesa; questa conversazione è stata spostata in chat .
Ingegnere mondiale il

2
Penso che valga la pena notare che conosco almeno una lingua che ti consente di fare ciò che descrive il tuo ultimo paragrafo: Python con Mock. A causa del modo in cui funzionano le importazioni dei moduli, praticamente tutto a parte le parole chiave può essere sostituito con un finto, anche metodi / classi / etc API standard. Quindi è possibile, ma potrebbe richiedere che il linguaggio sia progettato in modo tale da supportare quel tipo di flessibilità.
jpmc26,

6
Penso che ci sia una differenza tra "codice testabile" e "codice [contorto] per adattarsi al framework di test". Non sono sicuro di dove sto andando con questo commento, se non per dire che sono d'accordo sul fatto che il codice "contorto" sia errato e che il codice "verificabile" con buone interfacce sia buono.
Bryan Oakley,

2
Ho espresso alcuni dei miei pensieri nei commenti dell'articolo (poiché qui non sono ammessi commenti estesi), dai un'occhiata! Per essere chiari: sono l'autore dell'articolo citato :)
Sergey Kolodiy,

Devo essere d'accordo con @BryanOakley. Il "codice testabile" suggerisce che le tue preoccupazioni sono separate: è possibile testare un aspetto (modulo) senza interferenze da altri aspetti. Direi che questo è diverso da "adeguare le convenzioni di sperimentazione specifiche di supporto del progetto". Questo è simile ai modelli di progettazione: non dovrebbero essere forzati. Il codice che utilizza correttamente i modelli di progettazione sarebbe considerato un codice forte. Lo stesso vale per i principi di prova. Se rendere il tuo codice "testabile" si traduce in un'eccessiva rotazione del codice del tuo progetto, stai facendo qualcosa di sbagliato.
Vince Emigh,

68

Scrivere codice testabile è ancora una buona pratica anche in assenza di test?

Per prima cosa, l'assenza di test è un problema molto più grande del testare il codice o meno. Non avere unit test significa che non hai finito con il tuo codice / funzionalità.

A parte questo, non direi che è importante scrivere codice testabile - è importante scrivere codice flessibile . Il codice non flessibile è difficile da testare, quindi ci sono molte sovrapposizioni nell'approccio e nel modo in cui le persone lo chiamano.

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

  1. Fallo funzionare : se il codice non fa ciò che deve fare, è inutile.
  2. Renderlo gestibile : se il codice non è gestibile, smetterà rapidamente di funzionare.
  3. Rendilo flessibile : se il codice non è flessibile, smetterà di funzionare quando inevitabilmente arriverà il business e ti chiederà se il codice può fare XYZ.
  4. Renderlo veloce - oltre un livello accettabile di base, le prestazioni sono solo sugo.

I test unitari aiutano la manutenibilità 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 come il tempo attuale, renderlo flessibile è un bene, ma danneggia la manutenibilità rendendo il codice più difficile da usare giusto e più complesso. Poiché la manutenibilità è più importante, di solito commetto un errore verso l'approccio più semplice, anche se è meno verificabile.


4
Mi piace la relazione che metti in evidenza tra testabile e flessibile, che mi rende più comprensibile l'intero problema. La flessibilità consente al codice di adattarsi, ma lo rende necessariamente un po 'più astratto e meno intuitivo da capire, ma è un sacrificio utile per i vantaggi.
WannabeCoder,

3
ciò detto, spesso vedo che metodi che avrebbero dovuto essere privati ​​sono stati costretti a livello pubblico o di pacchetto per consentire al framework di unit test di accedervi direttamente. Lungi dall'essere un approccio ideale.
jwenting

4
@WannabeCoder Naturalmente, vale la pena aggiungere flessibilità solo quando alla fine si risparmia tempo. Ecco perché non scriviamo ogni singolo metodo su un'interfaccia: il più delle volte è più semplice riscrivere il codice piuttosto che incorporare troppa flessibilità fin dall'inizio. YAGNI è ancora un principio estremamente potente - assicurati solo che qualunque sia la cosa "non ti servirà", aggiungerlo retroattivamente non ti darà più lavoro in media che implementarlo in anticipo. È il codice che non segue YAGNI che ha la maggior parte dei problemi con flessibilità nella mia esperienza.
Luaan,

3
"Non avere test unitari significa che non hai finito con il tuo codice / funzione" - Non vero. La "definizione di fatto" è qualcosa che il team decide. Può includere o meno un certo grado di copertura del test. Ma da nessuna parte esiste un requisito rigoroso che dice che una funzione non può essere "eseguita" se non ci sono test per essa. Il team può scegliere di richiedere test, oppure no.
aroth,

3
@Telastyn In oltre 10 anni di sviluppo, non ho mai avuto un team che imponesse un framework di test unitari, e solo due ne avevano uno (entrambi avevano una scarsa copertura). Un posto richiedeva un documento verbale su come testare la funzione che stavi scrivendo. Questo è tutto. Forse sono sfortunato? Non sono un test anti-unità (sul serio, modico il sito SQA.SE, sono un test unitario molto professionale!) Ma non ho trovato che siano così diffusi come afferma la tua affermazione.
corsiKa

50

Sì, è una buona pratica. Il motivo è che la testabilità non è nell'interesse dei test. È per motivi di chiarezza e comprensibilità che ne deriva.

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

In che modo avere test migliora il nostro codice aziendale indipendentemente dal test stesso? Obbligandoci a segmentare la nostra 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é hai solo una funzione che restituisce l'ora corrente potresti pensare che non ha senso programmarla. Quanto può essere difficile farlo bene? Ma inevitabilmente il tuo programma utilizzerà questa funzione in altri codici e sicuramente vorrai testare quel codice in condizioni diverse, anche in momenti diversi. Pertanto, è una buona idea essere in grado di manipolare il tempo di ritorno della funzione, non perché si diffida della propria currentMillis()chiamata su una linea , ma perché è necessario verificare i chiamanti di quella chiamata in circostanze controllate. Come vedi, avere un codice testabile è utile anche se da solo, non sembra meritare così tanta attenzione.


Un altro esempio è se si desidera estrarre una parte del codice di un progetto in un altro posto (per qualsiasi motivo). Più le diverse parti della funzionalità sono indipendenti l'una dall'altra, più facile è estrarre esattamente la funzionalità di cui hai bisogno e niente di più.
valenterry,

10
Nobody cares about the tests themselves-- Lo voglio. Trovo che i test siano una migliore documentazione di ciò che fa il codice rispetto a qualsiasi commento o file readme.
jcollum,

Sto leggendo lentamente sulle pratiche di test da un po 'di tempo (come in qualche modo chi non fa ancora alcun test unitario) e devo dire, l'ultima parte sulla verifica della chiamata in circostanze controllate e il codice più flessibile che viene fornito con esso, ha fatto scattare ogni sorta di cose in posizione. Grazie.
plast1k,

12

Ad un certo punto il valore deve essere inizializzato, e perché non più vicino al consumo?

Perché potrebbe essere necessario riutilizzare quel codice, con un valore diverso da quello generato internamente. La possibilità di inserire il valore che si intende utilizzare come parametro, garantisce che è possibile generare tali valori in base a qualsiasi momento desiderato, non solo "ora" (con "ora" che significa quando si chiama il codice).

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

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

Inoltre, lo scopo del metodo nella mia mente è quello di restituire un valore basato sul tempo corrente, rendendolo un parametro che implica che questo scopo possa / debba essere modificato.

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, col tempo la tua percentuale di riutilizzo del codice aumenterà.


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. Tu chiedi:

Ad un certo punto il valore deve essere inizializzato, e perché non più vicino al consumo?

Proprio perché, nell'esempio a cui ti stai riferendo, rende quel codice non verificabile. A meno che non eseguiate solo un sottoinsieme dei test in diversi momenti della giornata. Oppure si resetta l'orologio di sistema. O qualche altra soluzione alternativa. Tutto ciò è peggio che semplicemente rendere flessibile il tuo codice.

Oltre ad essere inflessibile, quel piccolo metodo in questione ha due responsabilità: (1) ottenere il tempo di sistema e quindi (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 minimo impatto sul resto del codice. In questo modo il codice sopra sarà più semplice, con l'effetto collaterale di essere sistematicamente testabile.


1
Quindi dovresti testare la mattina presto per verificare di ottenere un risultato di "Notte" quando lo desideri. È difficile. Ora supponiamo che tu voglia verificare che la gestione della data sia corretta il 29 febbraio 2016 ... E alcuni programmatori iOS (e probabilmente altri) sono afflitti dall'errore di un principiante che fa casini poco prima o dopo l'inizio dell'anno, come fai prova per quello. E per esperienza, controllerei la gestione della data il 2 febbraio 2020.
gnasher729

1
@ gnasher729 Esattamente il mio punto. "Rendere testabile questo codice" è una semplice modifica che può risolvere molti problemi (test). Se non vuoi automatizzare i test, suppongo che il codice sia passabile così com'è. Ma sarebbe meglio una volta "testabile".
Eric King,

9

Certamente ha un costo, ma alcuni sviluppatori sono così abituati a pagarlo che hanno dimenticato il costo. Per il tuo esempio, ora hai due unità anziché una, hai richiesto il codice chiamante per inizializzare e gestire una dipendenza aggiuntiva e, sebbene GetTimeOfDaysia più verificabile, sei di nuovo nella stessa barca per testare la tua nuova IDateTimeProvider. È solo che se si hanno buoni test, i benefici di solito superano i costi.

Inoltre, in una certa misura, scrivere un codice testabile ti incoraggia a progettare il tuo codice in un modo più gestibile. Il nuovo codice di gestione delle dipendenze è fastidioso, quindi ti consigliamo di raggruppare tutte le tue funzioni dipendenti dal tempo, se possibile. Ciò può aiutare a mitigare e correggere i bug come, ad esempio, quando si carica una pagina in un limite di tempo, con alcuni elementi renderizzati utilizzando il tempo precedente e alcuni utilizzando il tempo successivo. Può anche velocizzare il programma evitando ripetute chiamate di sistema per ottenere l'ora corrente.

Naturalmente, questi miglioramenti dell'architettura dipendono fortemente da qualcuno che nota le opportunità e le implementa. Uno dei maggiori pericoli di concentrarsi così strettamente sulle unità è perdere di vista il quadro generale.

Molti framework di unit test ti consentono di rattoppare scimmia un oggetto finto in fase di esecuzione, il che ti consente di ottenere i vantaggi della testabilità senza tutto il caos. L'ho anche visto fatto in C ++. Esamina questa abilità in situazioni in cui sembra che il costo della testabilità non valga la pena.


+1: è necessario migliorare il design e l'architettura per semplificare la scrittura dei test delle unità.
BЈовић,

3
+ - è l'architettura del tuo codice che conta. I test più semplici sono solo un felice effetto collaterale.
gbjbaanb,

8

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

In generale, il codice testabile è un codice malleabile. È in piccoli, discreti, coesivi, pezzi, quindi i singoli bit possono essere chiamati per il riutilizzo. È ben organizzato e ben chiamato (per poter testare alcune funzionalità presti maggiore attenzione alla denominazione; se non stavi scrivendo test il nome per una funzione monouso avrebbe meno importanza). Tende ad essere più parametrico (come il tuo esempio di tempo), quindi aperto da usare da altri contesti rispetto allo scopo originale previsto. È ASCIUTTO, quindi meno disordinato e più facile da comprendere.

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


non sono d'accordo sul fatto che sia DRY: il wrapping di GetCurrentTime in un metodo MyGetCurrentTime sta ripetendo moltissimo la chiamata del sistema operativo senza alcun vantaggio se non quello di assistere gli strumenti di test. Questo è solo il più semplice degli esempi, in realtà peggiorano molto.
gbjbaanb,

1
"replicare la chiamata del sistema operativo senza alcun vantaggio" - fino a quando non si esegue in esecuzione su un server con un solo orologio, parlando con un server aws in un fuso orario diverso e che rompe il codice, quindi è necessario passare attraverso tutto il codice e aggiornalo per usare MyGetCurrentTime, che invece restituisce UTC. ; inclinazione dell'orologio, ora legale e ci sono altri motivi per cui potrebbe non essere una buona idea fidarsi ciecamente della chiamata del sistema operativo, o almeno avere un unico punto in cui è possibile inserire un'altra sostituzione.
Andrew Hill,

8

La scrittura di codice testabile è importante se vuoi essere in grado di dimostrare che il tuo codice funziona davvero.

Tendo a concordare con i sentimenti negativi di deformare il tuo codice in contorsioni atroci solo per adattarlo a un particolare framework di test.

D'altra parte, tutti qui, a un certo punto, hanno dovuto fare i conti con quella funzione magica lunga 1.000 linee che è semplicemente odiosa da gestire, praticamente non può essere toccata senza rompere uno o più oscuri, non evidenti dipendenze da qualche altra parte (o da qualche parte all'interno di se stesso, dove la dipendenza è quasi impossibile da visualizzare) ed è praticamente per definizione non verificabile. L'idea (che non è senza merito) che i framework di test siano diventati esagerati non dovrebbe essere presa come una licenza gratuita per scrivere codice di scarsa qualità e non verificabile, secondo me.

Gli ideali di sviluppo guidati dai test tendono a spingerti verso la scrittura di procedure a responsabilità singola, ad esempio, e questa è sicuramente una buona cosa. Personalmente, dico acquista in un'unica responsabilità, unica fonte di verità, ambito controllato (nessuna variabile globale strana) e riduci al minimo le fragili dipendenze e il tuo codice sarà testabile. 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, un codice così intelligente, o così lungo e / o interdipendente da non essere facilmente compreso da un altro essere umano non è un buon codice. E anche, per coincidenza, non è un codice che può essere facilmente testato.

Quindi, vicino al mio sommario, il codice testabile è un codice migliore ?

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

Ma credo che un codice migliore tenda ad essere anche un codice verificabile .

E che se stai parlando di software serio da utilizzare in attività serie, la spedizione di codice non testato non è la cosa più responsabile che potresti fare con i soldi del tuo datore di lavoro o dei tuoi clienti.

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


1
"codice migliore tende ad essere anche codice verificabile" Questa è la chiave. Renderlo testabile non lo rende migliore. Renderlo migliore spesso lo rende testabile e i test spesso forniscono informazioni che è possibile utilizzare per renderlo migliore, ma la semplice presenza di test non implica qualità e ci sono (rare) eccezioni.
anaximander,

1
Esattamente. Considera il contrapositivo. Se è un codice non verificabile, non è testato. Se non è testato, come fai a sapere se funziona o meno se non in una situazione dal vivo?
pjc50,

1
Tutti i test dimostrano che il codice supera i test. Altrimenti il ​​codice testato dall'unità sarebbe privo di bug e sappiamo che non è il caso.
wobbily_col,

@anaximander Exactly. Esiste almeno la possibilità che la semplice presenza di test sia una controindicazione che si traduce in un codice di qualità più scadente se tutto l'attenzione è solo sulla selezione delle caselle di controllo. "Almeno sette test unitari per ciascuna funzione?" "Dai un'occhiata." Ma credo davvero che se il codice è un codice di qualità, sarà più facile testarlo.
Craig,

1
... ma fare una burocrazia fuori dai test può essere uno spreco totale e non produrre informazioni utili o risultati affidabili. Indipendentemente; Vorrei che qualcuno avesse testato il bug SSL Heartbleed , sì? o il bug fallito di Apple ?
Craig,

5

Per me, tuttavia, sembra eccessivo passare il tempo come argomento.

Hai ragione e con il beffardo puoi rendere testabile il codice ed evitare di passare il tempo (intenzione di gioco di parole indefinita). Codice di esempio:

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

Ora supponiamo che tu voglia testare cosa succede durante un secondo bisestile. Come dici tu, per testarlo nel 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 supportava i secondi bisestili, il codice di test sarebbe simile al seguente:

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)

È possibile verificarlo, ma il codice è più complesso del necessario e i test non riescono ancora ad esercitare in modo affidabile il ramo di codice che verrà utilizzato dalla maggior parte del codice di produzione (ovvero, non passando un valore now). Per ovviare a questo, usa una simulazione . A partire dal codice di produzione originale:

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

Codice di prova:

@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)

Ciò offre numerosi vantaggi:

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

Da un lato, si spera che i futuri quadri beffardi renderanno le cose più facili. Ad esempio, poiché è necessario fare riferimento alla funzione derisa come una stringa, non è possibile fare in modo che gli IDE cambino automaticamente quando time_of_dayinizia a utilizzare un'altra fonte per il tempo.


Cordiali saluti: l'argomento predefinito è sbagliato. Sarà definito solo una volta, quindi la tua funzione restituirà sempre l'ora in cui è stata valutata per la prima volta.
ahruss,

4

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

Perché ci aiuta ad avvicinarci? Nella produzione, il nostro codice opera all'interno dell'ambiente di produzione, compresa 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 un cambiamento . Stiamo utilizzando le unità in modi diversi, con input diversi (simulazioni, input non validi che potrebbero non essere mai passati, ecc.) Rispetto a quelli che utilizzeremmo in produzione.

Questo prepara il nostro codice per il giorno in cui si verificano cambiamenti nel nostro sistema. Supponiamo che il nostro calcolo del tempo debba richiedere un tempo diverso in base a un fuso orario. Ora abbiamo la possibilità di passare in quel momento e non è necessario apportare modifiche al codice. Quando non vogliamo passare un tempo e vogliamo usare l'ora corrente, potremmo semplicemente usare un argomento predefinito. Il nostro codice può essere modificato perché è testabile.


4

Dalla mia esperienza, una delle decisioni più importanti e di vasta portata che prendi quando costruisci un programma è come suddividere il codice in unità (dove "unità" è usato nel suo senso più ampio). Se si utilizza un linguaggio OO basato su classi, è necessario suddividere tutti i meccanismi interni utilizzati per implementare il programma in un numero di classi. Quindi è necessario suddividere il codice di ogni classe in un numero di metodi. In alcune lingue, la scelta è come suddividere il codice in funzioni. Oppure, se fai la cosa SOA, devi decidere quanti servizi costruirai e cosa andrà in ciascun servizio.

La suddivisione scelta ha un effetto enorme sull'intero processo. Le buone scelte facilitano la scrittura del codice e comportano un minor numero di bug (anche prima di iniziare il test e il debug). Semplificano il cambiamento e la manutenzione. È interessante notare che si scopre che una volta che si riscontra un buon guasto, di solito è anche più facile testarlo rispetto a uno scadente.

Perché è così? Non credo di poter capire e spiegare tutti i motivi. Ma una ragione è che una buona ripartizione significa invariabilmente scegliere una "granulometria" moderata per le unità di implementazione. Non vuoi stipare troppe funzionalità e troppa logica in una singola classe / metodo / funzione / modulo / ecc. Ciò semplifica la lettura e la scrittura del codice, ma semplifica anche il test.

Non è solo quello, però. Una buona progettazione interna significa che il comportamento previsto (input / output / ecc.) Di ciascuna unità di implementazione può essere definito in modo chiaro e preciso. Questo è importante per i test. Una buona progettazione di solito significa che ogni unità di implementazione avrà un moderato numero di dipendenze dalle altre. Ciò semplifica la lettura e la comprensione del codice da parte degli altri, ma semplifica anche il test. Le ragioni continuano; forse altri possono articolare più ragioni che io non posso.

Per quanto riguarda l'esempio nella tua domanda, non credo che "una buona progettazione del codice" equivalga a dire che tutte le dipendenze esterne (come una dipendenza dall'orologio di 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, agiscono sul filesystem e così via, ciò non significa che non è possibile testare l'unità di codice in modo isolato. Il trucco è utilizzare una versione speciale delle librerie standard che consente di falsificare i valori di ritorno delle funzioni di sistema. Non ho mai visto altri menzionare questa tecnica, ma è abbastanza semplice avere a che fare con molte lingue e piattaforme di sviluppo. (Si spera che il tuo runtime della lingua sia open source e facile da costruire. Se l'esecuzione del codice comporta un passaggio di collegamento, si spera che sia anche facile controllare a quali librerie è collegato.)

In breve, il codice verificabile non è necessariamente un codice "buono", ma il codice "buono" è di solito verificabile.


1

Se stai seguendo i principi SOLID sarai dalla parte giusta, specialmente se estendi questo con KISS , DRY e YAGNI .

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

Se si tratta di un metodo più complesso in cui si manipolano i dati e si desidera essere sicuri che funzionerà anche se è necessario modificare la logica interna, sarebbe un'ottima chiamata per un metodo di prova. Molte volte ho dovuto cambiare un pezzo di codice dopo diversi giorni / settimane / mesi ed ero davvero felice di avere il caso di test. Quando ho sviluppato il metodo per la prima volta, l'ho testato con il metodo di prova ed ero sicuro che funzionasse. Dopo la modifica, il mio codice di test principale funzionava ancora. Quindi ero certo che la mia modifica non avesse rotto alcuni vecchi codici in produzione.

Un altro aspetto della scrittura dei test è mostrare ad 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 .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.