Moq: come arrivare a un parametro passato a un metodo di un servizio deriso


169

Immagina questa classe

public class Foo {

    private Handler _h;

    public Foo(Handler h)
    {
        _h = h;
    }

    public void Bar(int i)
    {
        _h.AsyncHandle(CalcOn(i));
    }

    private SomeResponse CalcOn(int i)
    {
        ...;
    }
}

Mo (q) cking Handler in un test di Foo, come potrei controllare a cosa Bar()è passato _h.AsyncHandle?


Intendevi "AsyncHandle" (extra "n")? E potresti pubblicare il codice per Handler o specificare il nome del tipo completo se è un tipo standard?
TrueWill,

Puoi mostrare il tuo test di scheletro per mostrare quello che stai pensando? Mentre apprezzo che dalla tua parte sia ovvio, dalla nostra parte, sembra che qualcuno che non abbia avuto il tempo di rendere la domanda responsabile senza fare una lunga risposta speculativa.
Ruben Bartelink,

1
Non esiste né Foo né Bar () né niente del genere. È solo un codice demo per mostrare la situazione in cui mi trovo senza distrazione dalle specifiche dell'applicazione. E ho avuto solo la risposta, speravo di ottenere.
Jan

Risposte:


283

È possibile utilizzare il metodo Mock.Callback:

var mock = new Mock<Handler>();
SomeResponse result = null;
mock.Setup(h => h.AnsyncHandle(It.IsAny<SomeResponse>()))
    .Callback<SomeResponse>(r => result = r);

// do your test
new Foo(mock.Object).Bar(22);
Assert.NotNull(result);

Se vuoi solo controllare qualcosa di semplice sull'argomento passato, puoi anche farlo direttamente:

mock.Setup(h => h.AnsyncHandle(It.Is<SomeResponse>(response => response != null)));

36
Una nota a margine, se hai più argomenti per la tua funzione, devi specificare tutti i tipi nel Callback<>()metodo Moq generico . Ad esempio, se il metodo avesse la definizione Handler.AnsyncHandle(string, SomeResponse), ne avresti bisogno /* ... */.Callback<string, SomeResponse>(r => result = r);. Non l'ho trovato esplicitamente dichiarato in molti posti, quindi ho pensato di aggiungerlo qui.
Frank Bryce,

12
Volevo solo correggere @Frank nel caso non avessi visto la risposta di @JavaJudt. Il modo corretto per ottenere due argomenti è:/* ... */.Callback<string, SomeResponse>((s1, s2) => { str1 = s1; result = s2});
renatogbp

2
Puoi anche ottenere lo stesso semplicemente dichiarando i tipi degli argomenti nell'espressione lambda, in questo modo:.Callback((string s1, SomeResponse s2) => /* stuff */ )
jb637

1
Potresti aggiornare la tua risposta per includere un riferimento Capture.Inall'helper integrato ?
Cao

29

La risposta di Gamlor ha funzionato per me, ma ho pensato di ampliare il commento di John Carpenter perché cercavo una soluzione che coinvolgesse più di un parametro. Ho pensato che altre persone che si imbattono in questa pagina potrebbero trovarsi in una situazione simile. Ho trovato queste informazioni nella documentazione di Moq .

Userò l'esempio di Gamlor, ma facciamo finta che il metodo AsyncHandle abbia due argomenti: a stringe un SomeResponseoggetto.

var mock = new Mock<Handler>();
string stringResult = string.Empty;
SomeResponse someResponse = null;
mock.Setup(h => h.AsyncHandle(It.IsAny<string>(), It.IsAny<SomeResponse>()))
    .Callback<string, SomeResponse>((s, r) => 
    {
        stringResult = s;
        someResponse = r;
    });

// do your test
new Foo(mock.Object).Bar(22);
Assert.AreEqual("expected string", stringResult);
Assert.IsNotNull(someResponse);

Fondamentalmente devi solo aggiungerne un altro It.IsAny<>()con il tipo appropriato, aggiungere un altro tipo al Callbackmetodo e modificare l'espressione lambda come appropriato.


22

Il metodo Callback funzionerà sicuramente, ma se lo stai facendo su un metodo con molti parametri può essere un po 'prolisso. Ecco qualcosa che ho usato per rimuovere parte della piastra della caldaia.

var mock = new Mock<Handler>();

// do your test   
new Foo(mock.Object).Bar(22);

var arg = new ArgumentCaptor<SomeResponse>();
mock.Verify(h => h.AsyncHandle(arg.Capture()));
Assert.NotNull(arg.Value);

Ecco la fonte per ArgumentCaptor:

public class ArgumentCaptor<T>
{
    public T Capture()
    {
        return It.Is<T>(t => SaveValue(t));
    }

    private bool SaveValue(T t)
    {
        Value = t;
        return true;
    }

    public T Value { get; private set; }
}

21

La risposta di Gamlor funziona, ma un altro modo di farlo (e uno che considero più espressivo nel test) è ...

