À quoi servent les objets factices?


167

Je suis nouveau dans les tests unitaires et j'entends continuellement les mots «objets factices» jetés partout. En termes simples, quelqu'un peut-il expliquer ce que sont les objets simulés et à quoi ils sont généralement utilisés lors de l'écriture de tests unitaires?


12
Ils sont un outil de sur-ingénierie massive avec une flexibilité dont vous n'avez pas besoin pour le problème à résoudre.
dsimcha le

2
duplicata possible de Qu'est-ce que la moquerie?
nawfal

Réponses:


361

Puisque vous dites que vous êtes nouveau dans les tests unitaires et que vous avez demandé des objets fictifs en "termes simples", je vais essayer l'exemple d'un profane.

Test unitaire

Imaginez des tests unitaires pour ce système:

cook <- waiter <- customer

Il est généralement facile d'envisager de tester un composant de bas niveau comme cook:

cook <- test driver

Le testeur commande simplement différents plats et vérifie que le cuisinier renvoie le plat correct pour chaque commande.

Il est plus difficile de tester un composant intermédiaire, comme le serveur, qui utilise le comportement d'autres composants. Un testeur naïf pourrait tester le composant serveur de la même manière que nous avons testé le composant cook:

cook <- waiter <- test driver

Le pilote d'essai commandait différents plats et s'assurait que le serveur renvoie le bon plat. Malheureusement, cela signifie que ce test du composant serveur peut dépendre du comportement correct du composant de cuisson. Cette dépendance est encore pire si le composant cuisinier a des caractéristiques peu conviviales pour les tests, comme un comportement non déterministe (le menu comprend la surprise du chef comme plat), beaucoup de dépendances (le cuisinier ne cuisinera pas sans tout son personnel), ou beaucoup de ressources (certains plats nécessitent des ingrédients coûteux ou prennent une heure à cuire).

Puisqu'il s'agit d'un test de serveur, idéalement, nous voulons tester uniquement le serveur, pas le cuisinier. Plus précisément, nous voulons nous assurer que le serveur transmet correctement la commande du client au cuisinier et livre correctement la nourriture du cuisinier au client.

Le test unitaire signifie tester les unités indépendamment, donc une meilleure approche serait d'isoler le composant testé (le serveur) en utilisant ce que Fowler appelle des doubles de test (mannequins, stubs, faux, simulacres) .

    -----------------------
   |                       |
   v                       |
test cook <- waiter <- test driver

Ici, le cuisinier de test est "de mèche" avec le pilote de test. Idéalement, le système testé est conçu de manière à ce que le cuisinier test puisse être facilement remplacé ( injecté ) pour travailler avec le serveur sans changer le code de production (par exemple sans changer le code serveur).

Objets simulés

Maintenant, le test cook (test double) pourrait être implémenté de différentes manières:

  • un faux cuisinier - quelqu'un se faisant passer pour un cuisinier en utilisant des plats surgelés et un micro-ondes,
  • un chef cuisinier - un vendeur de hot-dogs qui vous donne toujours des hot-dogs, peu importe ce que vous commandez, ou
  • un cuisinier simulé - un flic infiltré suivant un scénario se faisant passer pour un cuisinier dans une opération de piqûre.

Voir l'article de Fowler pour plus de détails sur les faux vs les talons vs les mocks vs les mannequins , mais pour l'instant, concentrons-nous sur un simulacre de cuisinier.

    -----------------------
   |                       |
   v                       |
mock cook <- waiter <- test driver

Une grande partie des tests unitaires du composant serveur se concentre sur la manière dont le serveur interagit avec le composant cuisinier. Une approche basée sur des simulations se concentre sur la spécification complète de l'interaction correcte et sur la détection des dysfonctionnements.

L'objet fictif sait à l'avance ce qui est censé se passer pendant le test (par exemple, lequel de ses appels de méthodes sera invoqué, etc.) et l'objet fictif sait comment il est censé réagir (par exemple, quelle valeur de retour fournir). Le simulacre indiquera si ce qui se passe réellement diffère de ce qui est censé se produire. Un objet fictif personnalisé pourrait être créé à partir de zéro pour chaque cas de test afin d'exécuter le comportement attendu pour ce cas de test, mais un cadre de simulation s'efforce de permettre à une telle spécification de comportement d'être clairement et facilement indiquée directement dans le cas de test.

