2013-03-23 17 views
9

Sto iniziando a utilizzare il framework Delphi-Mocks e sto avendo problemi con la simulazione di una classe che ha parametri nel costruttore. La funzione di classe "Crea" per TMock non consente i parametri. Se provi a creare un'istanza fittizia di TFoo.Create (Bar: someType); Ottengo un errore di conteggio dei parametri 'quando TObjectProxy.Create; tenta di chiamare il metodo 'Crea' di T.Delphi-Mock: simulazione di una classe con parametri nel costruttore

Chiaramente questo è perché il seguente codice non passa alcun parametro al metodo "Invoke":

instance := ctor.Invoke(rType.AsInstance.MetaclassType, []); 

Ho creato una funzione di classe di overload che Passa nei parametri:

class function Create(Args: array of TValue): TMock<T>; overload;static; 

e sta lavorando con il test limitato che ho fatto.

La mia domanda è:

È questo un bug o sto solo facendo male?

Grazie

PS: so che Delphi-Mocks è Interface-centrica, ma lo fa classi di supporto e la base di codice su cui sto lavorando è del 99% Classi.

+1

Ecco cosa non capisco. Se stai cercando di deridere una classe, perché vuoi creare un'istanza della classe che stai deridendo. Sicuramente il punto di derisione è che tu, beh, prendi in giro la classe. –

+1

Quando si esegue 'TMock .Create' il framework Mocks crea un'istanza di' TFoo'. Forse non capisco mock, ma ho pensato che il punto fosse che hai creato qualcosa che non era 'TFoo'. Voglio dire, se tutto ciò che devi fare è creare 'TFoo', quindi fallo. Se vuoi prenderlo in giro, trova un framework che crei una simulazione di 'TFoo' piuttosto che un'istanza di' TFoo'. –

+0

@David. Mi dispiace, la mia domanda salta direttamente al mio problema senza alcun background; Hai ragione. Voglio prendere in giro una classe il cui costruttore ha un parametro (s). Poiché l'esempio fornito nel progetto Delphi-Mocks mostra [esempio TesTObjectMock] (https://github.com/VSoftTechnologies/Delphi-Mocks/blob/master/Sample1Main.pas) la classe sotto test (TFoo) viene passata come parametro generico come in mock: = TMock .create. Il problema è nella funzione di classe "Crea" e chiama "Invoke". – TDF

risposta

6

Il problema fondamentale, a mio avviso, è che i risultati TMock<T>.Create nella classe in prova (CUT) vengono istanziati. Sospetto che il framework sia stato progettato partendo dal presupposto che si sarebbe deriso una classe base astratta. In tal caso, l'istanziazione sarebbe benigna. Sospetto che tu abbia a che fare con un codice legacy che non ha una classe base astratta e comoda per il CUT. Ma nel tuo caso, l'unico modo per istanziare il CUT consiste nel passare i parametri al costruttore e quindi sconfiggere l'intero scopo del derisione. E immagino piuttosto che sarà molto impegnativo riprogettare il codice legacy finché non si avrà una classe base astratta per tutte le classi che devono essere prese in giro.

Si sta scrivendo TMock<TFoo>.Create dove TFoo è una classe. Ciò comporta la creazione di un oggetto proxy.Ciò accade nel TObjectProxy<T>.Create. Il codice di cui si presenta così:

constructor TObjectProxy<T>.Create; 
var 
    ctx : TRttiContext; 
    rType : TRttiType; 
    ctor : TRttiMethod; 
    instance : TValue; 
begin 
    inherited; 
    ctx := TRttiContext.Create; 
    rType := ctx.GetType(TypeInfo(T)); 
    if rType = nil then 
    raise EMockNoRTTIException.Create('No TypeInfo found for T'); 

    ctor := rType.GetMethod('Create'); 
    if ctor = nil then 
    raise EMockException.Create('Could not find constructor Create on type ' + rType.Name); 
    instance := ctor.Invoke(rType.AsInstance.MetaclassType, []); 
    FInstance := instance.AsType<T>(); 
    FVMInterceptor := TVirtualMethodInterceptor.Create(rType.AsInstance.MetaclassType); 
    FVMInterceptor.Proxify(instance.AsObject); 
    FVMInterceptor.OnBefore := DoBefore; 
