Unit test per testare la creazione di un oggetto dominio


11

Ho un Unit Test, che assomiglia a questo:

[Test]
public void Should_create_person()
{
     Assert.DoesNotThrow(() => new Person(Guid.NewGuid(), new DateTime(1972, 01, 01));
}

Sto affermando che qui viene creato un oggetto Person, ovvero che la validazione non fallisce. Ad esempio, se il Guid è nullo o la data di nascita è precedente al 01/01/1900, la validazione fallirà e verrà generata un'eccezione (il che significa che il test fallisce).

Il costruttore si presenta così:

public Person(Id id, DateTime dateOfBirth) :
        base(id)
    {
        if (dateOfBirth == null)
            throw new ArgumentNullException("Date of Birth");
        elseif (dateOfBith < new DateTime(1900,01,01)
            throw new ArgumentException("Date of Birth");
        DateOfBirth = dateOfBirth;
    }

È una buona idea per un test?

Nota : sto seguendo un approccio classicista all'Unità di test del modello di dominio se questo ha rilevanza.


Il costruttore ha qualche logica che merita di essere affermata dopo l'inizializzazione?
Laiv

2
Non preoccuparti mai di provare i costruttori !!! La costruzione dovrebbe essere diretta. Ti aspetti errori in Guid.NewGuid () o costruttore di DateTime?
ivenxu,

@Laiv, consultare l'aggiornamento alla domanda.
w0051977,

1
Non vale nulla per implementare un test come quello che hai condiviso. Tuttavia, testerei anche il contrario. Vorrei testare il caso in cui birthDate provoca un errore. Questo è l'invariante della classe che vuoi essere sotto controllo e test.
Laiv

3
Il test sembra a posto, a parte una cosa: il nome. Should_create_person? Cosa dovrebbe creare una persona? Dagli un nome significativo, come Creating_person_with_valid_data_succeeds.
David Arno,

Risposte:


18

Questo è un test valido (anche se piuttosto troppo zelante) e a volte lo faccio per testare la logica del costruttore, tuttavia, come ha detto Laiv nei commenti, dovresti chiederti il ​​perché.

Se il tuo costruttore si presenta così:

public Person(Guid guid, DateTime dob)
{
  this.Guid = guid;
  this.Dob = dob;
}

C'è molto senso nel test se lancia? Se i parametri sono assegnati correttamente, posso capire ma il tuo test è piuttosto eccessivo.

Tuttavia, se il test fa qualcosa del genere:

public Person(Guid guid, DateTime dob)
{
  if(guid == default(Guid)) throw new ArgumentException("Guid is invalid");
  if(dob == default(DateTime)) throw new ArgumentException("Dob is invalid");

  this.Guid = guid;
  this.Dob = dob;
}

Quindi il test diventa più pertinente (poiché in realtà stai generando eccezioni da qualche parte nel codice).

Una cosa che direi, generalmente è una cattiva pratica avere molta logica nel tuo costruttore. La validazione di base (come i controlli null / default che sto facendo sopra) sono ok. Ma se ti stai connettendo a database e stai caricando i dati di qualcuno, è lì che il codice inizia a sentire davvero ...

Per questo motivo, se vale la pena testare il costruttore (perché c'è molta logica in corso), forse qualcos'altro non va.

Quasi certamente avrai altri test che coprono questa classe in livelli di logica aziendale, costruttori e assegnazioni di variabili avranno quasi sicuramente una copertura completa da questi test. Pertanto è forse inutile aggiungere test specifici specifici per il costruttore. Tuttavia, nulla è in bianco e nero e non avrei nulla a sfavore di questi test se li analizzassi, ma mi chiederei se aggiungono molto valore al di là dei test altrove nella tua soluzione.

Nel tuo esempio:

public Person(Id id, DateTime dateOfBirth) :
        base(id)
    {
        if (dateOfBirth == null)
            throw new ArgumentNullException("Date of Birth");
        elseif (dateOfBith < new DateTime(1900,01,01)
            throw new ArgumentException("Date of Birth");
        DateOfBirth = dateOfBirth;
    }

Non stai solo facendo la validazione, ma stai anche chiamando un costruttore di base. Per me questo fornisce ulteriori motivi per avere questi test in quanto hanno la logica di costruzione / convalida ora divisa in due classi che diminuisce la visibilità e aumenta il rischio di cambiamenti imprevisti.

TLDR

Vi è un certo valore in questi test, tuttavia è probabile che la logica di convalida / assegnazione sia coperta da altri test nella soluzione. Se c'è molta logica in questi costruttori che richiede test significativi, allora mi suggerisce che ci sia un cattivo odore di codice in agguato lì dentro.


@Laith, consulta l'aggiornamento della mia domanda
w0051977,

Ho notato che stai chiamando un costruttore di base nel tuo esempio. IMHO questo aggiunge più valore al tuo test, la logica del costruttore è ora suddivisa in due classi ed è quindi leggermente più alta il rischio di cambiamento, dando quindi più motivi per testarlo.
Liath

"Tuttavia, se il tuo test fa qualcosa del genere:" <Non intendi "se il tuo costruttore fa qualcosa del genere" ?
Kodos Johnson,

"C'è un certo valore in questi test" - interessante per me, comunque, il valore sta dimostrando che potremmo rendere superfluo questo test usando una nuova classe per rappresentare il dob della persona (ad esempio PersonBirthdate) che esegue la convalida della data di nascita. Allo stesso modo il Guidcontrollo potrebbe essere implementato sulla Idclasse. Ciò significa che non devi più avere quella logica di validazione nel Personcostruttore poiché non è possibile costruirne una con dati non validi, ad eccezione dei nullriferimenti. Certo, devi scrivere i test per le altre due classi :)
Stephen Byrne,

12

Già una buona risposta qui, ma penso che valga la pena menzionare un'altra cosa.

Quando si esegue TDD "dal libro", è necessario scrivere prima un test che chiama il costruttore, anche prima che il costruttore venga implementato. Tale test potrebbe effettivamente assomigliare a quello presentato, anche se l'implementazione del costruttore avrebbe una logica di validazione pari a zero.

Si noti inoltre che per TDD, si dovrebbe scrivere prima un altro test come

  Assert.Throws<ArgumentException>(() => new Person(Guid.NewGuid(), 
        new DateTime(1572, 01, 01));

prima di aggiungere il controllo per DateTime(1900,01,01)al costruttore.

Nel contesto TDD, il test mostrato ha perfettamente senso.


Bel angolo che non avevo preso in considerazione!
Liath

1
Questo mi dimostra perché una forma così rigida di TDD è una perdita di tempo: il test dovrebbe avere valore dopo la scrittura del codice, oppure stai scrivendo ogni riga di codice due volte, una volta come un'asserzione e una volta come codice. Direi che il costruttore stesso non è un pezzo di logica che deve essere testato; la regola commerciale "le persone nate prima del 1900 non devono essere rappresentabili" è verificabile e il costruttore è il luogo in cui tale regola sembra essere implementata, ma quando il test di un costruttore vuoto aggiungerebbe mai valore al progetto?
IMSoP

È davvero scritto dal libro? Vorrei creare un'istanza e chiamare subito il suo metodo in un codice. Quindi scriverei il test per quel metodo, e così facendo dovrei anche creare un'istanza per quel metodo, quindi sia il costruttore che il metodo saranno coperti in quel test. A meno che nel costruttore non ci sia qualche logica, ma quella parte è coperta da Liath.
Rafał Łużyński,

@ RafałŁużyński: TDD "dal libro" riguarda prima di tutto i test di scrittura . Significa in realtà scrivere sempre prima un test fallito (non compilare anche i conteggi come fallimento). Quindi prima scrivi un test chiamando il costruttore anche quando non c'è nessun costruttore . Quindi si tenta di compilare (che non riesce), quindi implementare un costruttore vuoto, compilare, eseguire il test, risultato = verde. Quindi scrivi il primo test fallito ed eseguilo - risultato = rosso, quindi aggiungi la funzionalità per rendere nuovamente il test "verde" e così via.
Doc Brown,

Ovviamente. Non intendevo scrivere prima l'implementazione, quindi test. Scrivo semplicemente "utilizzo" di quel codice a un livello superiore, quindi testa quel codice, quindi lo implemento. Sto facendo "Fuori TDD" di solito.
Rafał Łużyński,
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.