Fermer

juin 28, 2022

Comment simplifier les tests unitaires C# avec un framework moqueur

Comment simplifier les tests unitaires C# avec un framework moqueur


Le moyen le plus rapide de faire passer vos tests unitaires au niveau supérieur consiste à utiliser un cadre de simulation. Voyons comment ils fonctionnent ensemble.

Qu’est-ce que la moquerie ?

Il est temps de faire passer vos tests unitaires au niveau supérieur. Vous avez implémenté soit Religieuse ou xUnit ou
MSTest dans vos projets. Vous avez obtenu une couverture de code de plus de 80 %. Mais il y a juste certaines choses qui sont difficiles à tester ou à valider dans votre projet.

Comment testez-vous la « logique métier » dans votre classe de référentiel ? Comment testez-vous votre service Web ou votre base de données dépendante ? Oui, vous pouvez écrire des tests unitaires spéciaux et créer de faux objets pour imiter ces dépendances, mais pourquoi perdre votre temps à écrire du code qui n’est pas livré avec le produit final. Ou écrire beaucoup de code avec le ExcludeFromCoverage attribut. 😊 Eh bien, c’est là que la moquerie entre en jeu.

La moquerie est un processus qui vous permet de créer un objet fictif qui peut être utilisé pour simuler le comportement d’un objet réel. Vous pouvez utiliser l’objet fictif pour vérifier que l’objet réel a été appelé avec les paramètres attendus et pour vérifier que l’objet réel n’a pas été appelé avec des paramètres inattendus. Vous pouvez également vérifier que l’objet réel a été appelé le nombre de fois attendu. Et vous pouvez vérifier que l’objet réel a été appelé avec les paramètres attendus et que l’objet réel n’a pas été appelé avec des paramètres inattendus. Les possibilités sont infinies.

La moquerie se décline en trois saveurs : faux, talons et se moque. Les contrefaçons sont les plus simples. Ils sont utilisés lorsque vous souhaitez tester le comportement d’une classe qui n’a pas de dépendances. Les stubs sont utilisés lorsque vous souhaitez tester le comportement d’une classe qui a des dépendances et que vous ne souhaitez pas modifier le comportement. Les simulacres sont utilisés lorsque vous souhaitez tester le comportement d’une classe qui a des dépendances et potentiellement modifier les comportements.

Pour plus d’informations sur les moqueries et les différences entre les stubs, les faux et les mocks, lisez le Contrefaçons, talons et maquettes article de blog.

Commencer à se moquer

Tout d’abord, vous aurez besoin d’un cadre de simulation pour commencer, car vous ne souhaitez pas gérer le cycle de vie de tous les objets factices. Quelque chose comme Telerik JustMock ou leur version gratuite JustMock Lite.

JustMock : l'outil de simulation le plus rapide et le plus flexible pour créer des tests unitaires

Un framework mocking est ce que vous utilisez pour créer les objets qui « fait semblant » être le ou les objets que vous testez.

Organiser, agir, affirmer

Maintenant que vous disposez d’un cadre de simulation, commençons par les principales parties du processus de test unitaire : organiser, agir, affirmer. Arrange, Act, Assert (ou AAA) est un terme courant utilisé pour décrire le processus de configuration de l’environnement de test, d’exécution du test et de vérification des résultats. C’est une bonne pratique dans les tests unitaires. Fondamentalement, chacun de vos tests unitaires devrait comporter ces trois parties :

  • Organiser: Configurez le test.
  • Loi: Exécutez le test.
  • Affirmer: Vérifiez les résultats.

Lorsque j’écris des tests, dans ce cas en utilisant xUnit, je commence généralement par ce modèle « stub »:

[Fact]
public void GetContact_WithAnInvalidId_ShouldReturnNull()
{
    
 
    
 
    
}

Le nom de la méthode suit un format cohérent :

  • [Fact]: Ceci est un test de fait.
  • public void: Il s’agit d’une méthode publique.
  • GetContact: C’est la méthode que vous testez.
  • _WithAnInvalidId_: Quelles que soient les variables que vous utilisez, dans cet exemple, un ID invalide.
  • ShouldReturnNull: Le résultat attendu.

Bien que cette convention ne soit pas obligatoire, j’ai tendance à l’utiliser pour que lorsque je regarde les résultats, ou qu’un autre ingénieur regarde le code, il puisse voir l’intention du test.

Tous les tests unitaires réussissent

Trucs moqueurs

Il existe de nombreux types de choses à simuler, comme les services, les bases de données, les files d’attente et d’autres types de dépendances. Pour cet exemple d’introduction, je vais montrer différentes façons de tester une classe qui nécessite une dépendance d’une base de données. je vais utiliser xUnit et Telerik JustMock pour ces exemples.

Le projet utilisé dans cet exemple se trouve ici. Ce projet est un projet C # qui a été construit sur mon Tic flux. L’application fournit un moyen de gérer une liste de contacts. Il utilise une variété de technologies, notamment:

  • API Web
  • serveur SQL
  • ASPIC. NET Core
  • ASPIC. NET Core MVC
  • Stockage Azure
  • Fonctions Azure

