2013-05-20 19 views
10

Ho avuto un problema strano mentre usavo NSubstitute un paio di volte e sebbene so come aggirare non sono mai riuscito a spiegarlo.Restituire il risultato di un metodo che restituisce un altro sostituto genera un'eccezione in NSubstitute

Ho creato quello che sembra essere il test minimo richiesto per dimostrare il problema e sembra che abbia a che fare con l'utilizzo di un metodo per creare un valore di ritorno sostituito.

public interface IMyObject 
{ 
    int Value { get; } 
} 

public interface IMyInterface 
{ 
    IMyObject MyProperty { get; } 
} 

[TestMethod] 
public void NSubstitute_ReturnsFromMethod_Test() 
{ 
    var sub = Substitute.For<IMyInterface>(); 

    sub.MyProperty.Returns(MyMethod()); 
} 

private IMyObject MyMethod() 
{ 
    var ob = Substitute.For<IMyObject>(); 
    ob.Value.Returns(1); 
    return ob; 
} 

Quando eseguo la prova di cui sopra ottengo la seguente eccezione:

Test method globalroam.Model.NEM.Test.ViewModel.DelayedAction_Test.NSubstitute_ReturnsFromMethod_Test threw exception: 
NSubstitute.Exceptions.CouldNotSetReturnException: Could not find a call to return from. 
Make sure you called Returns() after calling your substitute (for example: mySub.SomeMethod().Returns(value)). 
If you substituted for a class rather than an interface, check that the call to your substitute was on a virtual/abstract member. 
Return values cannot be configured for non-virtual/non-abstract members. 

Tuttavia, se cambio il metodo di prova per restituire questo:

sub.MyProperty.Returns((a) => MyMethod()); 

o questo:

var result = MyMethod(); 
sub.MyProperty.Returns(result); 

Funziona .

Mi chiedo solo se qualcuno può spiegare perché questo accade?

risposta

20

Per far funzionare la sintassi NSubstitute, dietro le quinte c'è un po 'di confusione. Questo è uno di quei casi in cui ci morde. Diamo un'occhiata a una versione modificata del vostro esempio prima:

sub.MyProperty.Returns(someValue); 

In primo luogo, sub.MyProperty si chiama, che restituisce un IMyObject. Viene quindi chiamato il metodo di estensione Returns, che deve in qualche modo calcolare quale chiamata deve restituire someValue per. Per fare ciò, NSubstitute registra l'ultima chiamata ricevuta in qualche stato globale da qualche parte. Returns in pseudo-ish-codice sembra qualcosa di simile:

public static void Returns<T>(this T t, T valueToReturn) { 
    var lastSubstitute = bigGlobOfStaticState.GetLastSubstituteCalled(); 
    lastSubstitute.SetReturnValueForLastCall(valueToReturn); 
    bigGlobOfStaticState.ClearLastCall(); // have handled last call now, clear static state 
} 

Quindi valutare l'intera chiamata sembra un po 'come questo:

sub.MyProperty   // <-- last call is sub.MyProperty 
    .Returns(someValue) // <-- make sub.MyProperty return someValue and 
         //  clear last call, as we have already set 
         //  a result for it 

Ora vediamo cosa succede quando chiamiamo un altro sostituto durante il tentativo di impostare il valore di ritorno:

sub.MyProperty.Returns(MyMethod()); 

Anche in questo caso viene valutata sub.MyProperty, quindi deve valutare Returns. Prima di poterlo fare, è necessario valutare gli argomenti su Returns, che significa eseguire MyMethod(). Questa valutazione sembra più simile a questo:

//Evaluated as: 
sub.MyProperty  // <- last call is to sub.MyProperty, as before 
    .Returns(
    // Now evaluate arguments to Returns: 
    MyMethod() 
     var ob = Substitute.For<IMyObject>() 
     ob.Value  // <- last call is now to ob.Value, not sub.MyProperty! 
     .Returns(1) // <- ok, ob.Value now returns 1, and we have used up the last call 
    //Now finish evaluating origin Returns: 
    GetLastSubstituteCalled *ugh, can't find one, crash!* 

V'è un altro esempio dei problemi che questo può causare here.

È possibile aggirare questo rinviando la chiamata a MyMethod(), utilizzando:

sub.MyProperty.Returns(x => MyMethod()); 

Questo funziona perché MyMethod() eseguirà solo quando è necessario utilizzare un valore di ritorno, in modo che il metodo statico GetLastSubstituteCalled non lo fa confondersi.

Piuttosto che farlo, preferisco evitare altre chiamate ai sostituti mentre sono occupato a configurarne uno.

Spero che questo aiuti. :)

+0

Grazie per la risposta dettagliata. È molto più complicato di quanto mi aspettassi. Pensi che questo sia considerato un bug in NHibernate? Raccolgo dalla tua risposta che si tratta di un problema noto e piuttosto complesso da risolvere. – craftworkgames

+1

È una limitazione della scelta della sintassi. La modifica di NSub per l'utilizzo di SubstituteFactories locali potrebbe aiutare (potremmo mantenere un sacco di stati in giro per test e ricerche, piuttosto che stato globale), ma non sicuro se le persone sarebbero disposte a complicare la sintassi per la creazione di sottotitoli. Penso che lo preferirei, ma non sono sicuro che sia un problema abbastanza grande da giustificare il cambiamento a questo punto. –

+0

Il vero problema è che il codice sembrava corretto da ogni angolazione, quindi era difficile diagnosticare il problema. Forse potrebbero essere utili messaggi di errore migliori, ma non ho idea di cosa potrebbero essere. Grazie ancora. – craftworkgames