Vérification d'un paramètre spécifique avec Moq

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

Je commence à utiliser Moq et j'ai un peu de mal. J'essaie de vérifier que messageServiceClient reçoit le bon paramètre, qui est un XmlElement, mais je ne trouve aucun moyen de le faire fonctionner. Cela ne fonctionne que lorsque je ne vérifie pas une valeur particulière.

Des idées?

Réponse partielle: j'ai trouvé un moyen de tester que le XML envoyé au proxy est correct, mais je ne pense toujours pas que ce soit la bonne façon de le faire.

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

À propos, comment pourrais-je extraire l'expression de l'appel de vérification?

Luis Mirabal
la source

Réponses:

252

Si la logique de vérification n'est pas triviale, il sera compliqué d'écrire une grande méthode lambda (comme le montre votre exemple). Vous pouvez placer toutes les instructions de test dans une méthode distincte, mais je n'aime pas faire cela car cela perturbe le flux de lecture du code de test.

Une autre option consiste à utiliser un rappel sur l'appel du programme d'installation pour stocker la valeur qui a été transmise à la méthode simulée, puis à écrire des Assertméthodes standard pour la valider. Par exemple:

// 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
la source
6
L'un des grands avantages de cette approche est qu'elle vous donnera des échecs de test spécifiques pour savoir comment l'objet est incorrect (car vous testez chacun individuellement).
Rob Church
1
Je pensais que j'étais le seul à avoir fait cela, heureux de voir que c'était une approche raisonnable!
Will Apple par
3
Je pense que l'utilisation de It.Is <MyObject> (validateur) selon Mayo est meilleure car elle évite la manière légèrement gênante de sauvegarder la valeur du paramètre dans le cadre du lambda
stevec
ce thread est-il sûr, par exemple lors de l'exécution de tests en parallèle?
Anton Tolken
@AntonTolken Je ne l'ai pas essayé, mais dans mon exemple, l'objet qui est mis à jour est une variable locale (saveObject) donc il devrait être thread-safe.
Rich Tebb
113

J'ai vérifié les appels de la même manière - je pense que c'est la bonne façon de le faire.

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

Si votre expression lambda devient peu maniable, vous pouvez créer une fonction qui prend MyObjectcomme entrée et sorties 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";
}

Aussi, soyez conscient d'un bogue avec Mock où le message d'erreur indique que la méthode a été appelée plusieurs fois alors qu'elle ne l'était pas du tout. Ils l'ont peut-être déjà résolu - mais si vous voyez ce message, vous pouvez envisager de vérifier que la méthode a été effectivement appelée.

EDIT: Voici un exemple d'appels à verify plusieurs fois pour les scénarios où vous souhaitez vérifier que vous appelez une fonction pour chaque objet d'une liste (par exemple).

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

Même approche pour la configuration ...

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

Ainsi, chaque fois que GetStuff est appelé pour cet itemId, il renverra des éléments spécifiques à cet élément. Vous pouvez également utiliser une fonction qui prend itemId comme entrée et renvoie des éléments.

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

Une autre méthode que j'ai vue sur un blog il y a quelque temps (Phil Haack peut-être?) Avait une configuration retournant d'une sorte d'objet de file d'attente - chaque fois que la fonction était appelée, elle tirait un élément d'une file d'attente.

Mayo
la source
1
Merci, cela a du sens pour moi. Ce que je ne comprends toujours pas, c'est quand spécifier les détails dans la configuration ou la vérification. C'est assez déroutant. Pour le moment, je n'autorise rien dans la configuration et je spécifie les valeurs dans Verify.
Luis Mirabal
Comment pensez-vous que je peux vérifier les messages lorsqu'il y a plusieurs appels? Le client prend des messages et peut créer plusieurs messages en file d'attente, qui se termineront par plusieurs appels et dans chacun de ces appels, je dois vérifier différents messages. J'ai encore du mal avec les tests unitaires en général, je ne les connais pas encore très bien.
Luis Mirabal
Je ne pense pas qu'il y ait une solution miracle pour savoir comment procéder. Cela demande de la pratique et vous commencez à vous améliorer. Pour moi, je ne spécifie des paramètres que lorsque j'ai quelque chose à comparer et lorsque je ne teste pas déjà ce paramètre dans un autre test. En ce qui concerne les appels multiples, il existe plusieurs approches. Pour configurer et vérifier une fonction qui est appelée plusieurs fois, j'appelle généralement setup ou verify (Times.Once ()) pour chaque appel auquel j'attends - souvent avec une boucle for. Vous pouvez utiliser les paramètres spécifiques pour isoler chaque appel.
Mayo
J'ai ajouté quelques exemples pour plusieurs appels - voir la réponse ci-dessus.
Mayo
1
«Aussi, soyez conscient d'un bogue avec Mock où le message d'erreur indique que la méthode a été appelée plusieurs fois alors qu'elle ne l'a pas été du tout. Ils l'ont peut-être résolu maintenant - mais si vous voyez ce message, vous pouvez envisager de le vérifier la méthode a en fait été appelée. " - Un bug comme celui-ci invalide complètement une bibliothèque moqueuse IMHO. Quelle ironie ils n'avaient pas de code de test approprié pour cela :-)
Gianluca Ghettini
20

Une manière plus simple serait de faire:

ObjectA.Verify(
    a => a.Execute(
        It.Is<Params>(p => p.Id == 7)
    )
);
dmitry.sergeyev
la source
Je n'arrive pas à faire fonctionner cela, j'essaie de vérifier que ma méthode a été appelée avec le Code.WRCC comme paramètre. Mais mon test réussit toujours, même si le paramètre passé est 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

Je pense que le problème réside dans le fait que Moq vérifiera l'égalité. Et, étant donné que XmlElement ne remplace pas Equals, son implémentation vérifiera l'égalité des références.

Ne pouvez-vous pas utiliser un objet personnalisé pour pouvoir remplacer les égaux?

Fernando
la source
Oui, j'ai fini par faire ça. J'ai réalisé que le problème était de vérifier le Xml. Dans la deuxième partie de la question, j'ai ajouté une réponse possible désérialisant le xml en un objet
Luis Mirabal
1

Il y en avait aussi un, mais le paramètre de l'action était une interface sans propriétés publiques. J'ai fini par utiliser It.Is () avec une méthode distincte et dans cette méthode a dû se moquer de l'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
la source