end; 

Come si può vedere il codice fa un presupposto che la classe ha un costruttore senza parametri. Quando si chiama questo sulla classe, il cui costruttore ha parametri, questo si traduce in un'eccezione RTTI di runtime.

Come ho capito il codice, la classe viene creata solo al fine di intercettare i suoi metodi virtuali. Non vogliamo fare nient'altro con la classe, dal momento che preferirebbe sconfiggere lo scopo di deriderlo. Tutto ciò di cui hai veramente bisogno è un'istanza di un oggetto con un vtable adatto che possa essere manipolato da TVirtualMethodInterceptor. Non hai bisogno o vuoi che il tuo costruttore funzioni. Vuoi solo essere in grado di prendere in giro una classe che capita di avere un costruttore che ha parametri.

Quindi, invece di chiamare questo codice al costruttore, suggerisco di modificarlo per farlo chiamare NewInstance. Questo è il minimo indispensabile che devi fare per avere un vtable che può essere manipolato. E dovrai anche modificare il codice in modo che non tenti di distruggere l'istanza di simulazione e invece chiama FreeInstance. Tutto questo funzionerà bene fino a quando tutto ciò che fai è chiamare metodi virtuali sul mock.

Le modifiche appaiono così:

constructor TObjectProxy<T>.Create; 
var 
    ctx : TRttiContext; 
    rType : TRttiType; 
    NewInstance : TRttiMethod; 
    instance : TValue; 
begin 
    inherited; 
    ctx := TRttiContext.Create; 
    rType := ctx.GetType(TypeInfo(T)); 
    if rType = nil then 
    raise EMockNoRTTIException.Create('No TypeInfo found for T'); 

    NewInstance := rType.GetMethod('NewInstance'); 
    if NewInstance = nil then 
    raise EMockException.Create('Could not find NewInstance method on type ' + rType.Name); 
    instance := NewInstance.Invoke(rType.AsInstance.MetaclassType, []); 
    FInstance := instance.AsType<T>(); 
    FVMInterceptor := TVirtualMethodInterceptor.Create(rType.AsInstance.MetaclassType); 
    FVMInterceptor.Proxify(instance.AsObject); 
    FVMInterceptor.OnBefore := DoBefore; 
end; 

destructor TObjectProxy<T>.Destroy; 
begin 
    TObject(Pointer(@FInstance)^).FreeInstance;//always dispose of the instance before the interceptor. 
    FVMInterceptor.Free; 
    inherited; 
end; 

Francamente questo sembra un po 'più ragionevole per me. Non ha sicuramente senso chiamare costruttori e distruttori.

Per favore fatemi sapere se sono largo del marchio qui e ho perso il punto. Questo è del tutto possibile!

+0

Prima di tutto ... WOW, solo WOW! Apprezzo molto il lavoro che hai dedicato a questo. Sono continuamente stupito dalla comunità SO. Grazie. Quindi, riferendosi alla domanda originale, questo è molto probabilmente un bug? Se è così vorrei presentare la tua soluzione al progetto (dopo lunghi test ovviamente). Va bene per te? – TDF

+0

@TDF Beh, non ne so abbastanza del design di Delphi mock per sapere se si tratta di un bug. Non vorrei certo suggerirlo. Ti suggerisco di contattare l'autore. L'autore conosce il design migliore di tutti. È una domanda e un argomento molto interessanti.Dal modo in cui hai abbastanza reputazione per essere in grado di votare. Puoi votare e accettare. Sicuramente penso che tu abbia delle risposte qui che meritano voti. –

+0