Avec toutes ces dépendances, j’avais besoin d’un moyen de valider que ces dépendances fonctionnent comme prévu. Et avant que vous ne demandiez, non, je ne teste pas les fonctionnalités de SQL Server ou Azure Storage ou Azure Functions. Je teste seulement l’interaction avec ce service. C’est là que la moquerie entre en jeu. Pour le reste de cet article, je vais me concentrer sur le test du ContactManager classe et se moquer de la ContactRepository.

Avant de commencer, examinons ContactManager classer. La ContactManager met en œuvre le IContactManager interface. C’est ce que nous testons.

public interface IContactManager
{
    Contact GetContact(int contactId);
    List<Contact> GetContacts();
    List<Contact> GetContacts(string firstName, string lastName);
    Contact SaveContact(Contact contact);
    bool DeleteContact(int contactId);
    bool DeleteContact(Contact contact);
    List<Phone> GetContactPhones(int contactId);
    Phone GetContactPhone(int contactId, int phoneId);
    List<Address> GetContactAddresses(int contactId);
    Address GetContactAddress(int contactId, int addressId);
    
}

Le code complet de cette classe peut être trouvé ici.

On va se moquer du ContactRepository objet qui implémente IContactRepository interface.

public interface IContactRepository
{
    Contact GetContact(int contactId);
    List<Contact> GetContacts();
    List<Contact> GetContacts(string firstName, string lastName);
    Contact SaveContact(Contact contact);
    bool DeleteContact(int contactId);
    bool DeleteContact(Contact contact);
    List<Phone> GetContactPhones(int contactId);
    Phone GetContactPhone(int contactId, int phoneId);
    List<Address> GetContactAddresses(int contactId);
    Address GetContactAddress(int contactId, int addressId);
    
}

Bien que les objets dont on se moque n’aient pas besoin d’être des interfaces, cela aide certainement. La IContactManager interface est un contrat qui définit les méthodes qui interagissent avec un Contactez. La ContactManager la classe implémente le IContactManager interface, dans ce cas. Cependant, une chose à noter est que le ContactManager nécessite un IContactRepository dépendance, ce dont nous allons nous moquer.

La IContactRepository interface définit le contrat avec la base de données, que nous ne voulons pas tester. C’est là qu’intervient la moquerie. Nous voulons pouvoir tester que la logique dans le ContactManager la classe fonctionne comme prévu sans aller et venir avec la base de données. Cela nous permet de tester des choses comme la validation ou les objets lors de la sauvegarde, en renvoyant les exceptions correctes lorsque les choses tournent mal, etc.

Notre première simulation

Commençons par le test le plus courant. Validons qu’un appel à GetContacts renvoie une liste de contacts. Nous allons commencer par le test le plus simple, puis passer à des tests plus complexes.

La signature de GetContacts est:

Task<List<Contact>> GetContacts();

Si nous commençons avec notre modèle ci-dessus, nous devrions supprimer un test qui ressemble à ceci :

public void GetContacts_ShouldReturnLists()
{
    
 
    
 
    
}

Organiser : le test

Maintenant, regardons le organiser partie. Pour la partie arrangement, nous devons configurer les maquettes afin que le cadre de simulation sache quoi imiter ou se moquer. Voici la partie organiser pour le GetContacts_ShouldReturnLists méthode:

var mockContactRepository = Mock.Create<IContactRepository>();
Mock.Arrange(() => mockContactRepository.GetContacts())
  .Returns(new List<Contact>
  {
      new Contact { ContactId = 1 }, new Contact { ContactId = 2 }
  });
var contactManager = new ContactManager(mockContactRepository);

Sur la ligne 1, nous créons une variable, mockContactRepositoryc’est la simulation de la IContactRepository interface. Ligne 2, nous créons une maquette du GetContacts méthode. Lignes 3 à 6, nous créons une liste de contacts et disons au cadre fictif de renvoyer cet objet lorsqu’un appel est effectué à GetObjects. Enfin, à la ligne 7, nous créons un nouveau ContactManager objecter et passer dans la simulation IContactRepository objet.

Agir : Exécuter sur le test

Dans ce cas, le loi est banal. Nous appelons simplement le GetContacts méthode sur la ContactManager objet.

var contacts = contactManager.GetContacts();

Cela devrait renvoyer une liste de contacts avec deux contacts avec les identifiants 1 et 2.

Assertion : vérifier les résultats

Validons que la liste de contacts comporte deux contacts.

Assert.NotNull(contacts);
Assert.Equal(2, contacts.Count);

La ligne 1 vérifie que la liste des contacts n’est pas nulle. La ligne 2 vérifie que la liste de contacts contient deux contacts.

L’épreuve complète

[Fact]
public void GetContacts_ShouldReturnLists()
{
    
    var mockContactRepository = Mock.Create<IContactRepository>();
    Mock.Arrange(() => mockContactRepository.GetContacts())
      .Returns(new List<Contact>
      {
        new Contact { ContactId = 1 }, new Contact { ContactId = 2 }
      });
 
    var contactManager = new ContactManager(mockContactRepository);
 
    
    var contacts = contactManager.GetContacts();
 
    
    Assert.NotNull(contacts);
    Assert.Equal(2, contacts.Count);
}

