6

Nel nostro team di sviluppatori JavaScript abbiamo abbracciato lo stile di scrittura redux/react di puro codice funzionale. Tuttavia, sembra che abbiamo difficoltà a testare il nostro codice. Si consideri il seguente esempio:Come testare un albero di chiamate di funzione pure in isolamento?

function foo(data) { 
    return process({ 
     value: extractBar(data.prop1), 
     otherValue: extractBaz(data.prop2.someOtherProp) 
    }); 
} 

Questa funzione di chiamata dipende chiamate verso process, extractBar e extractBaz, ognuno dei quali può chiamare altre funzioni. Insieme, potrebbero richiedere una simulazione non banale per il parametro data da costruire per il test.

Nel caso in cui accettassimo la necessità di creare un simile oggetto fittizio e lo facessimo effettivamente nei test, troviamo rapidamente casi di test difficili da leggere e mantenere. Inoltre, molto probabilmente porta a testare sempre la stessa cosa, poiché probabilmente dovrebbero essere scritti anche i test unitari per , extractBar e extractBaz. Il test per ogni possibile caso limite implementato da queste funzioni tramite l'interfaccia foo è ingombrante.


Abbiamo alcune soluzioni in mente, ma non mi piacciono molto, in quanto nessuno dei due sembra uno schema che abbiamo visto in precedenza.

Soluzione 1:

function foo(data, deps = defaultDeps) { 
    return deps.process({ 
     value: deps.extractBar(data.prop1), 
     otherValue: deps.extractBaz(data.prop2.someOtherProp) 
    }); 
} 

Soluzione 2:

function foo(
    data, 
    processImpl = process, 
    extractBarImpl = extractBar, 
    extractBazImpl = extractBaz 
) { 
    return process({ 
     value: extractBar(data.prop1), 
     otherValue: extractBaz(data.prop2.someOtherProp) 
    }); 
} 

Soluzione 2 inquina foo firma del metodo molto rapidamente il numero di chiamate funzione dipendente aumenta.

Soluzione 3:

Basta accettare il fatto che foo è un'operazione complicata composto e testarlo nel suo complesso. Si applicano tutti gli inconvenienti


Si prega di suggerire altre possibilità. Immagino che questo sia un problema che la comunità di programmazione funzionale deve aver risolto in un modo o nell'altro.

risposta

6

Probabilmente non hai bisogno di nessuna delle soluzioni che hai considerato. Una delle differenze tra la programmazione funzionale e la programmazione imperativa è che lo stile funzionale dovrebbe produrre codice più facile da ragionare. Non solo nel senso di "giocare al compilatore" mentalmente e simulare cosa accadrebbe a un dato insieme di input, ma ragionare sul tuo codice in un senso più matematico.

Ad esempio, l'obiettivo del test delle unità è di testare "tutto ciò che può rompersi". Guardando il primo frammento di codice che hai postato, possiamo ragionare sulla funzione e chiedere, "Come potrebbe questa funzione interrompere?" È una funzione abbastanza semplice che non abbiamo affatto bisogno di giocare con il compilatore. Possiamo solo dire che la funzione si interromperebbe se la funzione process() non restituisce un valore corretto per un determinato insieme di input, ad esempio se ha restituito un risultato non valido o se ha generato un'eccezione. Questo a sua volta implica che dobbiamo anche verificare se extractBar() e extractBaz() restituiscono risultati corretti, al fine di passare i valori corretti a process().

Quindi, in realtà, è solo bisogno di verificare se foo() tiri eccezioni impreviste, perché tutto ciò che fa è chiamata process(), e si dovrebbe essere testando process() nella propria serie di test di unità. Stessa cosa con extractBar() e extractBaz(). Se queste due funzioni restituiscono risultati corretti quando vengono forniti input validi, passeranno i valori corretti a process() e se process() produce risultati corretti quando vengono forniti input validi, allora anche foo() restituirà risultati corretti.

Si potrebbe dire "E gli argomenti? Che cosa succede se estrae il valore errato dalla struttura data?" Ma può davvero rompersi? Se osserviamo la funzione, sta usando la notazione del punto JS principale per accedere alle proprietà su un oggetto. Non testiamo la funzionalità principale del linguaggio stesso nei nostri test unitari per la nostra applicazione. Possiamo solo guardare il codice, ragione per cui sta estraendo i valori basati sull'accesso alla proprietà dell'oggetto hard-coded e procediamo con i nostri altri test.

Questo non vuol dire che puoi semplicemente buttare via i tuoi test unitari, ma molti programmatori funzionali con esperienza trovano che hanno bisogno di molti meno test, perché devi solo testare le cose che possono rompersi, e la programmazione funzionale riduce il numero di oggetti fragili in modo da poter concentrare i test sulle parti che sono realmente a rischio.

E a proposito, se si sta lavorando con dati complessi, e si teme che potrebbe essere difficile, anche con FP, ragionare su tutte le possibili permutazioni, si potrebbe voler esaminare i test generativi. Penso che ci siano alcune librerie JS là fuori per quello.