Fichiers uniques ou multiples pour tester une seule classe?


20

En recherchant les meilleures pratiques de tests unitaires pour aider à élaborer des lignes directrices pour mon organisation, je me suis demandé s'il était préférable ou utile de séparer les montages de test (classes de test) ou de conserver tous les tests d'une seule classe dans un seul fichier.

Fwiw, je me réfère aux "tests unitaires" au sens pur, ce sont des tests en boîte blanche ciblant une seule classe, une assertion par test, toutes les dépendances moquées, etc.

Un exemple de scénario est une classe (appelez-la Document) qui a deux méthodes: CheckIn et CheckOut. Chaque méthode implémente différentes règles, etc. qui contrôlent leur comportement. En suivant la règle d'une assertion par test, j'aurai plusieurs tests pour chaque méthode. Je peux soit placer tous les tests dans une seule DocumentTestsclasse avec des noms comme CheckInShouldThrowExceptionWhenUserIsUnauthorizedet CheckOutShouldThrowExceptionWhenUserIsUnauthorized.

Ou, je pourrais avoir deux classes de test distinctes: CheckInShouldet CheckOutShould. Dans ce cas, mes noms de test seraient raccourcis, mais ils seraient organisés de sorte que tous les tests pour un comportement (méthode) spécifique soient ensemble.

Je suis sûr qu'il y a des avantages et des inconvénients à l'une ou l'autre approche et je me demande si quelqu'un a opté pour plusieurs fichiers et, si oui, pourquoi? Ou, si vous avez opté pour l'approche à fichier unique, pourquoi pensez-vous que c'est mieux?


2
Les noms de vos méthodes de test sont trop longs. Simplifiez-les, même si cela signifie qu'une méthode de test contiendrait plusieurs assertions.
Bernard

12
@Bernard: il est considéré comme une mauvaise pratique d'avoir plusieurs assertions par méthode, les noms de méthode de test longs ne sont cependant PAS considérés comme une mauvaise pratique. Nous documentons généralement ce que la méthode teste dans le nom lui-même. Par exemple constructor_nullSomeList (), setUserName_UserNameIsNotValid () etc ...
c_maker

4
@Bernard: j'utilise aussi des noms de test longs (et seulement là!). Je les aime, car il est si clair ce qui ne fonctionnait pas (si vos noms sont de bons choix;)).
Sebastian Bauer

Les assertions multiples ne sont pas une mauvaise pratique, si elles testent toutes le même résultat. Par exemple. vous n'auriez pas testResponseContainsSuccessTrue(), testResponseContainsMyData()et testResponseStatusCodeIsOk(). Vous les avez en un seul testResponse()qui a trois affirme: assertEquals(200, response.status), assertEquals({"data": "mydata"}, response.data)etassertEquals(true, response.success)
Juha Untinen

Réponses:


18

C'est rare, mais parfois il est logique d'avoir plusieurs classes de test pour une classe donnée à tester. En général, je le fais lorsque différentes configurations sont requises et partagées sur un sous-ensemble de tests.


3
Bon exemple de la raison pour laquelle cette approche m'a été présentée en premier lieu. Par exemple, lorsque je veux tester la méthode CheckIn, je veux toujours que l'objet sous test soit configuré dans un sens mais quand je teste la méthode CheckOut, j'ai besoin d'une configuration différente. Bon point.
SonOfPirate

14

Je ne vois pas vraiment de raison impérieuse pour laquelle vous diviseriez un test pour une seule classe en plusieurs classes de test. Étant donné que l'idée de conduite doit être de maintenir la cohésion au niveau de la classe, vous devez également vous efforcer de le faire au niveau du test. Juste quelques raisons aléatoires:

  1. Pas besoin de dupliquer (et de maintenir plusieurs versions de) le code de configuration
  2. Plus facile d'exécuter tous les tests d'une classe à partir d'un IDE s'ils sont groupés dans une classe de test
  3. Dépannage plus simple grâce au mappage un à un entre les classes de test et les classes.

Bons points. Si la classe sous test est un horrible kududge, je n'exclurais pas plusieurs classes de test, où certaines d'entre elles contiennent une logique d'aide. Je n'aime tout simplement pas les règles très rigides. A part ça, de bons points.
Job

1
J'éliminerais le code d'installation en double en ayant une classe de base commune pour les appareils de test fonctionnant avec une seule classe. Je ne suis pas sûr d'être d'accord avec les deux autres points.
SonOfPirate

