Integrazione del mocking con i parametri predefiniti di C++

Integrazione del mocking con i parametri predefiniti di C++

Quando inseriamo un pezzo di codice in uno unit test, a volte abbiamo bisogno di martellarlo in una forma che si inserisce in un cablaggio di prova. Un tipico esempio è per tagliare le dipendenze :la funzione che vorremmo testare dipende dall'interfaccia utente, da un database o semplicemente da qualcosa di veramente intricato a cui il nostro binario di test non può collegarsi.

Alcune di queste operazioni di refactoring sulla funzione testata sono vantaggiose:le sue dipendenze diventano meno numerose e più chiare e il codice risultante ha meno accoppiamenti.

Ma a volte, tutto questo martellamento ha l'effetto di lasciare la funzione testata in pessime condizioni. Ad esempio beffardo può influire sulla sua interfaccia quando la utilizziamo per sostituire una parte interna della funzione.

Questo articolo fa parte della serie sui parametri predefiniti in C++:

  • Parametri di default in C++:i fatti (compresi quelli segreti)
  • Devo sovraccaricare o utilizzare i parametri predefiniti?
  • Parametri predefiniti con parametri del tipo di modello predefiniti
  • Defaulted :un aiuto per aggirare i vincoli dei parametri predefiniti
  • Implementazione di parametri predefiniti che dipendono da altri parametri in C++
  • Come i parametri predefiniti possono aiutare a integrare i mock

Un esempio di presa in giro

Ad esempio, consideriamo una funzione f ciò accade per chiamare una funzione di registrazione per produrre alcuni dei suoi risultati:

int f(int x, int y)
{
    // doing calculations...
    log(intermediaryResult); 
    // calculating some more...
    return result;
}

E non compileremo il codice di registrazione nel binario di test. In effetti, non abbiamo nemmeno bisogno di f per registrare qualsiasi cosa quando viene eseguito nel suo unit test.

EDIT:come sottolineato da diversi lettori, alcuni logger sono implementati con un accesso globale e possono essere disattivati, senza bisogno di un mock. Qui l'esempio mira a illustrare qualsiasi pezzo di codice che non si desidera includere nel binario di test. Quindi log potrebbe essere sostituito con computeconvert o doSomething , purché rappresenti una funzione di cui non vogliamo il codice nel binario di test e che sostituiamo con un mock.

Esistono diversi modi per affrontare questo tipo di situazione e uno di questi, noto come "Interfaccia di estrazione ' refactoring, consiste nel deridere la funzionalità di logging con un'implementazione più semplice (qui non fa nulla) e passare questo mock a f . (Puoi trovare molti altri modi per testare una tale funzione in Working Effectively With Legacy Code di Michael Feathers).

L'idea di prendere in giro va in questa direzione:iniziamo creando un'interfaccia con le funzionalità che vogliamo prendere in giro:

class ILogger
{
public:
    virtual void log(int value) const = 0;
};

Quindi creiamo una classe che implementi questa interfaccia, da utilizzare nel test, e che non dipenda dalla funzione di logging:

class LoggerMock : public ILogger
{
public:
    void log(int value) const override { /* do nothing */ }
};

E un'altra classe che esegue effettivamente la chiamata a log funzione, da utilizzare nel codice di produzione:

class Logger : public ILogger
{
public:
    void log(int value) const override { ::log(value); }
};

Quindi f deve cambiare per accogliere questa nuova interfaccia:

int f(int x, int y, const ILogger& logger)
{
    // doing calculations...
    logger.log(intermediaryResult); 
    // calculating some more...
    return result;
}

Il codice di produzione chiama f in questo modo:

f(15, 42, Logger());

e il codice di prova lo chiama in questo modo:

f(15, 42, LoggerMock());

Secondo me, f è stato danneggiato durante il processo. In particolare a livello della sua interfaccia:

int f(int x, int y, const ILogger& logger);

Il logger doveva essere un dettaglio di implementazione di f e ora è fluttuato nella sua interfaccia. I problemi concreti che ciò provoca sono:

  • ogni volta che leggiamo una chiamata al f vediamo menzionato un logger, che è un'altra cosa che dobbiamo capire quando leggiamo un pezzo di codice.
  • quando un programmatore vuole usare f e guarda la sua interfaccia, questa interfaccia richiede di essere passata a un logger. Questo inevitabilmente fa sorgere la domanda:“Quale argomento dovrei passare? Ho pensato f era una funzione numerica, cosa dovrei passare per 'logger'??" E poi il programmatore deve scavare di più, eventualmente chiedere ai manutentori della funzione. Oh è usato per i test. Ah, vedo. Quindi cosa dovrei passare esattamente qui? Avresti uno snippet da copiare e incollare nel mio codice?

