Unit test di wrapper simili - o test di una singola unità è sufficiente?

1

Sto lavorando a un servizio che ha molte dipendenze. Il modo in cui li chiamo è che sto avvolgendo ogni client di servizio su un Adapter . Come questo (sto usando Java):

public abstract class AdapterBase<Request, Response> {
    protected abstract String adapterMetadata();
    protected abstract Response makeActualCall(Request request);

    // additional things are happening here, for each dependency call it's the same
    public Optional<Response> call(Request request) {
        try {
            return Optional.ofNullable(makeCall(request));
        } catch(Exception ex) {
            log.error("bad thing happened", ex);
            return Optional.empty();
        }
    }
}

Ora, per ogni API che sto chiamando, sto creando un nuovo adattatore e per ogni adattatore sto creando un nuovo test dell'unità.

Un'implementazione di esempio di questo AdapterBase :

public class FooAdapter<FooRequest, FooResponse> extends AdapterBase<FooRequest, FooResponse> {
    private FooClient fooClient;            
    public FooAdapter(FooClient fooClient) {
        this.fooClient = fooClient;
    }

    @Override
    protected String adapterMetadata() {
        "Calling Foo";
    }

    @Override
    protected FooResponse makeActualCall(FooRequest fooRequest) {
        return fooClient.call(fooRequest);
    }
}

E ho le classi ereditate simili per ogni API. Il test unitario di base per questo calcestruzzo FooAdapter sarebbe il seguente:

@RunWith(MockitoJUnitRunner.class)
public class FooAdapterTest {
    @Mock
    private FooClient fooClient;

    @Mock
    private FooResponse fooResponse;

    private FooAdapter fooAdapter;

    @Before
    public void setup() {
        fooAdapter = new FooAdapter(fooClient);
    }

