Come dovrei unità testare una funzione impura che chiama una serie di metodi?

3

Desidero testare il seguente metodo:

class MyClass(object):
    ...
    def my_method(self, args):
        ...  # Code which transforms args into arg1, arg2, ..., argN
        self.A.other_method1(arg1, arg2)
        self.A.other_method2(arg1, arg2, arg3)
        self.A.other_method3(arg1)
        self.B.other_method4(arg1, arg2)
        self.B.other_method5(arg1, arg2, arg4)
        self.B.other_method6(arg1, arg2, arg5, arg6)

Ovviamente questo è un esempio artificiale. Ho creato questo esempio per concentrarmi su quelli che ritengo siano i punti essenziali:

  • my_method prima fa un lavoro semplice per trasformare i valori che è stato dato. Non mi interessa come testare questo aspetto.
  • my_method quindi chiama una sequenza di metodi su altri attributi A e B della classe. Non mi interessa come testare questi altri metodi.
  • La sequenza dei richiami di metodo cambia A e B . Quindi my_method è una funzione non pura. Non credo di poter riprogettare per rendere la funzione pura, poiché tutti gli altri metodi sono I / O.

Il mio attuale approccio consiste nel mettere alla prova A e B usando unittest.mock.MagicMock , e poi asserire che questi metodi sono stati chiamati nell'ordine richiesto, con gli argomenti previsti. Il mio codice di test si presenta quindi come segue:

from unittest.mock import MagicMock, call

class MyTest(unittest.TestCase):
    def test_MyClass_my_method(self):
        x = MyClass()  # Instantiation details omitted
        x.A = MagicMock()
        x.B = MagicMock()
        x.my_method(0)
        result_A = x.A.method_calls
        result_B = x.B.method_calls
        expected_result_A = [call.other_method1(1, 2),
                             call.other_method2(1, 2, 3),
                             call.other_method2(1)]
        expected_result_B = [call.other_method4(1, 2),
                             call.other_method5(1, 2, 4),
                             call.other_method6(1, 2, 5, 6)]

        self.assertEquals(result_A, expected_result_A)
        self.assertEquals(result_B, expected_result_B)

Questo funziona. Ma non posso fare a meno di sentire che ho semplicemente riscritto my_method - e, peggio, strettamente accoppiato il test al funzionamento interno del metodo.

C'è un modo migliore per testare questo metodo? Il metodo in realtà è mal progettato?

    
posta jl6 07.06.2017 - 06:58
fonte

3 risposte

1

Questo sembra davvero un design migliorabile. Ecco alcune cose che prenderei in considerazione:

  • Hai detto che non puoi renderlo puro a causa di I / O. Ma separare il calcolo di ciò che deve essere prodotto e l'output effettivo ti consente di mantenerne la maggior parte pura.

  • Il test non ha alcun senso. Non verifica nulla di utile, ma come hai detto, si accoppia strettamente al lavoro interiore. Ancora una volta, si pone la domanda su quale dovrebbe essere l'intero metodo in modo verificabile?

  • I test di output sembrano mancare da qualche parte qui intorno. Potresti aver provato questi other_method s e ciò che hanno prodotto, ma testare un ordine di chiamata del metodo è ancora diverso dal test dell'output effettivo che ne deriva. Speri che la composizione di questi lavori funzioni correttamente, ma questo è davvero il motivo per cui sono necessari i test di integrazione di ordine superiore. Dato un test di integrazione che verifica l'intero output creato da my_method , sembra esserci un piccolo vantaggio per il test in questione.

  • Infine, anche A e B potrebbero valere la pena di essere esaminati. Dato che l'ordine di chiamata del metodo è così importante, prenderei in considerazione la possibilità di considerare questi metodi e di chiedermi se esiste un effettivo accoppiamento temporale tra di essi. Se questo è il caso, una riprogettazione di A e / o B sembra essere una buona idea.

risposta data 07.06.2017 - 07:16
fonte
0

assert that these methods were called in the required order

I test dovrebbero consentire di apportare modifiche più facilmente, in modo che gli errori di progettazione possano essere corretti. Non dovrebbero configurare errori di progettazione nella pietra.

my_method then calls a sequence of methods on other attributes A and B of the class. I am not concerned with how to test those other methods.

L'unica ragione per offrire questi metodi separati da A e B è se possono essere chiamati in diversi ordini. Se possono, un test non dovrebbe dettare un ordine. Se non possono, allora A e B dovrebbero smettere di invitare problemi. E alcuni nomi di discendenza ci darebbero un'idea di quello che dovrebbero fare.

class ArgumentDispatcher(object, A, B):
...
def dispatchArguments(self, args):
    ...  # Code which transforms args into arg1, arg2, ..., argN
    self.A.dispatchArguments(arg1, arg2, arg3)
    self.B.dispatchArguments(arg1, arg2, arg5, arg6)
    ...  # Code which either does something useful with arg7 ... argN 
    ...  # or rejects them somehow rather than silently ignore them.

The sequence of method calls mutates A and B. So my_method is very much a non-pure function. I don't believe I can redesign to make the function pure, as the other methods all do I/O.

Se tutti questi metodi fanno scaricare gli argomenti in IO, non c'è abbastanza comportamento qui da testare. Se sono così tanto come aggiungerli insieme, fai un test su questo.

    
risposta data 07.06.2017 - 08:11
fonte
0

my_method first does some straightforward work to transform the values it is given. I am not concerned with how to test this aspect.

Ma questo è la parte importante di my_method .

chiamare un gruppo di metodi nel giusto ordine è quasi troppo semplice per fallire e test che porteranno a una stupida ripetizione del codice di produzione nel test. Anche questo ordine non è suscettibile di cambiare per motivi casuali. Se l'ordine di chiamare i metodi è importante, puoi documentarli nei nomi dei metodi.

Ma i calcoli basati sugli input sono molto più fragili. Questi dovrebbero davvero essere protetti da UnitTests.

    
risposta data 07.06.2017 - 11:12
fonte

Leggi altre domande sui tag