Unit Testing in un mondo "no setter"

22

Non mi considero un esperto di DDD ma, come architetto di soluzioni, cerco di applicare le migliori pratiche ogni volta che è possibile. So che c'è un sacco di discussioni sui pro e contro dello stile "no" (pubblico) setter in DDD e posso vedere entrambi i lati dell'argomento. Il mio problema è che lavoro su una squadra con una grande diversità di competenze, conoscenze ed esperienza, il che significa che non posso fidarmi del fatto che ogni sviluppatore farà le cose nel modo "giusto". Ad esempio, se i nostri oggetti di dominio sono progettati in modo che le modifiche allo stato interno dell'oggetto siano eseguite da un metodo ma forniscano setter di proprietà pubbliche, qualcuno imposterà inevitabilmente la proprietà invece di chiamare il metodo. Utilizza questo esempio:

public class MyClass
{
    public Boolean IsPublished
    {
        get { return PublishDate != null; }
    }

    public DateTime? PublishDate { get; set; }

    public void Publish()
    {
        if (IsPublished)
            throw new InvalidOperationException("Already published.");

        PublishDate = DateTime.Today;

        Raise(new PublishedEvent());
    }
}

La mia soluzione è stata quella di rendere privati i setter di proprietà, il che è possibile perché l'ORM che stiamo usando per idratare gli oggetti usa la riflessione in modo che sia in grado di accedere ai setter privati. Tuttavia, questo presenta un problema quando si tenta di scrivere test unitari. Ad esempio, quando voglio scrivere un test unitario che verifica il requisito che non possiamo ri-pubblicare, devo indicare che l'oggetto è già stato pubblicato. Posso certamente farlo chiamando Publish due volte, ma il mio test presuppone che Publish sia implementato correttamente per la prima chiamata. Sembra un po 'puzzolente.

Rendiamo lo scenario un po 'più reale con il seguente codice:

public class Document
{
    public Document(String title)
    {
        if (String.IsNullOrWhiteSpace(title))
            throw new ArgumentException("title");

        Title = title;
    }

    public String ApprovedBy { get; private set; }
    public DateTime? ApprovedOn { get; private set; }
    public Boolean IsApproved { get; private set; }
    public Boolean IsPublished { get; private set; }
    public String PublishedBy { get; private set; }
    public DateTime? PublishedOn { get; private set; }
    public String Title { get; private set; }

    public void Approve(String by)
    {
        if (IsApproved)
            throw new InvalidOperationException("Already approved.");

        ApprovedBy = by;
        ApprovedOn = DateTime.Today;
        IsApproved = true;

        Raise(new ApprovedEvent(Title));
    }

    public void Publish(String by)
    {
        if (IsPublished)
            throw new InvalidOperationException("Already published.");

        if (!IsApproved)
            throw new InvalidOperationException("Cannot publish until approved.");

        PublishedBy = by;
        PublishedOn = DateTime.Today;
        IsPublished = true;

        Raise(new PublishedEvent(Title));
    }
}

Voglio scrivere test unitari che verificano:

  • Non posso pubblicare se il documento non è stato approvato
  • Non riesco a pubblicare nuovamente un documento
  • Una volta pubblicati, i valori PublishedBy e PublishedOn sono corretti set
  • Una volta pubblicato, l'evento PublishedEvent viene sollevato

Senza l'accesso ai setter, non posso mettere l'oggetto nello stato necessario per eseguire i test. L'apertura dell'accesso ai setter sconfigge lo scopo di impedire l'accesso.

In che modo (hai) hai risolto (d) questo problema?

    
posta SonOfPirate 18.03.2013 - 13:48
fonte

5 risposte

26

I cannot put the object into the state needed to perform the tests.

Se non è possibile inserire l'oggetto nello stato necessario per eseguire un test, non è possibile inserire l'oggetto nello stato nel codice di produzione, quindi non è necessario testare lo stato quello . Ovviamente, questo non è vero nel tuo caso, puoi mettere il tuo oggetto nello stato necessario, basta chiamare Approva.

  • Non posso pubblicare a meno che il documento non sia stato approvato: scrivi un test che invoca la pubblicazione prima di chiamare approve causa il giusto errore senza modificare lo stato dell'oggetto.

    void testPublishBeforeApprove() {
        doc = new Document("Doc");
        AssertRaises(doc.publish, ..., NotApprovedException);
    }
    
  • Non riesco a ripubblicare un documento: scrivi un test che approva un oggetto, quindi chiama la pubblicazione una volta riuscito, ma la seconda volta causa il giusto errore senza modificare lo stato dell'oggetto.

    void testRePublish() {
        doc = new Document("Doc");
        doc.approve();
        doc.publish();
        AssertRaises(doc.publish, ..., RepublishException);
    }
    
  • Quando vengono pubblicati, i valori PublishedBy e PublishedOn sono impostati correttamente: scrivi un test che chiama approve e poi chiama publish, asserisci che lo stato dell'oggetto cambia correttamente

    void testPublish() {
        doc = new Document("Doc");
        doc.approve();
        doc.publish();
        Assert(doc.PublishedBy, ...);
        ...
    }
    
  • Quando viene pubblicato, l'evento PublishedEvent viene generato: aggancia al sistema degli eventi e imposta un flag per assicurarti che sia chiamato

