La costruzione di oggetti con parametri null nei test unitari è corretta?


37

Ho iniziato a scrivere alcuni test unitari per il mio progetto attuale. Non ho davvero esperienza con esso però. Prima voglio "capirlo" completamente, quindi attualmente non sto usando né il mio framework IoC né una libreria beffarda.

Mi chiedevo se ci fosse qualcosa di sbagliato nel fornire argomenti nulli ai costruttori di oggetti nei test unitari. Lasciami fornire un codice di esempio:

public class CarRadio
{...}


public class Motor
{
    public void SetSpeed(float speed){...}
}


public class Car
{
    public Car(CarRadio carRadio, Motor motor){...}
}


public class SpeedLimit
{
    public bool IsViolatedBy(Car car){...}
}

Ancora un altro esempio di codice auto (TM), ridotto solo alle parti importanti per la domanda. Ora ho scritto un test simile a questo:

public class SpeedLimitTest
{
    public void TestSpeedLimit()
    {
        Motor motor = new Motor();
        motor.SetSpeed(10f);
        Car car = new Car(null, motor);

        SpeedLimit speedLimit = new SpeedLimit();
        Assert.IsTrue(speedLimit.IsViolatedBy(car));
    }
}

Il test funziona bene. SpeedLimitha bisogno di un Carcon un Motorper fare la sua cosa. Non è affatto interessato a CarRadioa, quindi ho fornito null per quello.

Mi chiedo se un oggetto che fornisce la corretta funzionalità senza essere completamente costruito sia una violazione di SRP o un odore di codice. Ho questa fastidiosa sensazione che lo faccia, ma speedLimit.IsViolatedBy(motor)non mi sento bene - un limite di velocità viene violato da un'auto, non da un motore. Forse ho solo bisogno di una prospettiva diversa per i test unitari rispetto al codice di lavoro, perché l'intera intenzione è testare solo una parte del tutto.

Costruire oggetti con null in unit test è un odore di codice?


24
Un po 'fuori tema, ma Motorprobabilmente non dovrebbe avere speedaffatto. Dovrebbe avere un throttlee calcolare un in torquebase all'attuale rpme throttle. È compito della macchina usare un Transmissionper integrarlo in una velocità attuale e trasformarlo in un rpmsegnale per tornare al Motor... Ma immagino, non eri in esso per il realismo comunque, vero?
cmaster

17
L'odore del codice è che il tuo costruttore prende null senza lamentarsi in primo luogo. Se ritieni che ciò sia accettabile (potresti avere le tue ragioni per questo): vai avanti, provalo!
Tommy

4
Considera il modello Null-Object . Spesso semplifica il codice, rendendolo al contempo NPE sicuro.
Bakuriu,

17
Congratulazioni : hai provato che con una nullradio, il limite di velocità è calcolato correttamente. Ora potresti voler creare un test per convalidare il limite di velocità con una radio; nel caso in cui il comportamento differisca ...
Matthieu M.

3
Non sono sicuro di questo esempio, ma a volte il fatto che tu voglia testare solo mezza classe è un indizio che qualcosa dovrebbe essere rotto
Owen

Risposte:


70

Nel caso dell'esempio precedente, è ragionevole che a Carpossa esistere senza a CarRadio. In tal caso, direi che non solo è accettabile passare un null a CarRadiovolte, direi che è obbligatorio. I test devono garantire che l'implementazione della Carclasse sia solida e che non generi eccezioni di puntatore null quando noCarRadio è presente.

Tuttavia, facciamo un esempio diverso: consideriamo a SteeringWheel. Supponiamo che a Cardebba avere un SteeringWheel, ma al test di velocità non interessa davvero. In questo caso, non vorrei passare un null SteeringWheelpoiché questo sta spingendo la Carclasse in luoghi dove non è progettata per andare. In questo caso, faresti meglio a creare una sorta di DefaultSteeringWheelquale (per continuare la metafora) è bloccato in linea retta.


44
E non dimenticare di scrivere un test unitario per verificare che a Carnon possa essere costruito senza un SteeringWheel...
cmaster

13
Potrei mancare qualcosa, ma se è possibile e persino comune creare un Carsenza un CarRadio, non avrebbe senso creare un costruttore che non richieda il passaggio e non debba preoccuparsi di un null esplicito ?
un earwig il

16
Le auto con guida autonoma potrebbero non avere il volante. Sto solo dicendo. ☺
BlackJack

