papo we(b)log

software engineering slave!
posts - 29, comments - 49, trackbacks - 26

Mock parziali: una alternativa ai delegates

cavolo! non so quanto tempo è che mi dico "devi iniziare una serie di post di mock objects" e non trovo mai il tempo per organizzare le idee e partire... ora, dopo l'ennesima volta che Ayende mi stupisce, ho deciso di buttare giù la bozza di un primo post/articoletto su la mia libreria di mock preferita: Rhino.Mocks.

per paura di dilungarmi troppo (so di essere prolisso e "logorroico"), vado subito al cuore della discussione. mi interessa mostrare un sistema che ho adottato per descrivere il design delle mie applicazioni (per intederci, io come molti considero il TestDriven Development e in particalare l'uso dei Mock Object uno strumento di progettazione più che di testing).

mi succede spesso di progettare infatti, sequendo la mia attitutidine prettamente "top-down" (nonostante uno dei miei autori preferiti suggerisca "Code from the Bottom Up"), partendo dal metodo più esterno (un qualche Application Controller o Façade ) e via via suddividere il compito in sotto responsabilità più semplici. assegnare le singole sotto responsabilità è la parte più critica, e le alternative di progetto che conosco e uso sono:

  1. delega: estrarre delle interfacce "collaboratrici", cosa che faccio spesso: in questo modo ottengo tanti oggetti con poche responsabilità (il principio "favorire la composizione di oggetti piuttosto che l'ereditarietà di classe")
  2. ereditarietà: definire una classe (di solito astratta) in cui pongo i singoli sotto-metodi come astratti o virtuali e lascio alle classi concrete il compito di implementarli (in pratica il pattern Template Method, che invece "favorisce l'ereditarietà di classe" !)

a questo punto, per descrivere in un unit test la struttura risultante ho varie possibilità. per quanto riguarda la delega. la soluzione più comune (e valida) è quella di usare il principio dell'Inversione del Controllo, fornendo un qualche meccanismo per sostituire a tempo di esecuzione (del test) le istanze delle varie interfacce con dei mock objects: ad esempio con costruttori che li accettano come parametri o usando delle proprietà con "setter". fin qui nulla di nuovo (per me, mentre per approfondire consiglio la serie di post di J.Miller sul TDD Starter Kit). eccovi un esempio per il test, si tratta di un "handler" che per salvare un oggetto del dominio utilizza un service layer per la validazione e poi un data access per le operazioni di persistenza:

using System;
using NUnit.Framework;
using Rhino.Mocks;

[TestFixture]
public class PartialMocksFixture
{
    
private MockRepository _mocks;
    
    [SetUp]
    
public void SetUp()
    {
        _mocks = 
new MockRepository();
    }
    
    [TearDown]
    
public void TearDown()
    {
        _mocks.VerifyAll();
    }
    
    [Test]
    
public void WithComposition()
    {
        Handler fixture;
        IServiceLayer mockService;
        IDataAccess mockDataAccess;
        DomainObject item;
        
        
//init
        
mockService = _mocks.CreateMock<IServiceLayer>();
        mockDataAccess = _mocks.CreateMock<IDataAccess>();
        
        item = 
new DomainObject();
        fixture = 
new Handler(mockService, mockDataAccess);
        
        
//expect
        
using(_mocks.Ordered())
        {
            Expect
                .Call(mockService.Validate(item))
                .Return(
true);
            
            mockDataAccess.SaveOrUpdate(item);
            LastCall
                .IgnoreArguments();
        }
        _mocks.ReplayAll();
        
        
//execute
        
fixture.Save(item);
    }
}

ed ecco il (banale) codice in grado di compilare ed eseguire con successo il test:

public interface IServiceLayer
{
    
bool Validate(DomainObject item);
}

public interface IDataAccess
{
    
void SaveOrUpdate(DomainObject item);
}

public class DomainObject{ }

public class Handler
{
    
private IServiceLayer _serviceLayer;
    
private IDataAccess _dataAccess;
    
    
public Handler(IServiceLayer serviceLayer, IDataAccess dataAccess)
    {
        _serviceLayer = serviceLayer;
        _dataAccess = dataAccess;
    }
    
    
public virtual void Save(DomainObject item)
    {
        
if( _serviceLayer.Validate(item) )
        {
            _dataAccess.SaveOrUpdate(item);
        }
    }
}

veniamo ora al caso in cui, per scelte di progetto, si sia scelto di usare l'ereditarietà: questo significa che l'oggetto da testare è lo stesso che contiene i metodi "fake" (quelli cioè da sostituire). si tratta della situazione in cui dobbiamo scrivere i test ad esempio di un Layer SuperType. come procedere quindi alla scrittura del test di unità?

parto con la soluzione che ho usato per prima, che è durata per qualche tempo e che oggi ho deciso di abbandonare. si tratta (come dice il titolo del post) di usare i delegates : per ogni metodo da sostituire definisco un delegate (con firma corrispondente), e introduco nella classe base (astratta o meno) una proprietà pubblica che "punti" all'implementazione del metodo: durante il test sostituiremo i delegates con dei mock object, mentre nel codice reale ci saranno dei delegates ai metodi reali della classe stessa. ecco il codice del test:

[Test]
public void WithDelegates()
{
    BaseHandler fixture;
    ValidatorDelegate mockValidator;
    PersisterDelegate mockPersister;
    DomainObject item;
    
    
//init
    
mockValidator = _mocks.CreateMock<ValidatorDelegate>();
    mockPersister = _mocks.CreateMock<PersisterDelegate>();
    
    item = 
new DomainObject();
    fixture = 
new BaseHandler();
    
    fixture.Validator = mockValidator;
    fixture.Persister = mockPersister;
    
    
//expect
    
using(_mocks.Ordered())
    {
        Expect
            .Call(mockValidator(item))
            .Return(
true);
        
        mockPersister(item);
        LastCall
            .IgnoreArguments();
    }
    _mocks.ReplayAll();
    
    
//execute
    
fixture.Save(item);
}

ed ecco il codice per eseguirlo con successo:

public delegate bool ValidatorDelegate(DomainObject item);
public delegate void PersisterDelegate(DomainObject item);

public class BaseHandler
{
    
private ValidatorDelegate _validator;
    
private PersisterDelegate _persister;
    
    
public ValidatorDelegate Validator { set { _validator = value; } }
    
public PersisterDelegate Persister { set { _persister = value; } }
    
    
public BaseHandler()
    {
        _validator = 
new ValidatorDelegate(this.Validate);
        _persister = 
new PersisterDelegate(this.SaveOrUpdate);
    }

    
public virtual bool Validate(DomainObject item) { throw new NotImplementedException(); }
    
public virtual void SaveOrUpdate(DomainObject item) { throw new NotImplementedException(); }
    
    
public virtual void Save(DomainObject item)
    {
        
if( _validator(item) )
        {
            _persister(item);
        }
    }
}

come è facile intuire, questa soluzione è piuttosto "noiosa", perchè ci obbliga a dichiarare una classe delegate per ogni "passo" del metodo principale e inoltre dobbiamo anche ricordarci sempre di impostare i delegate sui metodo corretti. insomma, se troviamo qualcosa di meglio non mi dispiace. (per intenderci, io adoro i delegates, ma qui mi sembrano un po' un abuso).

l'ultima soluzione (per me definitiva fino a prova contraria) ce la fornisce l'ormai sempre più sorprendente Ayende, con la sua Rhino.Mocks. quindi, intendiamoci bene, questa soluzione funziona a meraviglia con questa libreria di mock, ma non ho idea se esista o meno qualcosa di analogo nelle altre (da tempo ho smesso di aggiornarmi). ecco di cosa si tratta: usiamo come oggetto di test un oggetto mock, definito come "mock parziale". il comportamento infatti dei metodi di un mock parziale è

  • quello reale, di default
  • quello mock, in caso si definisca una expectation su tale metodo

ecco come diventa il test di unità:

[Test]
public void WithPartialMocks()
{
    AnotherHandler fixture;
    DomainObject item;
    
    
//init        
    
item = new DomainObject();
    fixture = (AnotherHandler)_mocks.PartialMock(
typeof(AnotherHandler));
    
    
//expect
    
using(_mocks.Ordered())
    {
        Expect
            .Call(fixture.Validate(item))
            .Return(
true);
        
        fixture.SaveOrUpdate(item);
        LastCall
            .IgnoreArguments();
    }
    _mocks.ReplayAll();
    
    
//execute
    
fixture.Save(item);
}

ed ecco il codice per farlo eseguire con successo:

public class AnotherHandler
{
    
public virtual bool Validate(DomainObject item) { throw new NotImplementedException(); }
    
public virtual void SaveOrUpdate(DomainObject item) { throw new NotImplementedException(); }
    
    
public virtual void Save(DomainObject item)
    {
        
if( Validate(item) )
        {
            SaveOrUpdate(item);
        }
    }
}

come si vede questa soluzione è più compatta, sia nel test che nell'implementazione (per l'assenza dei campi privati, di proprietà setter o di costruttori). e inoltre vale la pena notare che funziona (come in questo caso) anche con classi concrete e non necessariamente astratte, basta utilizzare metodi pubblici virtuali (insomma, le stesse limitazioni che si hanno per i mock di classi invece che di interfacce). per le mie esigenze e le mie abitudini di progettazione è un'ottimo risultato, spero possa tornare utile anche a qualcun'altro.

Alla prossima!
-papo-

Print | posted on mercoledì 29 novembre 2006 17:12 | Filed Under [ .NET TDD ]

Powered by:
Powered By Subtext Powered By ASP.NET