Devi anche scrivere un test per approvare.

In altre parole, non testare la relazione tra i campi interni e IsPublished e IsApproved, il test sarebbe abbastanza fragile se lo fai poiché cambiare il tuo campo significherebbe cambiare il codice del test, quindi il test sarebbe abbastanza inutile. Dovresti invece testare la relazione tra le chiamate dei metodi pubblici, in questo modo, anche se modifichi i campi in cui non avresti bisogno di modificare il test.

    
risposta data 18.03.2013 - 16:19
fonte
2

Ancora un altro approccio consiste nel creare un costruttore della classe che consente di impostare le proprietà interne durante l'istanziazione:

 public Document(
  String approvedBy,
  DateTime? approvedOn,
  Boolean isApproved,
  Boolean isPublished,
  String publishedBy,
  DateTime? publishedOn,
  String title)
{
  ApprovedBy = approvedBy;
  ApprovedOn = approvedOn;
  IsApproved = isApproved;
  IsApproved = isApproved;
  PublishedBy = publishedBy;
  PublishedOn = publishedOn;
}
    
risposta data 18.03.2013 - 14:42
fonte
1

Una strategia è che si eredita la classe (in questo caso il documento) e si scrivono test contro la classe ereditata. La classe ereditata consente in qualche modo di impostare lo stato dell'oggetto nei test.

In C # una strategia potrebbe essere quella di rendere i setter interni, quindi di esporre gli interni per testare il progetto.

Potresti anche usare la classe API come hai descritto ("Posso certamente farlo chiamando Pubblica due volte"). Questo sarebbe impostare lo stato dell'oggetto usando i servizi pubblici dell'oggetto, non mi sembra troppo maleodorante. Nel caso del tuo esempio, questo probabilmente sarebbe il modo in cui lo farei.

    
risposta data 18.03.2013 - 14:28
fonte
1

Per testare in assoluto isolamento i comandi e le query che gli oggetti del dominio ricevono, io sono usato fornire ogni test con una serializzazione dell'oggetto nello stato previsto. Nella sezione di organizzazione del test carica l'oggetto da testare da un file che ho preparato in precedenza. All'inizio ho iniziato con le serializzazioni binarie, ma JSON ha dimostrato di essere molto più facile da mantenere. Questo ha dimostrato di funzionare bene, ogni volta che l'isolamento assoluto nei test fornisce un valore reale.

modifica solo una nota, a volte la serializzazione JSON fallisce (come nel caso dei grafici di oggetti ciclici, che sono un odore, btw). In tali situazioni, salvataggio la serializzazione binaria. È un po 'pragmatico, ma funziona. : -)

    
risposta data 18.03.2013 - 21:06
fonte
-7

Dici

do try to apply best practices whenever possible

e

the ORM we are using to hydrate the objects uses reflection so it is able to access private setters

e devo pensare che l'uso della reflection per bypassare i controlli di accesso sulle tue classi non sia quello che definirei "best practice". Sarà anche terribilmente lento.

Personalmente, vorrei scartare il tuo framework di test unitario e andare con qualcosa in classe - sembra che tu stia scrivendo test dal punto di vista di testare l'intera classe, il che è positivo. In passato, per alcuni componenti complicati che necessitavano di test, ho incorporato gli assert e il codice di installazione nella classe stessa (era un modello di progettazione comune con un metodo test () in ogni classe), quindi si crea un client ciò semplifica semplicemente l'istanza di un oggetto e chiama il metodo di prova che può essere impostato come desiderato senza cattiveria come gli hack di riflessione.

Se ti preoccupi del codice gonfiato, avvolgi i metodi di prova in #ifdefs per renderli disponibili solo nel codice di debug (probabilmente una best practice in sé)

    
risposta data 18.03.2013 - 14:34
fonte

Leggi altre domande sui tag