Dipende implicitamente da funzioni pure cattive (in particolare, per il test)?

8

Per estendere un po 'il titolo, sto cercando di arrivare a qualche conclusione sul fatto che sia necessario o no dichiarare esplicitamente (cioè iniettare) funzioni pure da cui dipende qualche altra funzione o classe.

È un dato pezzo di codice meno testabile o peggio progettato se utilizza funzioni pure senza richiederlo? Mi piacerebbe arrivare a una conclusione sull'argomento, per qualsiasi tipo di pura funzione da semplici funzioni native (ad esempio max() , min() - indipendentemente dal linguaggio) a custom, più complicate che a loro volta possono dipendere implicitamente su altre pure funzioni.

La solita preoccupazione è che se qualche codice usa solo una dipendenza direttamente, non sarai in grado di testarlo da solo, cioè testerai allo stesso tempo tutte quelle cose che hai portato silenziosamente con te. Ma questo aggiunge un po 'di scia se si deve farlo per ogni piccola funzione, quindi mi chiedo se questo vale ancora per le funzioni pure, e perché o perché no.

    
posta DanielM 13.10.2017 - 17:55
fonte

4 risposte

10

No, non è male

I test che scrivi non devono interessare come viene implementata una determinata classe o funzione. Piuttosto, dovrebbe garantire che producano i risultati desiderati indipendentemente da come sono implementati esattamente.

Ad esempio, considera la seguente classe:

Coord2d{
    float x, y;

    ///Will round to the nearest whole number
    Coord2d Round();
}

Vorresti testare la funzione 'Round' per assicurarti che restituisca ciò che ti aspetti, indipendentemente da come la funzione è effettivamente implementata. Probabilmente scriverebbe un test simile al seguente;

Coord2d testValue(0.6, 0.1)
testValueRounded = testValue.Round()
CHECK( testValueRounded.x == 1.0 )
CHECK( testValueRounded.y == 0.0 )

Da un punto di vista del test, fintanto che la funzione Coord2d :: Round () restituisce ciò che ti aspetti, non ti interessa come è implementato.

In alcuni casi, l'iniezione di una funzione pura potrebbe essere un modo davvero brillante per rendere una classe più testabile o estensibile.

Nella maggior parte dei casi, tuttavia, come la funzione Coord2d :: Round () sopra, non è necessario iniettare una funzione min / max / floor / ceil / trunc. La funzione è progettata per arrotondare i suoi valori al numero intero più vicino. I test che scrivi dovrebbero controllare che la funzione faccia questo.

Infine, se vuoi testare il codice da cui dipende l'implementazione di una classe / funzione, puoi farlo semplicemente scrivendo i test per la dipendenza.

Ad esempio, se la funzione Coord2d :: Round () è stata implementata in questo modo ...

Coord2d Round(){
    return Coord2d( floor(x + 0.5f),  floor(y + 0.5f))
}

Se si desidera testare la funzione 'floor', è possibile farlo in un test dell'unità separato.

CHECK( floor (1.436543) == 1.0)
CHECK( floor (134.936) == 134.0)
    
risposta data 13.10.2017 - 18:21
fonte
4

Is implicitly depending on pure functions bad

Dal punto di vista del test - No, ma solo nel caso delle funzioni pure , quando la funzione restituisce sempre lo stesso output per lo stesso input.

Come si testano le unità che usano la funzione esplicitamente iniettata?
Inietterete una funzione falsa (falsa) e verificate che la funzione sia stata chiamata con argomenti corretti.

Ma poiché abbiamo la funzione pure , che restituisce sempre lo stesso risultato per gli stessi argomenti, il controllo degli argomenti di input è uguale per il controllo del risultato di output.

Con le funzioni pure non è necessario configurare dipendenze / stato extra.