Se moquer des gammes

Il existe une méthode dans le ContactManager appelé GetContact qui nécessite un entier comme paramètre. Dans notre cas métier, l’identifiant d’un contact est un nombre positif (entier). Alors mettons en place un test qui s’assure qu’un appel est reçu GetContact avec un nombre négatif renvoie null et un appel pour obtenir GetContact avec un nombre positif renvoie un contact.

Pour cela, nous allons utiliser une fonctionnalité appelée matchers. Les matchers vous permettent d’ignorer le passage des valeurs réelles en tant qu’arguments utilisés dans les simulations. Au lieu de cela, ils vous donnent la possibilité de ne transmettre qu’une expression qui satisfait le type d’argument ou la plage de valeurs attendue. Cela signifie que nous n’avons pas à écrire un test pour chaque valeur possible. Nous pouvons simplement écrire un test pour la plage de valeurs. Nous allons utiliser le InRange matcher pour nos deux tests.

Pour le test, GetContact_WithAnInvalidId_ShouldReturnNull où l’on s’attend à un null retour, nous organiserions le test comme ceci:

Mock.Arrange(() => 
  mockContactRepository.GetContact(Arg.IsInRange(int.MinValue, 0, RangeKind.Inclusive)))
    .Returns<Contact>(null);

Dans cet arrangement, nous disons que lorsqu’un appel à GetContact est faite avec un argument qui est de l’ordre de int.MinValue à 0 inclus, il faut revenir null.

Notre acte et notre affirmation ressemblent à :


var contact = contactManager.GetContact(-1); 
 

Assert.Null(contact);

Pour le test, GetContact_WithAnValidId_ShouldReturnContactnous organiserions le test comme ceci :

Mock.Arrange(() =>
  mockContactRepository.GetContact(Arg.IsInRange(1, int.MaxValue, RangeKind.Inclusive)))
    .Returns(
    (int contactId) => new Contact
    {
        ContactId = contactId
    });
 
var contactManager = new ContactManager(mockContactRepository);
const int requestedContactId = 1;

Celui-ci a demandé un peu plus de travail car nous devions spécifier un objet à renvoyer, lignes 3 à 6, et une valeur pour l’identifiant du contact, ligne 9, à valider dans notre test.

Notre acte et notre affirmation ressemblent à :



var contact = contactManager.GetContact(requestedContactId);
 

Assert.NotNull(contact);
Assert.Equal(requestedContactId, contact.ContactId);

Se moquer des exceptions

La GetContacts La méthode a une surcharge qui attend deux paramètres de chaîne, un pour le prénom et l’autre pour le nom. La méthode exige également que le prénom et le nom ne soient pas null ou vide. Si c’est le cas, il devrait jeter un ArgumentNullException. Créons un test qui valide qu’un appel à GetContacts avec un prénom et un nom vides lève l’exception.

Organisons le test comme ceci :


var mockContactRepository = Mock.Create<IContactRepository>();
Mock.Arrange(() =>
    mockContactRepository.GetContacts(null, Arg.IsAny<string>()));
 
var contactManager = new ContactManager(mockContactRepository);

Nous passons ici un null pour le FirstName paramètre et en utilisant le Arg.IsAny<string> matcher pour le LastName paramètre qui correspondra à n’importe quelle chaîne.

Notre acte, qui est aussi une assertion, ressemble à ceci :


ArgumentNullException ex =
  Assert.Throws<ArgumentNullException>(() => contactManager.GetContacts(null, "Guadagno"));

Ici, nous créons une variable ex qui est de type ArgumentNullException et puis nous affirmons que le GetContacts méthode lance un ArgumentNullException lorsqu’il est appelé avec le FirstName paramètre réglé sur null et le LastName paramètre réglé sur Guadagno.

Ensuite, dans l’assertion, nous vérifions que le message d’exception est correct.


Assert.Equal("firstName", ex.ParamName);
Assert.Equal("FirstName is a required field (Parameter 'firstName')", ex.Message);

Notez que JustMock prend en charge une autre manière de tester qu’une exception est levée. On peut utiliser Mock.Arrange lors de la configuration du test pour lever l’exception. Nous pouvons utiliser le Throws<T> matcher pour affirmer qu’une exception d’un type spécifique est levée.

Mock.Arrange(() => contactManager.GetContacts(null, "Guadagno"))
  .Throws<ArgumentNullException>("FirstName is a required field (Parameter 'firstName')"); 

Tests de contacts complets

Le code complet pour le ContactManager la classe peut être trouvée ici.

Le code complet pour le ContactManagerTest la classe peut être trouvée ici.

Emballer

Cela ne fait qu’effleurer la surface de la moquerie. Il existe de nombreuses autres façons de se moquer en utilisant un cadre de simulation comme JustMock. Peut-être que nous couvrirons plus dans un futur post.

JustMock est disponible pour un essai gratuit afin que vous puissiez le voir en action. Il est également inclus dans le cadre du robuste Telerik DevCraft paquet.




Source link