Verificando um parâmetro específico com Moq

169
public void SubmitMessagesToQueue_OneMessage_SubmitSuccessfully()
{
    var messageServiceClientMock = new Mock<IMessageServiceClient>();
    var queueableMessage = CreateSingleQueueableMessage();
    var message = queueableMessage[0];
    var xml = QueueableMessageAsXml(queueableMessage);
    messageServiceClientMock.Setup(proxy => proxy.SubmitMessage(xml)).Verifiable();
    //messageServiceClientMock.Setup(proxy => proxy.SubmitMessage(It.IsAny<XmlElement>())).Verifiable();

    var serviceProxyFactoryStub = new Mock<IMessageServiceClientFactory>();
    serviceProxyFactoryStub.Setup(proxyFactory => proxyFactory.CreateProxy()).Returns(essageServiceClientMock.Object);
    var loggerStub = new Mock<ILogger>();

    var client = new MessageClient(serviceProxyFactoryStub.Object, loggerStub.Object);
    client.SubmitMessagesToQueue(new List<IMessageRequestDTO> {message});

    //messageServiceClientMock.Verify(proxy => proxy.SubmitMessage(xml), Times.Once());
    messageServiceClientMock.Verify();
}

Estou começando a usar o Moq e lutando um pouco. Estou tentando verificar se messageServiceClient está recebendo o parâmetro correto, que é um XmlElement, mas não consigo encontrar nenhuma maneira de fazê-lo funcionar. Funciona apenas quando não verifico um valor específico.

Alguma ideia?

Resposta parcial: Encontrei uma maneira de testar se o xml enviado ao proxy está correto, mas ainda não acho que seja a maneira correta de fazê-lo.

public void SubmitMessagesToQueue_OneMessage_SubmitSuccessfully()
{
    var messageServiceClientMock = new Mock<IMessageServiceClient>();
    messageServiceClientMock.Setup(proxy => proxy.SubmitMessage(It.IsAny<XmlElement>())).Verifiable();
    var serviceProxyFactoryStub = new Mock<IMessageServiceClientFactory>();
    serviceProxyFactoryStub.Setup(proxyFactory => proxyFactory.CreateProxy()).Returns(messageServiceClientMock.Object);
    var loggerStub = new Mock<ILogger>();

    var client = new MessageClient(serviceProxyFactoryStub.Object, loggerStub.Object);
    var message = CreateMessage();
    client.SubmitMessagesToQueue(new List<IMessageRequestDTO> {message});

    messageServiceClientMock.Verify(proxy => proxy.SubmitMessage(It.Is<XmlElement>(xmlElement => XMLDeserializer<QueueableMessage>.Deserialize(xmlElement).Messages.Contains(message))), Times.Once());
}

A propósito, como eu poderia extrair a expressão da chamada Verificar?

Luis Mirabal
fonte

Respostas:

250

Se a lógica de verificação não for trivial, será complicado escrever um método lambda grande (como mostra o exemplo). Você pode colocar todas as instruções de teste em um método separado, mas eu não gosto de fazer isso porque isso interrompe o fluxo de leitura do código de teste.

Outra opção é usar um retorno de chamada na instalação para armazenar o valor que foi passado para o método simulado e, em seguida, escrever Assertmétodos padrão para validá-lo. Por exemplo:

// Arrange
MyObject saveObject;
mock.Setup(c => c.Method(It.IsAny<int>(), It.IsAny<MyObject>()))
        .Callback<int, MyObject>((i, obj) => saveObject = obj)
        .Returns("xyzzy");

// Act
// ...

// Assert
// Verify Method was called once only
mock.Verify(c => c.Method(It.IsAny<int>(), It.IsAny<MyObject>()), Times.Once());
// Assert about saveObject
Assert.That(saveObject.TheProperty, Is.EqualTo(2));
Rich Tebb
fonte
6
Um grande benefício dessa abordagem é que ela fornecerá falhas de teste específicas sobre como o objeto está incorreto (conforme você está testando cada um individualmente).
Rob Church
1
Eu pensei que era o único que fez isso, feliz por ver que é uma abordagem razoável!
Will Appleby
3
Acho que usando It.Is <MyObject> (validador) conforme Mayo é melhor, pois evita a maneira um pouco estranha de salvar o valor do parâmetro como parte do lambda
stevec
esse thread é seguro, por exemplo, ao executar testes em paralelo?
Anton Tolken 22/07/19
@AntonTolken Eu não tentei, mas no meu exemplo, o objeto que é atualizado é uma variável local (saveObject), portanto deve ser seguro para threads.
Rich Tebb 23/07/19
112

Venho verificando as chamadas da mesma maneira - acredito que é a maneira correta de fazer isso.

mockSomething.Verify(ms => ms.Method(
    It.IsAny<int>(), 
    It.Is<MyObject>(mo => mo.Id == 5 && mo.description == "test")
  ), Times.Once());

Se sua expressão lambda se tornar pesada, você poderá criar uma função que assume MyObjectcomo entrada e saída true/ false...

mockSomething.Verify(ms => ms.Method(
    It.IsAny<int>(), 
    It.Is<MyObject>(mo => MyObjectFunc(mo))
  ), Times.Once());

private bool MyObjectFunc(MyObject myObject)
{
  return myObject.Id == 5 && myObject.description == "test";
}