@DavideHeffernan Ho accettato la tua risposta e ho votato. Stai suggerendo che, come materia di etichetta, ho votato gli altri contributori? Hanno certamente aiutato con il problema, sono solo ignorante di questa usanza. Grazie – TDF

0

Disclaimer: Non ho conoscenza di Delphi-Mocks.

Immagino che questo sia di progettazione. Dal tuo codice di esempio sembra che Delphi-Mocks stia usando i generici. Se si desidera creare un'istanza di un'istanza di un parametro generico, come in:

function TSomeClass<T>.CreateType: T; 
begin 
    Result := T.Create; 
end; 

allora avete bisogno di un vincolo costruttore sulla classe generica:

TSomeClass<T: class, constructor> = class 

Avere un vincolo costruttore significa che il passato nel tipo deve avere un costruttore senza parametri.

Probabilmente si potrebbe fare qualcosa di simile

TSomeClass<T: TSomeBaseMockableClass, constructor> = class 

e dare TSomeBaseMockableClass un costruttore specifica pure che potrebbero poi essere utilizzati, MA:

richiedono tutti gli utenti del quadro di derivare tutte le loro classi da una classe base specifica è solo ... beh ... eccessivamente restrittiva (per usare un eufemismo) e soprattutto considerando l'eredità ereditaria di Delphi.

+0

Non è colpa dei farmaci generici. Il codice utilizza RTTI per chiamare il costruttore. Quale presuppone si chiama 'Crea'. Se il codice lo volesse potrebbe passare abbastanza facilmente i parametri quando si chiama 'Invoke' sull'istanza' TRttiMethod'. –

+0

@DavidHeffernan Aha. Ma anche se non spetta ai generici, in che modo il framework dovrebbe sapere quali parametri e quali valori passare? Rtti può determinare il numero e il tipo di parametri, ma il framework non avrebbe ancora un'idea del significato di essi. Se vuoi che il framework istanzia delle classi, hai bisogno di costruttori "generici": uno senza parametri e/o uno che riceve una matrice di TValue (o Variant); oppure devi avere un metodo generico sul framework mock per fornirgli una serie di TValue da passare in ordine di specifica in parametri specifici di un costruttore. –

+0

Passate semplicemente i parametri a 'TMock .Create'. Un array aperto di 'TValue'. Ma per me non riesco a capire perché vorresti usare i mock per creare un'istanza della classe reale. Non ha senso per me. –

3

Non sono sicuro di aver correttamente soddisfatto i vostri bisogni, ma forse questo approccio hacky potrebbe essere d'aiuto. Supponendo di avere una classe che ha bisogno di un parametro nel suo costruttore

type 
    TMyClass = class 
    public 
    constructor Create(AValue: Integer); 
    end; 

si può ereditare questa classe con un costruttore senza parametri e una proprietà di classe che contiene il parametro

type 
    TMyClassMockable = class(TMyClass) 
    private 
    class var 
    FACreateParam: Integer; 
    public 
    constructor Create; 
    class property ACreateParam: Integer read FACreateParam write FACreateParam; 
    end; 

constructor TMyClassMockable.Create; 
begin 
    inherited Create(ACreateParam); 
end; 

Ora è possibile utilizzare la proprietà di classe per trasferire il parametro al costruttore. Ovviamente devi dare la classe ereditata al framework mock, ma come nient'altro ha cambiato la classe derivata dovrebbe fare altrettanto.

Questo funzionerà anche, se si sa esattamente quando la classe è istanziata in modo da poter fornire il parametro appropriato alla proprietà della classe.

Inutile dire che questo approccio non è thread-safe.

+0

Il problema fondamentale che posso vedere è che il CUT finisce per essere istanziato. Sicuramente questo è ciò che stiamo cercando di evitare in primo luogo. Ovviamente, ciò che proponi consentirà di compilare e gestire il codice, entro i confini di questo framework. –

Problemi correlati