1
@James_Parsons Ho dimenticato di aggiungere che si trattava di una classe che non aveva controlli nulli perché veniva usata solo internamente e riceveva sempre parametri non nulli. Altri metodi Caravrebbero gettato un NRE con un null CarRadio, ma il codice pertinente SpeedLimitnon lo avrebbe fatto. Nel caso della domanda posta ora, saresti corretto e lo farei davvero.
R. Schmitz,

2
@mtraceur Il modo in cui tutti presumevano che la classe che consente la costruzione con un argomento null significa che null è un'opzione valida mi ha fatto capire che probabilmente avrei dovuto avere dei controlli null lì. Il vero problema che avrei avuto allora è coperto da molte risposte buone, interessanti e ben scritte qui che non voglio rovinare e che mi hanno aiutato. Anche accettare questa risposta ora perché non mi piace lasciare le cose incompiute ed è la più votata (anche se sono state tutte utili).
R. Schmitz,

23

Costruire oggetti con null in unit test è un odore di codice?

Sì. Nota la definizione C2 di odore di codice : "un CodeSmell è un suggerimento che qualcosa potrebbe essere sbagliato, non una certezza".

Il test funziona bene. SpeedLimit ha bisogno di un'auto con un motore per fare la sua cosa. Non è affatto interessato a un CarRadio, quindi ho fornito null per questo.

Se stai cercando di dimostrare che speedLimit.IsViolatedBy(car)non dipende dalCarRadio dall'argomento passato al costruttore, probabilmente sarebbe più chiaro al futuro manutentore rendere esplicito quel vincolo introducendo un doppio di test che fallisce il test se viene invocato.

Se, come è più probabile, stai solo cercando di salvarti il ​​lavoro di creazione di un CarRadioche sai che non è usato in questo caso, dovresti notare che

  • questa è una violazione dell'incapsulamento (stai lasciando che il fatto che CarRadionon sia utilizzato fuoriesca dal test)
  • hai scoperto almeno un caso in cui desideri creare un Carsenza specificare aCarRadio

Sospetto fortemente che il codice stia cercando di dirti che vuoi un costruttore che assomigli

public Car(IMotor motor) {...}

e quindi quel costruttore può decidere cosa fare con il fatto che non c'èCarRadio

public Car(IMotor motor) {
    this(null, motor);
}

o

public Car(IMotor motor) {
    this( new ICarRadio() {...} , motor);
}

o

public Car(IMotor motor) {
    this( new DefaultRadio(), motor);
}

eccetera.

Si tratta di passare null a qualcos'altro durante il test di SpeedLimit. Puoi vedere che il test si chiama "SpeedLimitTest" e l'unico Assert sta controllando un metodo di SpeedLimit.

Questo è un secondo odore interessante - per testare SpeedLimit, devi costruire un'intera macchina attorno a una radio, anche se l'unica cosa a cui probabilmente importa il controllo SpeedLimit è la velocità .

Questo potrebbe essere un suggerimento che SpeedLimit.isViolatedBydovrebbe accettare un'interfaccia di ruolo implementata dall'auto , piuttosto che richiedere un'intera auto.

interface IHaveSpeed {
    float speed();
}

class Car implements IHaveSpeed {...}

IHaveSpeedè un nome davvero schifoso; speriamo che il tuo linguaggio di dominio abbia un miglioramento.

Con questa interfaccia attiva, il test SpeedLimitpotrebbe essere molto più semplice

public void TestSpeedLimit()
{
    IHaveSpeed somethingTravelingQuickly = new IHaveSpeed {
        float speed() { return 10f; }
    }

    SpeedLimit speedLimit = new SpeedLimit();
    Assert.IsTrue(speedLimit.IsViolatedBy(somethingTravelingQuickly));
}

Nel tuo ultimo esempio suppongo che dovresti creare una classe finta che implementa l' IHaveSpeedinterfaccia in quanto (almeno in c #) non saresti in grado di creare un'istanza di un'interfaccia stessa.
Ryan Searle,

1
Esatto: l'ortografia efficace dipenderà dal sapore di Blub che stai usando per i tuoi test.
VoiceOfUnreason

18

Costruire oggetti con null in unit test è un odore di codice?

No

Affatto. Al contrario, se NULLè un valore disponibile come argomento (alcune lingue potrebbero avere costrutti che non consentono il passaggio di un puntatore null), è necessario verificarlo.

