L'utilisation de tests unitaires pour raconter une histoire est-elle une bonne idée?


13

J'ai donc un module d'authentification que j'ai écrit il y a quelque temps. Maintenant, je vois les erreurs de mon chemin et j'écris des tests unitaires pour cela. En écrivant des tests unitaires, j'ai du mal à trouver de bons noms et de bons domaines à tester. Par exemple, j'ai des choses comme

  • RequirementsLogin_should_redirect_when_not_logged_in
  • RequirementsLogin_should_pass_through_when_logged_in
  • Login_should_work_when_given_proper_credentials

Personnellement, je pense que c'est un peu moche, même si ça semble "convenable". J'ai également du mal à faire la différence entre les tests en les balayant simplement (je dois lire le nom de la méthode au moins deux fois pour savoir ce qui vient d'échouer)

Donc, j'ai pensé que peut-être au lieu d'écrire des tests qui testent purement la fonctionnalité, peut-être écrire un ensemble de tests qui couvrent des scénarios.

Par exemple, voici un talon de test que j'ai trouvé:

public class Authentication_Bill
{
    public void Bill_has_no_account() 
    { //assert username "bill" not in UserStore
    }
    public void Bill_attempts_to_post_comment_but_is_redirected_to_login()
    { //Calls RequiredLogin and should redirect to login page
    }
    public void Bill_creates_account()
    { //pretend the login page doubled as registration and he made an account. Add the account here
    }
    public void Bill_logs_in_with_new_account()
    { //Login("bill", "password"). Assert not redirected to login page
    }
    public void Bill_can_now_post_comment()
    { //Calls RequiredLogin, but should not kill request or redirect to login page
    }
}

Est-ce un motif entendu? J'ai vu des histoires d'acceptation et autres, mais c'est fondamentalement différent. La grande différence est que je propose des scénarios pour "forcer" les tests. Plutôt que d'essayer manuellement de trouver des interactions possibles que je devrai tester. De plus, je sais que cela encourage les tests unitaires qui ne testent pas exactement une méthode et une classe. Je pense que c'est OK cependant. De plus, je suis conscient que cela causera des problèmes pour au moins certains cadres de test, car ils supposent généralement que les tests sont indépendants les uns des autres et que l'ordre n'a pas d'importance (où il le serait dans ce cas).

Quoi qu'il en soit, est-ce un modèle conseillé du tout? Ou, serait-ce un ajustement parfait pour les tests d'intégration de mon API plutôt que comme des tests "unitaires"? C'est juste dans un projet personnel, donc je suis ouvert à des expériences qui peuvent ou non bien se passer.


4
Les lignes entre l' unité, l' intégration et les tests fonctionnels sont flous, si je dois choisir un nom pour votre talon de test, il serait fonctionnel.
yannis

Je pense que c'est une question de goût. Personnellement, j'utilise le nom de tout ce que je teste avec en _testannexe et utilise des commentaires pour noter les résultats que j'attends. S'il s'agit d'un projet personnel, trouvez un style avec lequel vous vous sentez à l'aise et respectez-le.
M. Lister

1
J'ai écrit une réponse avec des détails sur une manière plus traditionnelle d'écrire des tests unitaires en utilisant le modèle Arrange / Act / Assert, mais un ami a eu beaucoup de succès en utilisant github.com/cucumber/cucumber/wiki/Gherkin , qui est utilisé pour les spécifications et afaik peut générer des tests de concombre.
StuperUser

Bien que je n'utiliserais pas la méthode que vous avez montrée avec nunit ou similaire, nspec prend en charge la création de contexte et de tests dans une nature plus axée sur les histoires: nspec.org
Mike

1
changer "Bill" en "User" et vous avez terminé
Steven A. Lowe

Réponses:


15

Oui, c'est une bonne idée de donner à vos tests les noms des exemples de scénarios que vous testez. Et utiliser votre outil de test unitaire pour plus que des tests unitaires peut-être bien aussi, beaucoup de gens le font avec succès (moi aussi).

Mais non, ce n'est certainement pas une bonne idée d'écrire vos tests d'une manière où l'ordre d'exécution des tests est important. Par exemple, NUnit permet à l'utilisateur de sélectionner de manière interactive le test qu'il souhaite exécuter, ce qui ne fonctionnera plus comme prévu.

Vous pouvez éviter cela facilement ici en séparant la partie de test principale de chaque test (y compris l '"assertion") des parties qui mettent votre système dans l'état initial correct. En utilisant votre exemple ci-dessus: écrivez des méthodes pour créer un compte, vous connecter et poster un commentaire - sans aucune assertion. Réutilisez ensuite ces méthodes dans différents tests. Vous devrez également ajouter du code à la [Setup]méthode de vos appareils de test pour vous assurer que le système est dans un état initial correctement défini (par exemple, aucun compte jusqu'à présent dans la base de données, personne connecté jusqu'à présent, etc.).

EDIT: Bien sûr, cela semble être contraire à la nature "histoire" de vos tests, mais si vous donnez à vos méthodes d'assistance des noms significatifs, vous trouverez vos histoires dans chaque test.

Donc, cela devrait ressembler à ceci:

[TestFixture]
public class Authentication_Bill
{
    [Setup]
    public void Init()
    {  // bring the system in a predefined state, with noone logged in so far
    }

    [Test]
    public void Test_if_Bill_can_create_account()
    {
         CreateAccountForBill();
         // assert that the account was created properly 
    }

    [Test]
    public void Test_if_Bill_can_post_comment_after_login()
    { 
         // here is the "story" now
         CreateAccountForBill();
         LoginWithBillsAccount();
         AddCommentForBill();
        //  assert that the right things happened
    }

    private void CreateAccountForBill()
    {
        // ...
    }
    // ...
}

J'irais plus loin et dirais que l'utilisation d'un outil xUnit pour exécuter des tests fonctionnels est très bien, tant que vous ne confondez pas l'outillage avec le type de test, et que vous gardez ces tests séparés des tests unitaires réels, afin que les développeurs puissent toujours exécuter les tests unitaires rapidement au moment de la validation. Ces tests seront probablement beaucoup plus lents que les tests unitaires.
bdsl

4

Un problème avec le fait de raconter une histoire avec des tests unitaires est qu'il ne rend pas explicite que les tests unitaires doivent être organisés et exécutés de manière totalement indépendante les uns des autres.

Un bon test unitaire doit être complètement isolé de tout autre code dépendant, c'est la plus petite unité de code qui puisse être testée.

Cela donne l'avantage qu'en plus de confirmer que le code fonctionne, si un test échoue, vous obtenez gratuitement le diagnostic de l'endroit exact où le code est erroné. Si un test n'est pas isolé, vous devez regarder de quoi il dépend pour savoir exactement ce qui a mal tourné et passer à côté d'un avantage majeur du test unitaire. La question de l'ordre d'exécution peut également soulever beaucoup de faux négatifs, si un test échoue, il est possible que les tests suivants échouent malgré le code qu'ils testent parfaitement.

Un bon article plus en profondeur est le classique des tests hybrides sales .

Pour rendre les classes, les méthodes et les résultats lisibles, le grand test Art of Unit utilise la convention de dénomination

Classe de test:

ClassUnderTestTests

Méthodes d'essai:

MethodUnderTest_Condition_ExpectedResult

Pour copier l'exemple de @Doc Brown, plutôt que d'utiliser [Setup] qui s'exécute avant chaque test, j'écris des méthodes d'aide pour construire des objets isolés à tester.

[TestFixture]
public class AuthenticationTests
{
    private Authentication GetAuthenticationUnderTest()
    {
        // create an isolated Authentication object ready for test
    }

    [Test]
    public void CreateAccount_WithValidCredentials_CreatesAccount()
    {
         //Arrange
         Authentication codeUnderTest = GetAuthenticationUnderTest();
         //Act
         Account result = codeUnderTest.CreateAccount("some", "valid", "data");
         //Assert
         //some assert
    }

    [Test]
    public void CreateAccount_WithInvalidCredentials_ThrowsException()
    {
         //Arrange
         Authentication codeUnderTest = GetAuthenticationUnderTest();
         Exception result;
         //Act
         try
         {
             codeUnderTest.CreateAccount("some", "invalid", "data");
         }
         catch(Exception e)
         {
             result = e;
         }
         //Assert
         //some assert
    }
}

Ainsi, les tests qui échouent ont un nom significatif qui vous donne un récit sur la méthode qui a échoué, la condition et le résultat attendu.

C'est comme ça que j'ai toujours écrit des tests unitaires, mais un ami a eu beaucoup de succès avec Gerkin .


1
Bien que je pense que c'est un bon article, je ne suis pas d'accord sur ce que l'article lié dit sur le test "hybride". Avoir de "petits" tests d'intégration (en plus, pas alternativement aux tests unitaires purs , bien sûr) peut à mon humble avis être très utile, même s'ils ne peuvent pas vous dire exactement quelle méthode contient le mauvais code. Si ces tests peuvent être maintenus dépend de la propreté du code pour ces tests, ils ne sont pas "sales" en soi. Et je pense que l'objectif de ces tests peut être très clair (comme dans l'exemple du PO).
Doc Brown

3

Ce que vous décrivez ressemble plus à une conception axée sur le comportement (BDD) qu'à des tests unitaires. Jetez un œil à SpecFlow, une technologie BDD .NET basée sur le DSL Gherkin .

Des trucs puissants que tout être humain peut lire / écrire sans rien savoir du codage. Notre équipe de test connaît un grand succès en tirant parti de nos suites de tests d'intégration.

Concernant les conventions pour les tests unitaires, la réponse de @ DocBrown semble solide.


Pour information, BDD est exactement comme TDD, c'est juste le style d'écriture qui change. Exemple: TDD = assert(value === expected)BDD = value.should.equals(expected)+ vous décrivez les entités en couches qui résolvent le problème de "l'indépendance des tests unitaires". C'est un super style!
Offirmo
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.