Come mantenere indipendenti i test unitari?

4

Ho letto in molti punti che i test unitari dovrebbero essere indipendenti. Nel mio caso ho una classe che fa la trasformazione dei dati. Questi passaggi devono essere eseguiti in sequenza, altrimenti non hanno senso. Ad esempio,

load_data
parse_float
label
normalize
add_bias
save_data

Ad esempio, non posso normalizzare i dati prima di etichettarli, perché i valori sono cambiati e non ho più accesso ai valori originali dei miei dati. Oppure non riesco a salvare i dati prima che lo avessi effettivamente caricato.

Quindi la mia classe di test ha un aspetto simile a questo

class TestTransform(Testcase):
  def setUp(self):
    self.trans = Transform()

  def test_main():
    self.load()
    self.parse_float()
    self.label()

  def load(self):
    self.trans.load()
    assert ....

  def parse_float(self):
    self.trans.parse_float()
    assert ....

In questo caso, i miei test unitari dipendono chiaramente l'uno dall'altro, ma non riesco a vedere in quale altro modo potrei farlo. In alternativa, potrei scrivere qualcosa di simile

  def test_normalize(self):
    # setup
    self.trans.load()
    self.trans.parse_float()
    self.trans.label()
    # test begins here
    self.trans.normalize()
    assert ....

Ma in questo caso, eseguo molte volte più codice, il che è inefficiente e rende i miei test più lunghi.

Quindi la migliore pratica di mantenere le unittest indipendenti si applica in questo caso o no?

    
posta siamii 27.04.2013 - 20:27
fonte

1 risposta

9

Non stai facendo test unitari, ma test di integrazione.

  • Un test unitario garantisce che un singolo componente di un sistema funzioni come previsto.

  • Un test di integrazione combina diversi componenti e verifica che, insieme, funzionano ancora normalmente.

È vero, non puoi normalizzare i dati prima di etichettarli. Ma quando testate la normalizzazione dei dati, dovreste testare un dato etichettato. O dati che non sono stati specificamente etichettati, di proposito, per garantire che la normalizzazione fallisca con successo quando i dati non sono etichettati (o gestiscono con garbo questa situazione).

Per farlo, puoi usare il mocking. Il mocking è la tecnica in cui si sostituisce una parte di un sistema con qualcosa che fornisce solo risultati fittizi. Nei tuoi test di normalizzazione, potresti avere una simulazione che fornisce dati etichettati, un altro simulato che fornisce dati etichettati, ma in modo errato, e un altro che non etichetta nulla.

Queste tre simulazioni ti serviranno per testare la tua normalizzazione indipendentemente dall'effettivo processo di etichettatura, senza nemmeno trasportare se è già implementata o meno, né se funziona come previsto.

In questo modo, puoi testare la parte che salva i dati e poi svilupparla, quindi iniziare l'etichettatura dell'unità di prova e implementarla, infine la normalizzazione del test dell'unità e scrivere il codice corrispondente.

Una volta terminato, utilizzerai i tuoi test di integrazione per vedere se tutto il sistema funziona.

Esempio

Non capisco davvero il processo dalla tua domanda (è specifico del dominio?), quindi invento alcune illustrazioni di facile comprensione. Immaginiamo un'app che carichi i dati meteorologici (temperatura, rischio di precipitazioni, ecc.), Da diversi servizi web, la paragoni utilizzando regole specifiche, faccia previsioni su dati statistici e memorizzi il risultato in un database.

Non riesco a confrontare i dati senza caricarli e non posso creare predicati da dati statistici senza dati di sorta. Ma prima voglio attuare l'analisi statistica, perché non sono sicuro di come farlo, né di ciò che il mio cliente si aspetta di conseguenza, quindi faccio prima questa parte per aumentare le possibilità di terminare il progetto in tempo.

Per questo, posso creare un sacco di mock in base alle specifiche. Quelle schifezze mi forniranno i dati che specificherò nei test unitari stessi. Un'app non testata nel mondo reale sarebbe simile a questa:

data = [];
foreach (provider in weatherProviders)
{
    data.push(provider.getTransformedData());
}

todayOverall = compareWeatherData(data); // We want to implement this first.
pastDaysOverall = loadHistoricalData();
weather = predictWeather(todayOverall, pastDaysOverall);

Un'app che usa i mock sarà simile a quella:

data = [];
foreach (provider in weatherProviders)
{
    data.push(provider.getTransformedData());
}

var dataAccess = new SQLiteDatabaseAccess();
todayOverall = compareWeatherData(new BasicCompare(), data);
pastDaysOverall = loadHistoricalData(new HistoricalLoader(dataAccess));
weather = predictWeather(new WeatherOracle(dataAccess), todayOverall, pastDaysOverall);

Come vedi, compareWeatherData prende un argomento aggiuntivo, che fa il lavoro effettivo, loadHistoricalData ha bisogno di un oggetto che carica effettivamente i dati da un'origine dati e che, a sua volta, richiede che venga specificato un fornitore di dati, ecc. Questo ha molti vantaggi rispetto al codice precedente:

  • Se domani il cliente mi dice che la mia app deve supportare sia SQLite che MySQL, non mi interessa: farò due provider invece di uno, e specificherò il primo o il secondo a seconda di la configurazione dell'applicazione. Allo stesso modo, se l'oracolo meteorologico della prima versione è davvero brutto, posso semplicemente lavorare su un oracolo migliore, quindi spostarli facilmente.

  • Quando un'unità testa le diverse parti, posso sostituirle con una simulazione. Ad esempio, posso alimentare WeatherOracle con FictionalDataMock: DataAccess anziché SQLiteDatabaseAccess: DataAccess , e in FictionalDataMock , specificare i valori manualmente.

Quando finalmente arriva all'unità testare la riga todayOverall = compareWeatherData(new BasicCompare(), data); , ho abbastanza potere per farlo senza nemmeno pensare ad altre parti del sistema:

/**
 * Ensures that the comparison handles correctly the case where one of the data providers
 * was on crack and supplied us with a temperature below Absolute Zero (-459.67°F).
 */
public test minimumTemperatureFailure()
{
    dataSet1 = new TransformedDataMock(sky.snow, fromFahrenheit(23));
    dataSet2 = new TransformedDataMock(sky.heavySnow, fromFahrenheit(-480)); // Too cold!

    data = [ dataSet1, dataSet2 ];

    actual = new BasicCompare().compare(data);

    // The temperature below Absolute Zero should be ignored.
    assert.areEqual(dataSet1.temperature, actual.averageTemperature);

    // Since the second data provider was doing drugs, other data from it shouldn't be
    // trusted neither.
    assert.areEqual(dataSet1.sky, actual.mostRealisticSky);
}
    
risposta data 27.04.2013 - 21:08
fonte

Leggi altre domande sui tag