Un'altra domanda è cosa succede quando passi NULL. Ma questo è esattamente ciò che riguarda un test unitario: assicurarsi che si stia verificando un risultato definito per ogni possibile input. Ed NULL è un potenziale input, quindi dovresti avere un risultato definito e dovresti avere un test per quello.

Quindi, se la tua auto dovrebbe essere costruibile senza una radio, dovresti scrivere un test per questo. Se non lo è e dovrebbe generare un'eccezione, dovresti scrivere un test per questo.

Ad ogni modo, sì, dovresti scrivere un test per NULL. Sarebbe un odore di codice se lo lasciassi fuori. Ciò significherebbe che hai un ramo di codice non testato che hai appena assunto sarebbe magicamente privo di bug.


Nota che sono d'accordo con le altre risposte che il tuo codice in prova potrebbe essere migliorato. Tuttavia, se il codice migliorato ha parametri che sono puntatori, passare NULLanche per il codice migliorato è un buon test. Vorrei un test per mostrare cosa succede quando passo NULLcome motore. Non è un odore di codice, è l'opposto di un odore di codice. Sta testando.


Sembra che tu stia scrivendo dei test Carqui. Anche se non vedo nulla che considererei un'affermazione sbagliata, la domanda riguarda i test SpeedLimit.
R. Schmitz,

@ R.Schmitz Non sono sicuro di cosa intendi. Ho citato la domanda, si tratta di passare NULLal costruttore di Car.
nvoigt

1
Si tratta di passare null a qualcos'altro durante il testSpeedLimit . Si può vedere che il test si chiama "SpeedLimitTest" e l'unico Assertsta controllando un metodo di SpeedLimit. Test Car- ad esempio "se la tua auto dovrebbe essere costruibile senza una radio, dovresti scrivere un test per quello" - accadrebbe in un test diverso.
R. Schmitz,

7

Personalmente, esiterei a iniettare null. In effetti, ogni volta che inietto dipendenze in un costruttore, una delle prime cose che faccio è sollevare ArgumentNullException se quelle iniezioni sono nulle. In questo modo posso tranquillamente usarli all'interno della mia classe, senza dozzine di controlli null sparsi in giro. Ecco un modello molto comune. Nota l'uso di sola lettura per garantire che quelle dipendenze impostate nel costruttore non possano essere impostate su null (o qualsiasi altra cosa) in seguito:

class Car
{
   private readonly ICarRadio _carRadio;
   private readonly IMotor _motor;

   public Car(ICarRadio carRadio, IMotor motor)
   {
      if (carRadio == null)
         throw new ArgumentNullException(nameof(carRadio));

      if (motor == null)
         throw new ArgumentNullException(nameof(motor));

      _carRadio = carRadio;
      _motor = motor;
   }
}

Con quanto sopra, potrei forse avere un test di unità o due, in cui iniettare valori null, solo per assicurarmi che i miei controlli null funzionino come previsto.