var mock = new Mock<Handler>();
var desiredParam = 47; // this is what you want to be passed to AsyncHandle
new Foo(mock.Object).Bar(22);
mock.Verify(h => h.AsyncHandle(desiredParam), Times.Once());

Verifica è molto potente e vale la pena dedicare del tempo per abituarsi.


15
Questo approccio va bene se vuoi solo verificare se un metodo è stato chiamato con un parametro noto. Nel caso in cui il parametro non sia ancora stato creato nel momento in cui si scrive il test (ad es. L'unità in questione crea il parametro internamente), Callback consente di acquisire e interrogare ciò, mentre il proprio approccio non lo farebbe.
Michael,

1
Devo archiviare il valore passato perché devo verificare che tutti i set di oggetti siano stati passati.
MrFox,

Inoltre, a volte il parametro è un'entità e si desidera asserzioni separate per ciascun campo nell'entità. Il modo migliore per farlo è catturare il parametro, piuttosto che usare Verifica e un matcher.
Kevin Wong,

12

L'alternativa è anche quella di utilizzare la Capture.Infunzione di moq. È la moqfunzione OOTB che abilita la cattura degli argomenti nella raccolta.

//Arrange
var args = new List<SomeResponse>();
mock.Setup(h => h.AnsyncHandle(Capture.In(args)));

//Act
new Foo(mock.Object).Bar(22);

//Assert
//... assert args.Single() or args.First()

1
Bella risposta! Non ero a conoscenza delle classi Moq.Capture e Moq.CaptureMatch fino a quando non ho visto questa risposta. Capture è una migliore alternativa a CallbackIMO. Poiché si utilizza Capture direttamente nell'elenco dei parametri, è molto meno soggetto a problemi durante il refactoring dell'elenco dei parametri di un metodo e pertanto rende i test meno fragili. Con Callback, è necessario mantenere sincronizzati i parametri passati in Setup con i parametri di tipo utilizzati per Callback e in passato mi ha sicuramente causato problemi.
Justin Holzer,

7

Puoi usare il It.Is<TValue>()matcher.

var mock = new Mock<Handler>();
new Foo(mock.Object).Bar(22);
mock.Verify(h => h.AsyncHandle(It.Is<SomeResponse>(r => r != null )));

2

Questo funziona anche:

Mock<InterfaceThing> mockedObject = new Mock<InterfaceThing>();
var objectParameter = mockedObject.Invocations[1].Arguments[0] as ObjectParameter;

2

Molte buone risposte qui! Vai con le funzionalità predefinite di Moq fino a quando non devi fare affermazioni su diversi parametri di classe passati alle tue dipendenze. Se ti trovi in ​​quella situazione, però, la funzione Moq Verify con It.Is Matchers non fa un buon lavoro per isolare l'errore del test, e il modo Returns / Callback di catturare argomenti aggiunge righe di codice non necessarie al tuo test (e i test lunghi sono un must per me).

Ecco un riassunto: https://gist.github.com/Jacob-McKay/8b8d41ebb9565f5fca23654fd944ac6b con un'estensione Moq (4.12) che ho scritto che fornisce un modo più dichiarativo per fare affermazioni su argomenti passati a beffe, senza gli svantaggi summenzionati. Ecco come appare la sezione Verifica ora:

        mockDependency
            .CheckMethodWasCalledOnce(nameof(IExampleDependency.PersistThings))
            .WithArg<InThing2>(inThing2 =>
            {
                Assert.Equal("Input Data with Important additional data", inThing2.Prop1);
                Assert.Equal("I need a trim", inThing2.Prop2);
            })
            .AndArg<InThing3>(inThing3 =>
            {
                Assert.Equal("Important Default Value", inThing3.Prop1);
                Assert.Equal("I NEED TO BE UPPER CASED", inThing3.Prop2);
            });

Sarei entusiasta se Moq fornisse una funzione che ha realizzato la stessa cosa pur essendo dichiarativa e fornendo l'isolamento di fallimento. Dita incrociate!


1
Mi piace questo. La verifica di Moq compete con l'asserzione di xUnit per l'autorità di eseguire asserzioni. Questo non sembra giusto nella parte Moq dell'installazione. La configurazione della funzione It.Is dovrebbe supportare anche le non espressioni.
Thomas,

"Moq compete con Assert di xUnit per l'autorità di eseguire assert" - Ben detto @Thomas. Aggiungo che perderebbe la concorrenza. Moq è bravo a farti sapere se c'è stata una chiamata corrispondente, ma affermazioni specifiche ti danno informazioni molto migliori. Il principale svantaggio del mio approccio è perdere la sicurezza del tipo e il controllo dell'ordine dei parametri. Ho cercato un miglioramento su questo per un po ', speriamo che ci sia un ninja C # là fuori che può hackerare un esempio insieme! Altrimenti se trovo un modo lo aggiornerò.
Jacob McKay,
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.