Dans JUnit par exemple, vous voudrez peut-être tester des choses en utilisant différents Runners, ce qui conduit à la nécessité de différentes classes.
Joachim Nilsson

Comme j'écris une classe avec un grand nombre de méthodes avec des mathématiques complexes dans chacune d'elles, je trouve que j'ai 85 tests et que je n'ai jusqu'à présent écrit des tests que pour 24% des méthodes. Je me retrouve ici parce que je me demandais si c'était fou de bifurquer ce nombre énorme de tests dans des catégories distinctes pour pouvoir exécuter des sous-ensembles.
Mooing Duck

7

Si vous êtes obligé de fractionner les tests unitaires d'une classe sur plusieurs fichiers, cela peut indiquer que la classe elle-même est mal conçue. Je ne peux penser à aucun scénario où il serait plus avantageux de diviser les tests unitaires pour une classe qui adhère raisonnablement au principe de responsabilité unique et aux autres meilleures pratiques de programmation.

De plus, avoir des noms de méthode plus longs dans les tests unitaires est acceptable, mais si cela vous dérange, vous pouvez toujours repenser votre convention de dénomination des tests unitaires pour raccourcir les noms.


Hélas, dans le monde réel, toutes les classes n'adhèrent pas à SRP et al ... et cela devient un problème lorsque vous testez un code hérité.
Péter Török

3
Je ne sais pas comment SRP se rapporte ici. J'ai plusieurs méthodes dans ma classe et j'ai besoin d'écrire des tests unitaires pour toutes les différentes exigences qui déterminent leur comportement. Est-il préférable d'avoir une classe de test avec 50 méthodes de test ou 5 classes de test avec 10 tests chacune qui se rapportent à une méthode spécifique dans la classe testée?
SonOfPirate