La conversation autour d'un test basé sur une simulation pourrait ressembler à ceci:

pilote d'essai pour simuler un cuisinier : attendez-vous à une commande de hot-dog et donnez-lui ce faux hot-dog en réponse

pilote d'essai (se faisant passer pour un client) au serveur : je voudrais un hot-dog s'il vous plaît le
serveur se moque du cuisinier : 1 hot-dog s'il vous plaît
simulez le cuisinier au serveur : commandez: 1 hot-dog prêt (donne un hot-dog factice au serveur)
serveur pour tester le pilote : voici votre hot-dog (donne un hot-dog factice au pilote d'essai)

pilote d'essai : TEST RÉUSSI!

Mais comme notre serveur est nouveau, voici ce qui pourrait arriver:

pilote d'essai pour simuler un cuisinier : attendez-vous à une commande de hot-dog et donnez-lui ce faux hot-dog en réponse

pilote d'essai (se faisant passer pour un client) au serveur : je voudrais un hot-dog s'il vous plaît
serveur pour simuler le cuisinier : 1 hamburger s'il vous plaît
mock cook arrête le test: on m'a dit d'attendre une commande de hot-dog!

le pilote de test note le problème: TEST ECHEC! - le serveur a changé la commande

ou

pilote d'essai pour simuler un cuisinier : attendez-vous à une commande de hot-dog et donnez-lui ce faux hot-dog en réponse

pilote d'essai (se faisant passer pour un client) au serveur : je voudrais un hot-dog s'il vous plaît le
serveur se moque du cuisinier : 1 hot-dog s'il vous plaît
simulez le cuisinier au serveur : commandez: 1 hot-dog prêt (donne un hot-dog factice au serveur)
serveur pour tester le pilote : voici vos frites (donne des frites d'un autre ordre pour tester le pilote)

pilote d'essai note les frites inattendues: TEST ECHEC! le serveur a rendu le mauvais plat

Il peut être difficile de voir clairement la différence entre les objets fictifs et les stubs sans un exemple contrastant basé sur les stub pour aller avec cela, mais cette réponse est déjà bien trop longue :-)

Notez également qu'il s'agit d'un exemple assez simpliste et que les frameworks moqueurs permettent des spécifications assez sophistiquées du comportement attendu des composants pour prendre en charge des tests complets. Il y a beaucoup de matériel sur les objets simulés et les frameworks moqueurs pour plus d'informations.


12
C'est une excellente explication, mais ne testez-vous pas dans une certaine mesure l'implémentation du serveur? Dans votre cas, c'est probablement normal parce que vous vérifiez qu'il utilise la bonne API, mais que se passe-t-il s'il existe différentes façons de le faire et que le serveur peut choisir l'une ou l'autre? Je pensais que le but des tests unitaires était de tester l'API, et non l'implémentation. (C'est une question que je me pose toujours lorsque je lis sur les moqueries.)
davidtbernal

8
Merci. Je ne peux pas dire si nous testons "l'implémentation" sans voir (ou définir) les spécifications du serveur. Vous pouvez supposer que le serveur est autorisé à cuisiner le plat lui-même ou à passer la commande dans la rue, mais je suppose que les spécifications du serveur incluent l'utilisation du chef prévu - après tout, le chef de production est un chef gastronomique coûteux et nous ' Je préfère que notre serveur l'utilise. Sans cette spécification, je suppose que je "devrais conclure que vous avez raison - le serveur peut remplir la commande comme il le souhaite pour être" correct ". OTOH, sans spécification, le test n'a pas de sens. [Suite ...]
Bert F

8
JAMAIS, vous faites un excellent point qui mène au fantastique sujet des tests unitaires boîte blanche vs boîte noire. Je ne pense pas qu'il y ait un consensus de l'industrie selon lequel les tests unitaires doivent être une boîte noire au lieu d'une boîte blanche («tester l'API, pas l'implémentation»). Je pense que le meilleur test unitaire doit probablement être une combinaison des deux pour équilibrer la fragilité du test avec la couverture du code et l'exhaustivité du cas de test.
Bert F

