Moq: como chegar a um parâmetro passado para um método de um serviço simulado

169

Imagine essa aula

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 em um teste de Foo, como eu seria capaz de verificar o que Bar()passou _h.AsyncHandle?

Jan
fonte
Você quis dizer "AsyncHandle" (extra "n")? E você poderia postar o código para Handler ou especificar o nome completo do tipo, se for um tipo padrão?
TrueWill 17/07/10
Você pode mostrar seu teste de esqueleto para mostrar o que está pensando? Embora eu aprecie que, do seu lado, é óbvio, do nosso lado, parece que alguém não tomou o tempo necessário para responder à pergunta sem fazer uma resposta especulativa longa.
Ruben Bartelink
1
Não existe um Foo nem um Bar () nem nada assim. São apenas alguns códigos de demonstração para mostrar a situação em que estou, sem nos desviarmos dos detalhes da aplicação. E eu tenho apenas a resposta que esperava obter.
Jan

Respostas:

283

Você pode usar o método 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 você quiser apenas verificar algo simples no argumento passado, também poderá fazê-lo diretamente:

mock.Setup(h => h.AnsyncHandle(It.Is<SomeResponse>(response => response != null)));
Gamlor
fonte
36
Uma observação lateral, se você tiver vários argumentos para sua função, precisará especificar todos os tipos no Callback<>()método Moq genérico . Por exemplo, se seu método tivesse a definição Handler.AnsyncHandle(string, SomeResponse), você precisaria /* ... */.Callback<string, SomeResponse>(r => result = r);. Eu não encontrei isso explicitamente declarado em muitos lugares, então achei que o adicionaria aqui.
Frank Bryce
12
Só queria corrigir o @Frank, caso você não tenha visto a resposta do @JavaJudt. A maneira adequada para obter dois argumento é:/* ... */.Callback<string, SomeResponse>((s1, s2) => { str1 = s1; result = s2});
renatogbp
2
Você também pode obter o mesmo por apenas declarar os tipos de argumentos na expressão lambda, assim:.Callback((string s1, SomeResponse s2) => /* stuff */ )
jb637
1
Você poderia atualizar sua resposta para incluir uma referência ao Capture.Inauxiliar interno?
cao
29

A resposta de Gamlor funcionou para mim, mas pensei em expandir o comentário de John Carpenter porque estava procurando uma solução que envolvesse mais de um parâmetro. Imaginei que outras pessoas que tropeçam nessa página podem estar em uma situação semelhante. Encontrei essas informações na documentação do Moq .

Usarei o exemplo de Gamlor, mas vamos fingir que o método AsyncHandle usa dois argumentos: a stringe um SomeResponseobjeto.

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);

Basicamente, você só precisa adicionar outro It.IsAny<>()com o tipo apropriado, adicionar outro tipo ao Callbackmétodo e alterar a expressão lambda conforme apropriado.

JavaJudt
fonte
22

O método de retorno de chamada certamente funcionará, mas se você estiver fazendo isso em um método com muitos parâmetros, pode ser um pouco detalhado. Aqui está algo que eu usei para remover parte do clichê.

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);

Aqui está a fonte do 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; }
}
Andrew Radford
fonte
21

A resposta de Gamlor funciona, mas outra maneira de fazê-lo (e uma que considero mais expressiva no teste) é ...

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());

A verificação é muito poderosa e vale a pena se acostumar.

Pete Martin
fonte
15
Essa abordagem é boa se você quiser apenas verificar se um método foi chamado com um parâmetro conhecido. No caso em que o parâmetro ainda não foi criado no momento de escrever o teste (por exemplo, a unidade em questão cria o parâmetro internamente), o retorno de chamada permite capturar e interrogar isso, enquanto sua abordagem não o faria.
Michael
1
Preciso armazenar o valor passado, pois preciso verificar se todos os objetos foram passados.
MrFox
Além disso, às vezes o parâmetro é uma entidade e você deseja declarações separadas para cada campo na entidade. A melhor maneira de fazer isso é capturar o parâmetro, em vez de usar o Verify e um correspondente.
Kevin Wong
12

A alternativa é também usar o Capture.Inrecurso do moq. É o moqrecurso OOTB que permite a captura de argumentos na coleção.

//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()
Johnny
fonte
1
Ótima resposta! Eu não conhecia as classes Moq.Capture e Moq.CaptureMatch até ver esta resposta. A captura é uma alternativa melhor à CallbackIMO. Como você usa o Capture diretamente na lista de parâmetros, é muito menos suscetível a problemas ao refatorar a lista de parâmetros de um método e, portanto, torna os testes menos frágeis. Com o retorno de chamada, você deve manter os parâmetros passados ​​na instalação em sincronia com os parâmetros de tipo usados ​​para o retorno de chamada, e isso definitivamente me causou problemas no passado.
Justin Holzer
7

Você poderia usar o 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 )));
Ed Yablonsky
fonte
2

Isso também funciona:

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

Muitas boas respostas aqui! Vá com o conjunto de recursos Moq pronto para uso até precisar fazer afirmações sobre vários parâmetros de classe passados ​​para suas dependências. No entanto, se você acabar nessa situação, o recurso Moq Verify com It.Is não faz um bom trabalho em isolar a falha do teste, e a maneira de capturar retornos / retorno de chamada adiciona linhas de código desnecessárias ao seu teste (e testes longos não são para mim).

Aqui está uma síntese: https://gist.github.com/Jacob-McKay/8b8d41ebb9565f5fca23654fd944ac6b com uma extensão do Moq (4.12) que eu escrevi, que fornece uma maneira mais declarativa de fazer afirmações sobre argumentos passados ​​para zombarias, sem os inconvenientes mencionados acima. Aqui está a aparência da seção Verificar agora:

        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);
            });

Eu ficaria feliz se o Moq fornecesse um recurso que realizasse a mesma coisa, sendo tão declarativo e fornecendo o isolamento de falhas que isso faz. Dedos cruzados!

Jacob McKay
fonte
1
Eu gosto disso. A verificação do Moq concorre com a afirmação do xUnit pela autoridade de realizar afirmações. Isso não parece certo na parte Moq da instalação. A configuração do recurso It.Is também deve suportar não expressões.
Thomas
"O Moq concorre com a afirmação do xUnit pela autoridade de realizar afirmações" - Bem dito @Thomas. Eu acrescentaria que perderia a concorrência. O Moq é bom em informar se houve uma chamada correspondente, mas afirmações específicas fornecem informações muito melhores. A principal desvantagem da minha abordagem é perder o tipo de segurança e a verificação de ordem dos parâmetros. Eu tenho procurado uma melhoria sobre isso por um tempo, espero que haja um ninja C # por aí que possa hackear um exemplo juntos! Caso contrário, se eu encontrar uma maneira, atualizarei isso.
Jacob McKay