2
@SonOfPirate: Si vous avez besoin d'autant de tests, cela peut signifier que vos méthodes en font trop. Le SRP est donc très pertinent ici. Si vous appliquez SRP aux classes ainsi qu'aux méthodes, vous aurez beaucoup de petites classes et méthodes faciles à tester et vous n'aurez pas besoin d'autant de méthodes de test. (Mais je suis d'accord avec Péter Török que lorsque vous testez du code hérité, les bonnes pratiques sont à la porte et vous n'avez qu'à faire de votre mieux avec ce qui vous est donné ...)
c_maker

Le meilleur exemple que je peux donner est la méthode CheckIn où nous avons des règles métier qui déterminent qui est autorisé à effectuer l'action (autorisation), si l'objet est dans l'état approprié pour être archivé, comme s'il est extrait et si des modifications ont été faites. Nous aurons des tests unitaires qui vérifieront que les règles d'autorisation sont appliquées ainsi que des tests pour vérifier que la méthode se comporte correctement si CheckIn est appelé lorsque l'objet n'est pas extrait, non modifié, etc. C'est pourquoi nous nous retrouvons avec beaucoup tests. Suggérez-vous que c'est faux?
SonOfPirate

Je ne pense pas qu'il y ait de réponse dure et rapide, bonne ou mauvaise à ce sujet. Si ce que vous faites fonctionne pour vous, c'est parfait. Ce n'est que mon point de vue sur une ligne directrice générale.
FishBasketGordo

2

Un de mes arguments contre la séparation des tests en plusieurs classes est qu'il devient plus difficile pour les autres développeurs de l'équipe (en particulier ceux qui ne sont pas aussi avertis des tests) de localiser les tests existants ( Gee, je me demande s'il existe déjà un test pour cela ? Je me demande où ce serait? ) et aussi où mettre de nouveaux tests ( je vais écrire un test pour cette méthode dans cette classe, mais je ne sais pas trop si je devrais les mettre dans un nouveau fichier, ou un fichier existant) un? )

Dans le cas où différents tests nécessitent des configurations radicalement différentes, j'ai vu certains praticiens du TDD mettre le "fixture" ou le "setup" dans différentes classes / fichiers, mais pas les tests eux-mêmes.


1

J'aimerais pouvoir me souvenir / trouver le lien où la technique que j'ai choisie d'adopter a été démontrée pour la première fois. Essentiellement, je crée une classe abstraite unique pour chaque classe sous test qui contient des montages de test imbriqués (classes) pour chaque membre testé. Cela fournit la séparation initialement souhaitée mais conserve tous les tests dans le même fichier pour chaque emplacement. De plus, cela génère un nom de classe qui permet un regroupement et un tri faciles dans le lanceur de test.

Voici un exemple de la façon dont cette approche est appliquée à mon scénario d'origine:

public abstract class DocumentTests : TestBase
{
    [TestClass()]
    public sealed class CheckInShould : DocumentTests
    {
        [TestMethod()]
        public void ThrowExceptionWhenUserIsNotAuthorized()
        {
        }
    }

    [TestClass()]
    public sealed class CheckOutShould : DocumentTests
    {
        [TestMethod()]
        public void ThrowExceptionWhenUserIsNotAuthorized()
        {
        }
    }
}

Il en résulte que les tests suivants apparaissent dans la liste des tests:

DocumentTests+CheckInShould.ThrowExceptionWhenUserIsNotAuthorized
DocumentTests+CheckOutShould.ThrowExceptionWhenUserIsNotAuthorized

Au fur et à mesure que de nouveaux tests sont ajoutés, ils sont facilement regroupés et triés par nom de classe, ce qui conserve également tous les tests de la classe sous test répertoriés.

Un autre avantage de cette approche que j'ai appris à exploiter est la nature orientée objet de cette structure signifie que je peux définir du code à l'intérieur des méthodes de démarrage et de nettoyage de la classe de base DocumentTests qui seront partagées par toutes les classes imbriquées ainsi qu'à l'intérieur de chacune classe imbriquée pour les tests qu'elle contient.


1

Essayez de penser l'inverse pendant une minute.

Pourquoi auriez-vous une seule classe de test par classe? Faites-vous un test de classe ou un test unitaire? Dépendez-vous même des cours ?

Un test unitaire est censé tester un comportement spécifique, dans un contexte spécifique. Le fait que votre classe ait déjà un CheckInmoyen, il devrait y avoir un comportement qui en a besoin en premier lieu.

Que pensez-vous de ce pseudo-code:

// check_in_test.file

class CheckInTest extends TestCase {
        /** @test */
        public function unauthorized_users_cannot_check_in() {
                $this->expectException();

                $document = new Document($unauthorizedUser);

                $document->checkIn();
        }
}

Maintenant, vous ne testez pas directement la checkInméthode. Vous testez plutôt un comportement (qui consiste à s'enregistrer heureusement;)), et si vous devez le refactoriser Documentet le diviser en différentes classes, ou y fusionner une autre classe, vos fichiers de test sont toujours cohérents, ils ont toujours du sens puisque vous ne changez jamais la logique lors de la refactorisation, seulement la structure.

Un fichier de test pour une classe, il est tout simplement plus difficile de refactoriser chaque fois que vous en avez besoin, et les tests ont également moins de sens en termes de domaine / logique du code lui-même.


0

En général, je recommanderais de considérer les tests comme testant un comportement de la classe plutôt qu'une méthode. C'est-à-dire que certains de vos tests peuvent avoir besoin d'appeler les deux méthodes sur la classe afin de tester un comportement attendu.

Habituellement, je commence avec une classe de tests unitaires par classe de production, mais je peux éventuellement diviser cette classe de tests unitaires en plusieurs classes de tests en fonction du comportement qu'ils testent. En d'autres termes, je recommanderais de ne pas diviser la classe de test en CheckInShould et CheckOutShould, mais plutôt de diviser par le comportement de l'unité testée.


0

1. Réfléchissez à ce qui risque de mal tourner

Pendant le développement initial, vous savez assez bien ce que vous faites et les deux solutions fonctionneront probablement bien.

Cela devient plus intéressant lorsqu'un test échoue après un changement beaucoup plus tard. Il y a deux possibilités:

  1. Vous avez sérieusement cassé CheckIn(ou CheckOut). Encore une fois, le fichier unique ainsi que la solution à deux fichiers sont OK dans ce cas.
  2. Vous avez modifié les deux CheckInet CheckOut(et peut-être leurs tests) d'une manière qui a du sens pour chacun d'eux, mais pas pour les deux ensemble. Vous avez rompu la cohérence de la paire. Dans ce cas, la division des tests en deux fichiers rendra la compréhension du problème plus difficile.

2. Réfléchissez aux tests utilisés

Les tests ont deux objectifs principaux:

  1. Vérifiez automatiquement que le programme fonctionne toujours correctement. Que les tests soient dans un fichier ou plusieurs n'a pas d'importance à cet effet.
  2. Aidez un lecteur humain à comprendre le programme. Cela sera beaucoup plus facile si les tests de fonctionnalités étroitement liées sont maintenus ensemble, car voir leur cohérence est un moyen rapide de compréhension.

Donc?

Ces deux perspectives suggèrent que le maintien des tests ensemble peut aider, mais ne fera pas de mal.

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.