1
Cette réponse n'est selon moi pas assez technique. Je veux savoir pourquoi je devrais utiliser un objet simulé lorsque je peux utiliser des objets réels.
Niklas R.

1
Excellente explication !! Merci!! @BertF
Bharath Murali

28

Un objet simulé est un objet qui se substitue à un objet réel. Dans la programmation orientée objet, les objets fictifs sont des objets simulés qui imitent le comportement d'objets réels de manière contrôlée.

Un programmeur informatique crée généralement un objet simulé pour tester le comportement d'un autre objet, à peu près de la même manière qu'un concepteur de voiture utilise un mannequin de test de collision pour simuler le comportement dynamique d'un humain lors d'un impact sur un véhicule.

http://en.wikipedia.org/wiki/Mock_object

Les objets fictifs vous permettent de configurer des scénarios de test sans faire appel à des ressources volumineuses et lourdes telles que des bases de données. Au lieu d'appeler une base de données à des fins de test, vous pouvez simuler votre base de données à l'aide d'un objet fictif dans vos tests unitaires. Cela vous libère du fardeau d'avoir à configurer et à démolir une vraie base de données, juste pour tester une seule méthode dans votre classe.

Le mot «Mock» est parfois utilisé à tort de manière interchangeable avec «Stub». Les différences entre les deux mots sont décrites ici. Essentiellement, une maquette est un objet stub qui comprend également les attentes (c'est-à-dire les «assertions») pour le comportement correct de l'objet / de la méthode testée.

Par exemple:

class OrderInteractionTester...
  public void testOrderSendsMailIfUnfilled() {
    Order order = new Order(TALISKER, 51);
    Mock warehouse = mock(Warehouse.class);
    Mock mailer = mock(MailService.class);
    order.setMailer((MailService) mailer.proxy());

    mailer.expects(once()).method("send");
    warehouse.expects(once()).method("hasInventory")
      .withAnyArguments()
      .will(returnValue(false));

    order.fill((Warehouse) warehouse.proxy());
  }
}

Notez que les objets warehouseet mailerfictifs sont programmés avec les résultats attendus.


2
La définition que vous avez donnée ne diffère pas du tout de celle d '«objet stub» et, en tant que telle, n'explique pas ce qu'est un objet factice.
Brent Arias

Une autre correction "le mot" Mock "est parfois utilisé à tort de manière interchangeable avec" stub "".
Brent Arias

@Myst: L'utilisation des deux mots n'est pas universelle; cela varie selon les auteurs. Fowler le dit, et l'article de Wikipedia le dit. Cependant, n'hésitez pas à modifier le changement et à supprimer votre vote défavorable. :)
Robert Harvey

1
Je suis d'accord avec Robert: l'utilisation du mot "mock" a tendance à varier dans l'industrie, mais il n'y a pas de définition définie selon mon expérience, sauf que ce n'est généralement PAS l'objet réellement testé, mais plutôt qu'il existe pour faciliter les tests lorsque l'utilisation du réel l'objet ou toutes ses parties seraient très gênants et peu importants.
mkelley33

15