Além disso, esteja ciente de um bug no Mock, no qual a mensagem de erro informa que o método foi chamado várias vezes quando não foi chamado. Eles podem ter corrigido isso agora - mas se você vir essa mensagem, poderá verificar se o método foi realmente chamado.

EDIT: Aqui está um exemplo de chamada verificar várias vezes para os cenários em que você deseja verificar se você chama uma função para cada objeto em uma lista (por exemplo).

foreach (var item in myList)
  mockRepository.Verify(mr => mr.Update(
    It.Is<MyObject>(i => i.Id == item.Id && i.LastUpdated == item.LastUpdated),
    Times.Once());

A mesma abordagem para a instalação ...

foreach (var item in myList) {
  var stuff = ... // some result specific to the item
  this.mockRepository
    .Setup(mr => mr.GetStuff(item.itemId))
    .Returns(stuff);
}

Portanto, toda vez que o GetStuff for chamado para esse itemId, ele retornará itens específicos para esse item. Como alternativa, você pode usar uma função que tome itemId como entrada e retorne itens.

this.mockRepository
    .Setup(mr => mr.GetStuff(It.IsAny<int>()))
    .Returns((int id) => SomeFunctionThatReturnsStuff(id));

Um outro método que eu vi em um blog algum tempo atrás (Phil Haack talvez?) Tinha a configuração retornando de algum tipo de objeto de desenfileiramento - cada vez que a função era chamada, ele puxava um item de uma fila.

maionese
fonte
1
Obrigado, faz sentido para mim. O que ainda não consigo entender é quando especificar detalhes em Configuração ou Verificar. É bem confuso. No momento, estou apenas permitindo qualquer coisa na instalação e especificando os valores na verificação.
Luis Mirabal
Como você acha que posso verificar as mensagens quando há várias chamadas? O cliente recebe mensagens e pode criar várias mensagens queueable, que terminarão em várias chamadas e, em cada uma dessas chamadas, tenho que verificar mensagens diferentes. Ainda estou lutando com testes de unidade em geral, ainda não estou muito familiarizado com isso.
Luis Mirabal
Eu não acho que exista uma bala mágica de prata em termos de como você deve fazer isso. É preciso prática e você começa a melhorar. Para mim, só especifico parâmetros quando tenho algo para compará-los e quando ainda não estou testando esse parâmetro em outro teste. Quanto a várias chamadas, existem várias abordagens. Para configurar e verificar uma função chamada várias vezes, costumo chamar setup ou verificar (Times.Once ()) para cada chamada que espero - geralmente com um loop for. Você pode usar os parâmetros específicos para isolar cada chamada.
Mayo
Adicionei alguns exemplos para várias chamadas - veja a resposta acima.
Mayo
1
"Além disso, esteja ciente de um bug no Mock, no qual a mensagem de erro afirma que o método foi chamado várias vezes quando não foi chamado. Eles podem ter corrigido o problema agora - mas se você vir essa mensagem, considere verificar se o método foi realmente chamado ". - Um bug como esse invalida completamente uma biblioteca de zombaria IMHO. Como é irônico que eles não têm código de teste adequado para ele :-)
Gianluca Ghettini
20

Uma maneira mais simples seria fazer:

ObjectA.Verify(
    a => a.Execute(
        It.Is<Params>(p => p.Id == 7)
    )
);
dmitry.sergeyev
fonte
Não consigo fazer isso funcionar, estou tentando verificar se meu método foi chamado com o Code.WRCC como parâmetro. Mas meu teste passa sempre, mesmo que o parâmetro passado é WRDD .. m.Setup(x => x.CreateSR(Code.WRDD)).ReturnsAsync(0); await ClassUnderTest.CompleteCC(accountKey, It.IsAny<int>(), mockRequest); m.Verify(x => x.CreateSR(It.Is<Code>(p => p == Code.WRCC)), Times.Once());
Greg Quinn
1

Eu acredito que o problema no fato de que Moq irá verificar a igualdade. E, como XmlElement não substitui Equals, sua implementação verificará a igualdade de referência.

Você não pode usar um objeto personalizado para substituir os iguais?

Fernando
fonte
Sim, acabei fazendo isso. Percebi que o problema estava verificando o Xml. Na segunda parte da pergunta I adicionou-se uma possível resposta deserialising o XML para um objecto
Luis Mirabal
1

Tinha um desses também, mas o parâmetro da ação era uma interface sem propriedades públicas. Acabou usando It.Is () com um método separado e, dentro desse método, foi necessário zombar da interface

public interface IQuery
{
    IQuery SetSomeFields(string info);
}

void DoSomeQuerying(Action<IQuery> queryThing);

mockedObject.Setup(m => m.DoSomeQuerying(It.Is<Action<IQuery>>(q => MyCheckingMethod(q)));

private bool MyCheckingMethod(Action<IQuery> queryAction)
{
    var mockQuery = new Mock<IQuery>();
    mockQuery.Setup(m => m.SetSomeFields(It.Is<string>(s => s.MeetsSomeCondition())
    queryAction.Invoke(mockQuery.Object);
    mockQuery.Verify(m => m.SetSomeFields(It.Is<string>(s => s.MeetsSomeCondition(), Times.Once)
    return true
}
ds4940
fonte