Questo è un prezzo difficile da pagare per inserire una funzione in uno unit test. Non potremmo farlo diversamente?

Nascondere il mock nel codice di produzione

Tanto per essere chiari, non ho nulla contro l'idea di prendere in giro. È un modo pratico per mettere il codice esistente in test automatici e il test automatico ha un valore immenso. Ma non mi sento molto ben equipaggiato con tecniche specifiche in C++ per ottenere mocking e test in generale, senza danneggiare in alcuni casi il codice di produzione.

Vorrei indicare un modo per utilizzare i parametri predefiniti per facilitare la presa in giro in C++. Non sto dicendo che sia perfetto, tutt'altro. Mostrandolo qui, spero che questo sia abbastanza interessante per te in modo che possiamo iniziare a scambiarci sull'argomento come gruppo e trovare insieme come usare la potenza del C++ per rendere espressivo il codice testabile.

Ci sono almeno due cose che possiamo fare per limitare l'impatto su f :impostare il mock come parametro predefinito e utilizzare la denominazione per essere molto espliciti sul suo ruolo.

Parametro fittizio predefinito

Impostiamo il parametro mock come parametro predefinito, per impostazione predefinita sull'implementazione di produzione:

int f(int x, int y, const ILogger& logger = Logger());

Per ottenere ciò abbiamo bisogno che la funzione prenda il mock in riferimento a const o in base al valore.

In questo caso il codice di produzione non deve più preoccuparsi di passargli un valore logger:

f(15, 42);

Il modo predefinito di agire di f è quello naturale:i suoi richiami al log funzione eseguire la registrazione. Non c'è bisogno che il sito di chiamata sia esplicito al riguardo.

Dal lato del cablaggio di prova, tuttavia, vogliamo fare qualcosa di specifico:impedire che le chiamate di registrazione raggiungano il log funzione. Ha senso mostrare al sito di chiamata che qualcosa è cambiato:

f(15, 42, LoggerMock());

Una convenzione di denominazione

Per chiarire i dubbi che si potrebbero avere sull'ultimo parametro quando si guarda l'interfaccia, possiamo usare un nome specifico per designare questo modello. Prendendo ispirazione per lavorare in modo efficace con il codice legacy, mi piace usare la nozione di "cucitura" di Michael Feathers. Rappresenta un punto nel codice in cui possiamo collegare diverse implementazioni. Un po' come una cucitura è un punto di giunzione tra due pezzi di tessuto, dove puoi operare per cambiarne uno senza danneggiarlo.

Quindi la nostra interfaccia potrebbe chiamarsi LoggerSeam invece di ILogger :

int f(int x, int y, const LoggerSeam& logger = Logger());

In questo modo, la parola "Seam" nell'interfaccia trasmette il messaggio "Non preoccuparti, abbiamo solo bisogno di questo a scopo di test", e il parametro predefinito dice "Abbiamo risolto, ora continua con il tuo normale utilizzo di f “.

Andare oltre

Questo è stato un esempio molto semplice di presa in giro, ma ci sono altre questioni che vale la pena indagare. E se ci fossero diverse cose da deridere nella funzione e non solo la registrazione? Dovremmo avere più cuciture e altrettanti parametri, o uno grande che contenga tutto ciò di cui la funzione ha bisogno per deridere?

E se il mock contenesse dati e non solo comportamenti? Non è stato possibile costruirlo in un parametro predefinito. Ma la presa in giro non riguarda comunque solo il comportamento?

Un altro punto da notare è che con l'implementazione di cui sopra, se la funzione è dichiarata in un file di intestazione, il Logger predefinito deve essere definito accanto alla dichiarazione della funzione, perché il parametro predefinito nel prototipo chiama il suo costruttore.

In breve:come pensi che possiamo rendere più espressivo il codice testabile?

Potrebbe piacerti anche

  • Il refactoring "Extract Interface", in fase di compilazione