Come ulteriore vantaggio otterrai un migliore incapsulamento, verificherai il comportamento effettivo dell'unità.
E molto importante dal punto di vista del test - sarai libero di rifattorizzare il codice interno dell'unità senza cambiare test - ad esempio decidi di aggiungere un altro argomento per la pura funzione - con una funzione esplicitamente iniettata (beffata nei test) di cui avrai bisogno cambia la configurazione del test, dove con la funzione usata in modo implicito non fai nulla.

Posso immaginare la situazione quando è necessario iniettare una funzione pura - è quando si desidera offrire ai consumatori il comportamento dell'unità.

public decimal CalculateTotalPrice(Order order, Func<Customer, decimal> calculateDiscount)
{
    // Do some calculation based on order data
    var totalPrice = order.Lines.Sum(line => line.Price);

    // Then
    var discountAmount = calculateDiscount(order.Customer);

    return totalPrice - discountAmount;
}

Per il metodo sopra ti aspetti che il calcolo dello sconto possa essere modificato dai consumatori, quindi devi escludere il test della logica dai test unitari per il metodo CalcualteTotalPrice .

    
risposta data 13.10.2017 - 20:35
fonte
4

In un mondo ideale di programmazione OO SOLIDO si inietterebbe ogni comportamento esterno. In pratica, utilizzerai sempre alcune funzioni semplici (o non così semplici).

Se stai calcolando il massimo di una serie di numeri, probabilmente sarebbe eccessivo per iniettare una funzione max e deriderla nei test unitari. Di solito non ti interessa disaccoppiare il tuo codice da un'implementazione specifica di max e testarlo in isolamento.

Se stai facendo qualcosa di complesso come trovare un percorso di costo minimo in un grafico, allora è meglio iniettare l'implementazione concreta per la flessibilità e deriderla nei test unitari per ottenere prestazioni migliori. In questo caso potrebbe valere la pena di disaccoppiare l'algoritmo di individuazione dei percorsi dal codice che lo utilizza.

Non ci può essere una risposta per "qualsiasi tipo di pura funzione", devi decidere dove tracciare la linea. Ci sono troppi fattori coinvolti in questa decisione. Alla fine devi pesare i benefici contro i problemi che il disaccoppiamento ti dà, e questo dipende da te e dal tuo contesto.

Vedo nei commenti che mi chiedi della differenza tra l'iniezione di classi (in realtà si inietta oggetti) e le funzioni. Non c'è differenza, solo le caratteristiche del linguaggio lo fanno apparire diverso. Da un punto di vista astratto il richiamo di una funzione non è diverso dal chiamare il metodo di un oggetto e si inietta (o no) la funzione per gli stessi motivi per cui si immette (o no) l'oggetto: per disaccoppiare quel comportamento dal codice chiamante e avere il possibilità di utilizzare diverse implementazioni del comportamento e decidere da qualche altra parte quale implementazione utilizzare.

Quindi sostanzialmente qualunque criterio tu trovi valido per l'iniezione delle dipendenze puoi usarlo a prescindere che la dipendenza sia un oggetto o una funzione. Potrebbero esserci alcune sfumature a seconda della lingua (ad esempio se stai usando java questo è un non-problema).

    
risposta data 13.10.2017 - 18:47
fonte
3

Le funzioni pure non influiscono sulla testabilità della classe a causa delle loro proprietà:

  • il loro output (o errore / eccezione) dipende solo dai loro input;
  • il loro output non dipende dallo stato del mondo;
  • la loro operazione non modifica lo stato del mondo.

Ciò significa che sono approssimativamente nello stesso regno dei metodi privati, che sono completamente sotto il controllo della classe, con il vantaggio aggiuntivo di non dipendere nemmeno dallo stato corrente dell'istanza (cioè "questo"). La classe utente "controlla" l'output perché controlla completamente gli input.

Gli esempi che hai dato, max () e min (), sono deterministici. Tuttavia, random () e currentTime () sono procedure, non funzioni pure (dipendono da / modificano lo stato globale fuori banda), ad esempio. Questi ultimi due dovrebbero essere iniettati per rendere possibile il test.

    
risposta data 15.10.2017 - 03:26
fonte

Leggi altre domande sui tag