Les objets simulés sont des objets simulés qui imitent le comportement des objets réels. En général, vous écrivez un objet fictif si:

  • L'objet réel est trop complexe pour l'incorporer dans un test unitaire (par exemple une communication réseau, vous pouvez avoir un objet fictif qui simule été l'autre pair)
  • Le résultat de votre objet est non déterministe
  • L'objet réel n'est pas encore disponible

12

Un objet Mock est une sorte de Test Double . Vous utilisez des mockobjects pour tester et vérifier le protocole / l'interaction de la classe testée avec d'autres classes.

En règle générale, vous allez en quelque sorte des attentes de «programme» ou «d'enregistrement»: les appels de méthode que vous attendez de votre classe pour un objet sous-jacent.

Disons par exemple que nous testons une méthode de service pour mettre à jour un champ dans un widget. Et que dans votre architecture il y a un WidgetDAO qui s'occupe de la base de données. Parler avec la base de données est lent et la configuration et le nettoyage par la suite sont compliqués, nous allons donc nous moquer du WidgetDao.

pensons à ce que le service doit faire: il doit récupérer un widget de la base de données, faire quelque chose avec et l'enregistrer à nouveau.

Donc, en pseudo-langage avec une pseudo-fausse bibliothèque, nous aurions quelque chose comme:

Widget sampleWidget = new Widget();
WidgetDao mock = createMock(WidgetDao.class);
WidgetService svc = new WidgetService(mock);

// record expected calls on the dao
expect(mock.getById(id)).andReturn(sampleWidget);   
expect(mock.save(sampleWidget);

// turn the dao in replay mode
replay(mock);

svc.updateWidgetPrice(id,newPrice);

verify(mock);    // verify the expected calls were made
assertEquals(newPrice,sampleWidget.getPrice());

De cette façon, nous pouvons facilement tester le développement de classes qui dépendent d'autres classes.


11

Je recommande vivement un excellent article de Martin Fowler expliquant ce que sont exactement les simulacres et en quoi ils diffèrent des talons.


10
Pas vraiment adapté aux débutants, n'est-ce pas?
Robert Harvey

@Robert Harvey: Peut-être, en tout cas c'est bon de voir que cela a été utile pour clarifier votre réponse :)
Adam Byrtek

Les articles de Martin Fowler sont écrits à la manière des RFC: secs et froids.
revo

9

Lors du test unitaire d'une partie d'un programme informatique, vous souhaitez idéalement tester uniquement le comportement de cette partie particulière.

Par exemple, regardez le pseudo-code ci-dessous à partir d'un morceau imaginaire d'un programme qui utilise un autre programme pour appeler print quelque chose:

If theUserIsFred then
    Call Printer(HelloFred)
Else
   Call Printer(YouAreNotFred)
End

Si vous testiez cela, vous voudriez principalement tester la partie qui regarde si l'utilisateur est Fred ou non. Vous ne voulez pas vraiment tester la Printerpartie des choses. Ce serait un autre test.

C'est là qu'interviennent les objets simulés. Ils prétendent être d'autres types de choses. Dans ce cas, vous utiliseriez une maquette Printerpour qu'elle agisse comme une vraie imprimante, mais ne ferait pas de choses gênantes comme l'impression.


Il existe plusieurs autres types d'objets factices que vous pouvez utiliser qui ne sont pas des simulacres. La principale chose qui rend Mocks Mocks est qu'ils peuvent être configurés avec des comportements et des attentes.

Les attentes permettent à votre Mock de générer une erreur lorsqu'il n'est pas utilisé correctement. Ainsi, dans l'exemple ci-dessus, vous pouvez souhaiter être sûr que l'imprimante est appelée avec HelloFred dans le cas de test «l'utilisateur est Fred». Si cela ne se produit pas, votre Mock peut vous en avertir.

Le comportement dans Mocks signifie que, par exemple, de votre code a fait quelque chose comme:

If Call Printer(HelloFred) Returned SaidHello Then
    Do Something
End

Vous voulez maintenant tester ce que fait votre code lorsque l'imprimante est appelée et renvoie SaidHello, afin que vous puissiez configurer le Mock pour renvoyer SaidHello lorsqu'il est appelé avec HelloFred.

Une bonne ressource à ce sujet est Martin Fowlers post Mocks Arn't Stubs


7

Les objets mock et stub sont une partie cruciale des tests unitaires. En fait, ils font beaucoup pour s'assurer que vous testez des unités plutôt que des groupes d'unités.

En un mot, vous utilisez des stubs pour briser la dépendance de SUT (System Under Test) sur d'autres objets et des simulations pour le faire et vérifier que SUT a appelé certaines méthodes / propriétés sur la dépendance. Cela revient aux principes fondamentaux des tests unitaires - que les tests doivent être facilement lisibles, rapides et ne nécessitant pas de configuration, ce qui pourrait impliquer l'utilisation de toutes les classes réelles.

En règle générale, vous pouvez avoir plus d'un stub dans votre test, mais vous ne devriez en avoir qu'un seul. En effet, le but de la simulation est de vérifier le comportement et votre test ne doit tester qu'une seule chose.

Scénario simple utilisant C # et Moq:

public interface IInput {
  object Read();
}
public interface IOutput {
  void Write(object data);
}

class SUT {
  IInput input;
  IOutput output;

  public SUT (IInput input, IOutput output) {
    this.input = input;
    this.output = output;
  }

  void ReadAndWrite() { 
    var data = input.Read();
    output.Write(data);
  }
}

[TestMethod]
public void ReadAndWriteShouldWriteSameObjectAsRead() {
  //we want to verify that SUT writes to the output interface
  //input is a stub, since we don't record any expectations
  Mock<IInput> input = new Mock<IInput>();
  //output is a mock, because we want to verify some behavior on it.
  Mock<IOutput> output = new Mock<IOutput>();

  var data = new object();
  input.Setup(i=>i.Read()).Returns(data);

  var sut = new SUT(input.Object, output.Object);
  //calling verify on a mock object makes the object a mock, with respect to method being verified.
  output.Verify(o=>o.Write(data));
}

Dans l'exemple ci-dessus, j'ai utilisé Moq pour démontrer les stubs et les simulations. Moq utilise la même classe pour les deux - Mock<T>ce qui le rend un peu déroutant. Quoi qu'il en soit, au moment de l'exécution, le test échouera s'il output.Writen'est pas appelé avec des données as parameter, alors que l'échec de l'appel input.Read()n'échouera pas.


4

Comme une autre réponse suggérée via un lien vers "Les mocks ne sont pas des stubs ", les mocks sont une forme de "test double" à utiliser à la place d'un objet réel. Ce qui les différencie des autres formes de doubles de test, tels que les objets stub, est que d'autres doubles de test offrent une vérification d'état (et éventuellement une simulation) tandis que les simulacres offrent une vérification du comportement (et éventuellement une simulation).

Avec un stub, vous pouvez appeler plusieurs méthodes sur le stub dans n'importe quel ordre (ou même de manière répétitive) et déterminer le succès si le stub a capturé une valeur ou un état que vous vouliez. En revanche, un objet fictif s'attend à ce que des fonctions très spécifiques soient appelées, dans un ordre spécifique, et même un certain nombre de fois. Le test avec un objet fictif sera considéré comme "échoué" simplement parce que les méthodes ont été invoquées dans une séquence ou un nombre différent - même si l'objet fictif avait l'état correct à la fin du test!

De cette manière, les objets fictifs sont souvent considérés comme plus étroitement associés au code SUT que les objets stub. Cela peut être une bonne ou une mauvaise chose, selon ce que vous essayez de vérifier.


3

Une partie de l'intérêt d'utiliser des objets fictifs est qu'ils n'ont pas à être vraiment implémentés selon les spécifications. Ils peuvent simplement donner des réponses factices. Par exemple, si vous devez implémenter les composants A et B, et que les deux "s'appellent" (interagissent avec), vous ne pouvez pas tester A tant que B n'est pas implémenté, et vice versa. Dans le développement piloté par les tests, c'est un problème. Donc, vous créez des objets simulés ("factices") pour A et B, qui sont très simples, mais ils donnent une sorte de réponse lorsqu'ils interagissent avec. De cette façon, vous pouvez implémenter et tester A à l'aide d'un objet fictif pour B.


1

Pour php et phpunit est bien expliqué dans la documentation phpunit. vois ici documentation phpunit

Dans un simple mot, l'objet moqueur est juste un objet factice de votre original et renvoie sa valeur de retour, cette valeur de retour peut être utilisée dans la classe de test


0

C'est l'une des principales perspectives des tests unitaires. oui, vous essayez de tester votre unité de code unique et vos résultats de test ne devraient pas être pertinents pour le comportement d'autres beans ou objets. vous devriez donc vous moquer d'eux en utilisant des objets Mock avec une réponse correspondante simplifiée.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.