Detto questo, questo diventa un problema, una volta che fai due cose:

  1. Programma per interfacce anziché per classi.
  2. Utilizzare un framework beffardo (come Moq per C #) per iniettare dipendenze beffe invece di null.

Quindi per il tuo esempio avresti:

interface ICarRadio
{
   bool Tune(float frequency);
}

interface Motor
{
   bool SetSpeed(float speed);
}

...
var car = new Car(new Moq<ICarRadio>().Object, new Moq<IMotor>().Object);

Maggiori dettagli sull'uso di Moq sono disponibili qui .

Ovviamente, se vuoi capirlo senza prima deridere, puoi comunque farlo, purché utilizzi le interfacce. Basta creare un finto CarRadio e Motor come segue e iniettare quelli invece:

class DummyCarRadio : ICarRadio
{
   ...
}
class DummyMotor : IMotor
{
   ...
}

...
var car = new Car(new DummyCarRadio(), new DummyMotor())

3
Questa è la risposta che avrei scritto
Liath

1
Vorrei spingermi fino al punto di dire che anche gli oggetti fittizi devono adempiere al loro contratto. Se IMotoravesse un getSpeed()metodo, DummyMotorsarebbe necessario restituire una velocità modificata anche dopo un successo setSpeed.
ComFreek,

1

Non è solo ok, è una tecnica di rottura delle dipendenze ben nota per ottenere il codice legacy in un cablaggio di prova. (Vedi "Lavorare efficacemente con il codice legacy" di Michael Feathers.)

Puzza? Bene...

Il test non ha odore. Il test sta facendo il suo lavoro. Sta dichiarando "questo oggetto può essere nullo e il codice sotto test funziona ancora correttamente".

L'odore è in fase di implementazione. Dichiarando un argomento del costruttore, stai dichiarando "questa classe ha bisogno di questa cosa per funzionare correttamente". Ovviamente, non è vero. Qualcosa di falso è un po 'puzzolente.


1

Facciamo un passo indietro ai primi principi.

Un test unitario verifica alcuni aspetti di una singola classe.

Vi saranno tuttavia costruttori o metodi che accettano altre classi come parametri. Il modo in cui gestirli varierà da caso a caso, ma l'installazione dovrebbe essere minima poiché sono tangenziali all'obiettivo. Ci possono essere scenari in cui il passaggio di un parametro NULL è perfettamente valido, ad esempio un'auto senza radio.

Sto assumendo qui, che stiamo solo testando la linea di corsa per cominciare (per continuare il tema auto), ma si può anche voler passare NULL ad altri parametri per controllare che qualsiasi meccanismo di codice e distribuendo eccezione difensive funzionano come necessario.

Fin qui tutto bene. Tuttavia, cosa succede se NULL non è un valore valido. Bene, qui sei nel mondo oscuro di beffe, tronconi, manichini e falsi .

Se tutto questo sembra un po 'troppo da affrontare, in realtà stai già utilizzando un manichino passando NULL nel costruttore di auto poiché è un valore che potenzialmente non verrà utilizzato.

Ciò che dovrebbe diventare chiaro abbastanza rapidamente, è che stai potenzialmente sparando il tuo codice con molta gestione NULL. Se sostituisci i parametri di classe con le interfacce, allora apri anche la strada per deridere, DI ecc. Che li rendono molto più semplici da gestire.


1
"I test unitari servono per testare il codice di una singola classe." Sigh . Questo non è ciò che i test unitari sono e seguire questa linea guida ti condurrà in classi gonfie o test controproducenti e difficili.
Formica P

3
trattare "unità" come eufemismo per "classe" è purtroppo un modo di pensare molto comune, ma in realtà tutto ciò che fa è (a) incoraggiare test "immondizia, immondizia", ​​che possono compromettere totalmente la validità dell'intero test suite e (b) impongono limiti che dovrebbero essere liberi di cambiare, il che può paralizzare la produttività. Vorrei che più persone ascoltassero il loro dolore e sfidassero questo preconcetto.
Ant P

3
Nessuna tale confusione. Prova le unità di comportamento, non le unità di codice. Non c'è nulla nel concetto di un unit test - tranne nella diffusa mente di alveari di cult dei carichi di test del software - che implica che un'unità di test è accoppiata al concetto distintamente OOP di una classe. E anche un malinteso molto dannoso.
Formica P

1
Non sono sicuro esattamente cosa intendi: sto discutendo l'esatto contrario di ciò che stai insinuando. Troncare / deridere i confini rigidi (se assolutamente necessario) e testare un'unità comportamentale . Tramite l'API pubblica. Che cos'è quell'API dipende da cosa stai costruendo ma l'impronta dovrebbe essere minima. L'aggiunta di astrazione, polimorfismo o codice di ristrutturazione non dovrebbe far fallire i test - "unità per classe" contraddice questo e compromette completamente il valore dei test in primo luogo.
Formica P

1
RobbieDee - Sto parlando dell'esatto contrario. Considera che potresti essere quello che ospita idee sbagliate comuni, rivisitare ciò che consideri una "unità" e forse scavare un po 'più a fondo in ciò di cui Kent Beck stava realmente parlando quando parlava di TDD. Ciò che la maggior parte della gente chiama TDD ora è una mostruosità di culto del carico. youtube.com/watch?v=EZ05e7EMOLM
Ant P

1

Dando un'occhiata al tuo design, suggerirei che a Carsia composto da un set di Features. Una caratteristica può essere una CarRadio, o EntertainmentSystemche può consistere in cose come una CarRadio, DvdPlayerecc.

Quindi, come minimo, dovresti costruire un Carcon un set di Features. EntertainmentSysteme CarRadiosarebbero implementatori dell'interfaccia (Java, C # ecc.) dipublic [I|i]nterface Feature e questa sarebbe un'istanza del modello di progettazione composita.

Pertanto, costruiresti sempre un Carcon Featurese un test unitario non creerebbe mai un'istanza Carsenza Features. Anche se potresti considerare di deridere aBasicTestCarFeatureSet che ha un set minimo di funzionalità richieste per il tuo test di base.

Solo alcuni pensieri.

John

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.