    @Test
    public void whenExceptionIsThrownThenEmptyObservableReturns() {
        when(fooClient.call(any()).thenThrow(FooException.class);

        Optional<FooResponse> actual = fooAdapter.call(new FooRequest());

        assertFalse(actual.isPresent());
    }

    @Test
    public void whenCallSucceededThenResultReturns() {
        when(fooClient.call(any()).thenReturn(fooResponse);

        Optional<FooResponse> actual = fooAdapter.call(new FooRequest());

        assertTrue(actual.isPresent());
        assertEquals(fooResponse, actual.get());
    }
}

E per tutti gli adattatori di cemento, ho gli stessi test di unità. A questo punto, mi chiedo se questi test unitari non abbiano alcun valore per noi. È la stessa cosa, l'unica differenza è che diversi "client" possono generare eccezioni diverse, ma a causa dell'implementazione di AdapterBase , non importa quale sia stata l'eccezione.

Avere queste classi sta ovviamente aumentando la metrica di copertura dei test, ma non sono convinto al 100% su quale valore ne ricaviamo. Sto pensando di cambiarlo con un'implementazione artificiale di AdapterBase e ho solo un test unitario contro di esso, poiché tutti gli adattatori stanno seguendo lo stesso schema.

Stavo leggendo che troppi test unitari di livello molto basso potrebbero essere un ostacolo alle modifiche agili poiché richiedono molte modifiche al codice nella base di codice non correlata alla produzione. Tuttavia, ogni adattatore è una singola unità, quindi deve essere testato.

Sono curioso di sapere quale sarebbe il modo giusto di seguire in questo caso.

    
posta maestro 09.11.2018 - 00:12
fonte

2 risposte

4

Esiterei prima di non testare nemmeno il codice "banale". Sicuro che questo codice possa essere coperto da test automatici non unitari, nel qual caso potresti sostenere che tale copertura è sufficiente per mostrare la correttezza del tuo programma. Ma nel grande schema delle cose i test unitari sono più semplici da ragionare, esegui molto più velocemente e sono più precisi nel localizzare il problema esatto (se scritto bene).

Ancora più importante il test è la documentazione attiva. Anche se non sono a conoscenza della sua esistenza specifica quando apporto una modifica a questo codice in seguito, quel test dell'unità mi informa se ho infranto un'aspettativa. È abbastanza ovvio che il logging non è un requisito, se lo cancellassi domani il sistema dovrebbe funzionare, cioè potrei installarlo senza che nessuno si lamenta. L'unico requisito importante è che un errore vuoto venga restituito in caso di errore e che altrimenti deve esserci un oggetto risposta.

Ma guardando il tuo codice, sì, qui c'è un gran numero di piastre per la caldaia. Quindi forse il refactoring del codice per escludere il piatto della caldaia è un risultato migliore.

  1. Puoi usare fooClient.call(request) invece di avere bisogno di un adattatore? - Se sì, l'adattatore e il test dell'unità scompaiono in fumo. Una celebrazione si verifica perché il codice è stato eliminato.
  2. L'adattatore fornisce un'astrazione utile? - Probabilmente no, non nasconde nessun tipo di informazione, né contribuisce alla conoscenza del tuo dominio. Sembra davvero una classe di riempimento. (non necessariamente la logica di riempimento).
  3. L'adattatore fornisce un comportamento aggiuntivo? - Forse i suoi errori di trapping, fornendo alcuni metadati rudimentali e generalmente come un tipo di confine. Se il tipo sottostante fa tutto questo, allora la logica è duplicata, vedi se può essere cancellata.
  4. L'adattatore è per lo più piastra di riscaldamento? - Sì, sicuramente la conoscenza in essa contenuta potrebbe essere essenziale ridotta a Adaptor("meta-data", request -> fooClient.call(request)) e quella linea potrebbe essere memorizzata all'interno di una sorta di IoC, Factory, procedura codificata.

Java ha la sintassi lambda. link

Una possibile riscrittura:

public class Adapter<Request, Response> {
    protected String metadata;
    protected Function<Request, Response> adapted;

    public Adapter(String metadata, Function<Request, Response> adapted)
    {
        this.metadata = metadata;
        this.adapted = adapted;
    }

    public Optional call(Request request) {
        try {
            return Optional.ofNullable(adapted(request));
        } catch(Exception ex) {
            log.error("bad thing happened", ex);
            return Optional.empty();
        }
    }
}

Ora hai una classe da testare per la logica effettiva.

E poi da qualche altra parte:

List a = new List();
a.Add(new Adaptor("meta-data", request -> fooClient.call(request)));
a.Add(new Adaptor("meta-data", request -> fooClient2.call(request)));
a.Add(new Adaptor("meta-data", request -> request.prop ? null : fooClient2.call(request)));

Avrai comunque bisogno di esercitare questi lambda, ma potrebbe comunque essere più semplice nel contesto di fabbrica.

Un'altra riscrittura consiste nell'esporre l'adattatore stesso come un lambda nella forma Function<Request, Response> . Questo poi si ridurrebbe a un lambda per servizio adattato

request -> {
    try {
        return Optional.ofNullable(adapted(request));
    } catch(Exception ex) {
        log.error("bad thing happened", ex);
        return Optional.empty();
    }
}

Questo non riduce la quantità di test, ha ancora la stessa area di superficie della classe dell'adattatore, ma c'è sicuramente meno numero di piastre e apre modi per semplificare la configurazione. (che potrebbe potenzialmente essere una vittoria più grande).

    
risposta data 09.11.2018 - 08:56
fonte
1

I get paid for code that works, not for tests, so my philosophy is to test as little as possible to reach a given level of confidence -- Kent Beck.

Tony Hoare ha parlato di codice che "ovviamente non ha carenze". La mia solita ortografia è "troppo stupida per fallire".

Quanto spesso modifichi queste implementazioni? Quante volte trovi effettivamente bug in loro? I test forniscono valore extra come documentazione? Stanno ancora contribuendo ai tuoi processi di progettazione?

Scrivere un test costa denaro. Mantenere un test costa denaro. Eseguire un test costa denaro. Se i test non ti restituiscono nulla, quei soldi vengono sprecati.

Per iscritto [Perché la maggior parte dei test unitari sono rifiuti], Jim Coplien ha osservato che i test a basso rischio hanno un payoff basso (potenzialmente negativo). Descrive un numero di categorie di test in cui il valore prop è sospetto - sospetto che scoprirai che i test che stai interrogando qui rientrano in una o più di queste categorie.

Cavalli per i corsi.

    
risposta data 09.11.2018 - 02:32
fonte