Le mock nei test unitari sono pericolose nei linguaggi dinamici?

5

Ho iniziato a fare affidamento su un framework di simulazione in php per i miei test di unità.

La mia preoccupazione è che con un linguaggio dinamico, non c'è modo di imporre un tipo di ritorno. Quando fai il mocking, devi assicurarti che il tipo di ritorno del tuo metodo di simulazione sia corretto.

Prendi ad esempio la seguente interfaccia

interface MyInterface {

  /**
   * @return SomeObject
   */
   public function myMethod();

}

Se stiamo testando un'unità con una classe che richiede un'istanza di MyInterface , prendiamo in giro la funzione myMethod per restituire un SomeObject noto.

La mia preoccupazione è ciò che accade se accidentalmente il nostro metodo di simulazione restituisce un tipo diverso di oggetto, ad es. SomeOtherObject, and SomeOtherObject and SomeObject are not the same duck type (don't have the same methods). We'll now have code that is relying on having SomeOtherObject , when all implementations of MyInterface myMethod will return SomeObject'.

Ad esempio, potremmo scrivere il seguente codice di produzione;

$someOtherObject = $myInterfaceImp->myMethod();
$newObj = new NewObject($someOtherObject);

Speriamo che il problema sia ovvio. Stiamo passando ciò che pensiamo sia SomeOtherObject in un costruttore. I nostri test passeranno, ma è molto probabile che in produzione avremo fallimenti come myMethod() restituirà effettivamente SomeObject . Proviamo quindi a passare questo nel costruttore e ottenere un errore se è presente un controllo di tipo o viene chiamato un metodo inesistente.

A meno che non scriviamo test di integrazione e accettazione molto dettagliati utilizzando le reali implementazioni (in pratica dovremmo assicurarci che coprano tutti i test che facciamo a livello di unità) potremmo non renderci conto che abbiamo un problema finché non ne riceviamo alcuni errore di runtime in produzione.

Oltre alla disciplina per sviluppatori, esiste un modo per mitigare il rischio di mock e implementazioni non corrette?

    
posta Gaz_Edge 09.05.2015 - 17:04
fonte

5 risposte

6

La risposta a questa domanda "sono motti in unit test pericolosi in linguaggi dinamici?" è "Hell yes!"

I seguenti articolo spiegano i pericoli.

Descrivono uno scenario in cui il nome del metodo di un'interfaccia è cambiato. Ad esempio, cambiando il nome di un metodo da error() a errors() . Se stiamo prendendo in giro il metodo error() nei nostri test, non diventeranno mai rossi per avvertirci che abbiamo un problema.

Invece, i mock dovrebbero essere usati con parsimonia. I comportamenti dell'unità in prova dovrebbero essere testati piuttosto che testare i dettagli dell'implementazione, cosa che avviene come conseguenza dell'utilizzo di mock nei test unitari. Le classi vere dovrebbero essere utilizzate come dipendenze, invece di affidarsi eccessivamente a mock.

    
risposta data 12.05.2015 - 20:45
fonte
2

Direi che i mock sono pericolosi in ogni lingua.

Diciamo che chiamo:

priceCalculator.calculate(
    qty,
    item,
    isPlusCustomer,
    isSenior); 

Cosa succede se:

  1. isPlusCustomer e isSenior dovrebbero essere nell'altro ordine?
  2. qty doveva essere un peso, non una quantità.
  3. hai avuto bisogno di inizializzare priceCalculator prima di usarlo?

Ci sono molti modi in cui i mock non riescono a rilevare i problemi. Alcuni sottoinsiemi sono risolti da lingue tipizzate staticamente. Ma in generale il problema rimane.

Ci sono due soluzioni di cui sono consapevole.

Il primo è il test del contratto. L'idea qui è che se fai un test che dice via mock:

expect(priceCalculator.calculate(5, item, true, false)).returns(5.00);

Hai bisogno di un test del contratto altrove che dice:

assert(new PriceCalculator().calculate(5, item, true, false)).returns(5.00);

In modo che ogni aspettativa che imposti abbia un test di corrispondenza per assicurarti che funzioni effettivamente nel modo in cui lo rivendichi.

Il secondo non è usare i mock. Invece di prendere in giro il PriceCalculator, basta usare il vero PriceCalculator. Invece di preoccuparti di deridere correttamente il PriceCalculator, puoi effettivamente testare il vero PriceCalculator.

    
risposta data 09.05.2015 - 18:45
fonte
2

Non è necessario testare il tipo di ritorno corretto in un linguaggio tipizzato dinamicamente, a meno che il metodo non sia specificamente progettato per restituire un tipo specifico (cioè una fabbrica). Affidati invece ad altri tipi di test più significativi da una prospettiva logica del programma.

Per dare un esempio semplice (e certamente forzato), se mi aspettassi che un metodo restituisse un long , invece restituiva un int , il valore restituito sarebbe errato per qualsiasi risultato numerico troppo grande per entra in un int .

L'intero punto di utilizzo di un linguaggio tipizzato in modo dinamico è che non devi pensare tanto ai tipi. Altrimenti, perché scambieresti una lingua strongmente tipizzata per una tipizzata dinamicamente con test di tipo, quando avresti potuto lasciare che il compilatore facesse tutto il lavoro per te automaticamente?

La maggior parte delle lingue con tipi dinamici presenta pratiche standard che è possibile utilizzare per ridurre al minimo i rischi. Ad esempio, in Javascript, utilizziamo === invece di == per evitare la coercizione di tipo imprevisto durante i confronti numerici e utilizzare i numeri ridotti a pochi centesimi nei calcoli monetari per evitare errori di arrotondamento binario (tutti i numeri sono in virgola mobile in Javascript).

Vale la pena notare: i metodi di fabbrica sono meno importanti nelle lingue dinamicamente tipizzate rispetto a quelle statiche.

    
risposta data 09.05.2015 - 17:26
fonte
2

Prima di iniziare aggiungerò il contesto alla domanda per evidenziare il problema. Prendere in considerazione:

class SUT {
  public function tested(MyInterface $x) {
    $this->hidden($x->myMethod());
  }
  protected function hidden($x) {
    $x->aMethodOfSomeObject()
  }
}

//Somewhere in test:
$sutInstance($myInterfaceMock);

Ora. Your MyInterface :: MyMethod restituisce SomeObject e il test esegue il suo lavoro. Ma quando cambi MyInterface :: MyMethod per restituire SomeOtherObject il test passerà ancora (falso positivo con errato)
E c'è anche di peggio: se rimuovi MyMethod da MyInterface questo test continuerà comunque.

Cattive notizie:

Non puoi farci molto nel codice di prova.
Deve essere fatto a livello di struttura irridente.
Ho controllato PHPUnit e c'è un compito per deridere metodi non esistenti. È stato creato nel 2010 e non è ancora stato chiuso (quindi dal mio punto di vista i creatori di PHPUnit non sono interessati a creare questa funzionalità crutale).

Buone notizie:

Puoi contribuire a PHPUnit o qualsiasi altra cosa usi e risolvere questi problemi. Molto tempo fa, quando non ero sovraccarico di lavoro, ho creato una piccola biblioteca di derisione e ho cercato di risolvere questi problemi ( link ).
Il modo in cui l'ho fatto è stato l'analisi di PHPDoc e la ricerca di annotazioni @return. Ogni volta che veniva chiamato il metodo mocked (in questo caso MyInstace :: MyMethod), il tipo restituito era controllato dal tipo di analisi dall'annotazione.
Nell'esempio, dopo aver modificato $ mock- > il risultato MyMethod (SomeObject) è testato sul tipo (digitare dall'annotazione è SomeOtherObject) e poiché SomeObject non è un'istanza del test SomeOtherObject non riesce. (Esempio: il metodo mocked restituisce string quando @return intero è definito nell'annotazione del metodo: link riga 1242)

  1. Puoi provare a programmare in modo offensivo. Controllare tutti gli argomenti nei metodi se i tipi sono corretti.
    Tuttavia, il test non dirà nulla, ma esploderà prima (forse durante il lavoro degli sviluppatori, i tester lo troveranno o il test comportamentale ti farà sapere se ne usi) e meno probabilmente esploderà in faccia all'utente finale.

  2. Usa il suggerimento del tipo ogni volta che è possibile. Durante lo sviluppo sarà molto più facile trovare metodi (e il loro test) che dovresti controllare.

  3. Migliora i tuoi strumenti di test. Questa è un'elaborazione sulla mia idea precedente dalla parte "Buona nuova". Devi investire del tempo per questo e solo se stai usando le annotazioni per il valore di ritorno (e credo che sia così). In caso di PHPUnit puoi sostituire

    $myInterfaceMock->expects($this->any())
        ->method('myMethod')
        ->will($this->returnValue($someObjectMock)); 
    //with
    $myInterfaceMock->expects($this->any())
        ->method('myMethod')
        ->will($this->myReturnValue($someObjectMock, 'MyInterface', 'MyMethod'));
    
    //and in your test class:
    
    protected function myReturnValue($value, $class, $method) {
        if (!checkMethodExists($class, $method)) {
            throw BadTestException();
        }
    
        $expectedType = getExpectedType($class, $method);
    
        if (!isTypeOf($value, $expectedType) {
            throw BadTestException();
        }
    
        return $value;
    }
    

    L'implementazione di lettori di annotazioni pronte per il 98% degli scenari più comuni dovrebbe richiedere circa 2 giorni.
    L'estensione per più metodi PHPUnit come returnCallback o onConsecutiveCalls e così via dovrebbe richiedere 1 giorno in più.

  4. Non affidarti troppo a mock. Usa oggetti reali invece di mock quando è possibile. Se crei real $ myInstanceObject che crea SomeOtherObject invece di SomeObject previsto dal metodo testato, il test fallirebbe.

  5. Penso che questo problema sia un po 'oltre il dominio di testing unitario. Questo tipo di errori dovrebbe essere gestito a livello di test funzionale o comportamentale.

risposta data 10.05.2015 - 00:37
fonte
1

is there any way to mitigate the risk of incorrect mocks and implementations?

In python-mock , puoi specificare una specifica simulata . Una specifica vincola quali metodi e attributi sono disponibili nell'oggetto mocked, quindi riceverai un errore se il sistema sotto test (SUT) tenta di chiamare o accedere a qualcosa di inaspettato.

My concern is what happens if we accidentally have our mocked method return a different type of object e.g. SomeOtherObject? We'll now have code that is relying on having SomeOtherObject, when all implementations of MyInterface myMethod will return SomeObject.

Non dovresti preoccuparti dei tipi effettivi. I tipi effettivi dovrebbero essere considerati dettagli di implementazione; è il tipo di anatra che dovresti essere più preoccupato. Verificare che i metodi corretti vengano chiamati con gli argomenti corretti, in genere non è necessario testare il tipo effettivo del valore restituito. Invece di affermare il tipo effettivo, asserisci lo stato che ti aspetti dall'oggetto, dopo il SUT.

Unless we write very detailed integration and acceptance tests (basically we'd have to ensure they covered all the testing we do an the unit level) we may not ever realise that we have an issue until we receive some runtime error in production.

I mock sono per i test unitari, non dovresti usare i mock per i test di integrazione e accettazione. I test di integrazione e accettazione dovrebbero utilizzare il più possibile gli oggetti reali e utilizzare solo una simulazione minimale per sostituire i sistemi esterni. Ciò potrebbe non essere completamente sotto il tuo controllo.

    
risposta data 10.05.2015 - 12:01
fonte

Leggi altre domande sui tag