2010-02-11 11 views
8

récursive Si j'ai une méthode qui se dit sous une certaine condition, est-il possible d'écrire un test pour vérifier le comportement? J'aimerais voir un exemple, je me moque du cadre factice ou du langage. J'utilise RhinoMocks en C#, donc je suis curieux de savoir s'il s'agit d'une caractéristique manquante du cadre, ou si je ne comprends pas quelque chose de fondamental, ou si c'est juste une impossibilité.Comment écrire un test simulacres d'une méthode

+0

Il est pas clair pour moi. Qu'est-ce que vous essayez exactement de tester? Que la méthode s'appelle "sous certaines conditions" (que la "pile d'appels" suivra un certain chemin "sous certaines conditions") ou autre chose? – Ando

Répondre

3

En supposant que vous voulez faire quelque chose comme obtenir le nom du fichier à partir d'un chemin complet, par exemple:

c:/windows/awesome/lol.cs -> lol.cs 
c:/windows/awesome/yeah/lol.cs -> lol.cs 
lol.cs -> lol.cs 

et vous avez:

public getFilename(String original) { 
    var stripped = original; 
    while(hasSlashes(stripped)) { 
    stripped = stripped.substringAfterFirstSlash(); 
    } 
    return stripped; 
} 

et que vous voulez écrire:

public getFilename(String original) { 
    if(hasSlashes(original)) { 
    return getFilename(original.substringAfterFirstSlash()); 
    } 
    return original; 
} 

récursivité est ici un détail de mise en œuvre et ne doit pas être testé. Vous voulez vraiment pouvoir basculer entre les deux implémentations et vérifier qu'elles produisent le même résultat: les deux produisent lol.cs pour les trois exemples ci-dessus. Cela étant dit, parce que vous êtes récursif par nom, plutôt que de dire thisMethod.again() etc., dans Ruby vous pouvez alias la méthode originale à un nouveau nom, redéfinir la méthode avec l'ancien nom, invoquer le nouveau nom et vérifiez si vous vous retrouvez dans la nouvelle méthode définie.

def blah 
    puts "in blah" 
    blah 
end 

alias blah2 blah 

def blah 
    puts "new blah" 
end 

blah2 
+0

Voulez-vous dire que dans ce cas, un test unitaire qui vérifie l'état de la méthode est assez bon? – JeremyWeir

+0

désolé, je ne comprends pas complètement votre question. Dans mon exemple de chemin de fichier, un test unitaire qui vérifie la sortie de la méthode est suffisant, voire meilleur qu'un test qui vérifie la récursivité. Cependant, je ne connais pas votre situation spécifique, donc cela pourrait être différent. – miaubiz

+0

@jayrdub - En général, la vérification d'état est exactement ce que vous voulez faire avec vos tests unitaires. Vérifiez la valeur de retour de la méthode et/ou les propriétés publiques de l'objet testé. Tout le reste est un détail d'implémentation et peut changer lors du refactoring. – TrueWill

1

Il n'y a rien à surveiller la profondeur pile/nombre de fonction (récursive) appelle dans un cadre moqueur, je suis au courant. Cependant, les tests unitaires indiquant que les conditions préalables correctement simulées fournissent les sorties correctes doivent être les mêmes que se moquer d'une fonction non récursive.

infini récursion qui conduit à un débordement de pile, vous devrez déboguer séparément, mais les tests unitaires et se moque ont jamais exterminé ce besoin en premier lieu.

6

une méthode qui se dit sous une certaine condition, est-il possible d'écrire un test pour vérifier le comportement?

Oui. Cependant, si vous devez tester la récursivité, il est préférable de séparer le point d'entrée dans l'étape de récurrence et de récursivité à des fins de test.

Quoi qu'il en soit, voici l'exemple comment tester si vous ne pouvez pas faire cela. Vous n'avez pas vraiment besoin de se moquer:

// Class under test 
public class Factorial 
{ 
    public virtual int Calculate(int number) 
    { 
     if (number < 2) 
      return 1 
     return Calculate(number-1) * number; 
    } 
} 

// The helper class to test the recursion 
public class FactorialTester : Factorial 
{ 
    public int NumberOfCalls { get; set; } 

    public override int Calculate(int number) 
    { 
     NumberOfCalls++; 
     return base.Calculate(number) 
    } 
}  

// Testing 
[Test] 
public void IsCalledAtLeastOnce() 
{ 
    var tester = new FactorialTester(); 
    tester.Calculate(1); 
    Assert.GreaterOrEqual(1, tester.NumberOfCalls ); 
} 
[Test] 
public void IsCalled3TimesForNumber3() 
{ 
    var tester = new FactorialTester(); 
    tester.Calculate(3); 
    Assert.AreEqual(3, tester.NumberOfCalls ); 
} 
4

Vous ne comprenez pas le but des objets simulés. Les mocks (au sens de Mockist) sont utilisés pour tester les interactions comportementales avec les dépendances du système testé.

Ainsi, par exemple, vous pourriez avoir quelque chose comme ceci:

interface IMailOrder 
{ 
    void OrderExplosives(); 
} 

class Coyote 
{ 
    public Coyote(IMailOrder mailOrder) {} 

    public void CatchDinner() {} 
} 

Coyote dépend IMailOrder. Dans le code de production, une instance de Coyote reçoit une instance d'Acme, qui implémente IMailOrder. (Cela peut se faire par injection manuelle de dépendance ou par un cadre de DI.)

Vous voulez tester la méthode CatchDinner et vérifier qu'il appelle OrderExplosives.Pour ce faire, vous:

  1. Créer un objet fantaisie qui implémente IMailOrder et créer une instance de Coyote (le système en cours de test) en faisant passer l'objet fantaisie à son constructeur. (Organiser)
  2. Appelez CatchDinner. (Act)
  3. Demandez à l'objet maquette de vérifier qu'une attente donnée (OrderExplosives appelée) a été satisfaite. (Assert)

Lorsque vous définissez les attentes sur l'objet fantaisie peut dépendre de votre cadre de moquerie (isolation).

Si la classe ou la méthode que vous testez n'a aucune dépendance externe, vous n'avez pas besoin (ou ne voulez pas) d'utiliser des objets fictifs pour cet ensemble de tests. Peu importe si la méthode est récursive ou non.

Vous souhaitez généralement tester les conditions aux limites, vous pouvez donc tester un appel qui ne doit pas être récursif, un appel avec un appel récursif unique et un appel récursif. (Miaubiz a un bon point sur récursivité étant un détail de mise en œuvre, bien que.)

EDIT: par « appel » dans le dernier paragraphe que je voulais dire un appel avec des paramètres ou un état objet qui déclencherait une profondeur de récursivité donnée. Je recommande également de lire The Art of Unit Testing.

EDIT 2: code d'essai Exemple d'utilisation Moq:

var mockMailOrder = new Mock<IMailOrder>(); 
var wily = new Coyote(mockMailOrder.Object); 

wily.CatchDinner(); 

mockMailOrder.Verify(x => x.OrderExplosives()); 
+0

"Si la classe ou la méthode que vous testez n'a aucune dépendance externe, vous n'avez pas besoin (ou ne voulez pas) d'utiliser des objets fictifs pour cet ensemble de tests, que la méthode soit récursive ou non." C'est la partie dont je devais me souvenir, merci. J'ai aimé votre réponse, mais elle a choisi automatiquement avant que je puisse. – JeremyWeir

+0

@jayrdub - Merci! :) – TrueWill

0

Voici mon approche paysanne »(en Python, testé, voir les commentaires pour les raisons)

Notez que la mise en œuvre le détail "exposition" est hors de question ici, puisque ce que vous testez est l'architecture sous-jacente qui se trouve être utilisée par le code "de premier niveau". Donc, le tester est légitime et sage (j'espère aussi, c'est ce que vous avez en tête).

Le code (l'idée principale est de passer d'une simple mais fonction récursive « invérifiable » à une paire équivalente de fonctions dépendantes récursive (et donc testables)):

def factorial(n): 
    """Everyone knows this functions contract:) 
    Internally designed to use 'factorial_impl' (hence recursion).""" 
    return factorial_impl(n, factorial_impl) 

def factorial_impl(n, fct=factorial): 
    """This function's contract is 
    to return 'n*fct(n-1)' for n > 1, or '1' otherwise. 

    'fct' must be a function both taking and returning 'int'""" 
    return n*fct(n - 1) if n > 1 else 1 

Le test:

import unittest 

class TestFactorial(unittest.TestCase): 

    def test_impl(self): 
     """Test the 'factorial_impl' function, 
     'wiring' it to a specially constructed 'fct'""" 

     def fct(n): 
      """To be 'injected' 
      as a 'factorial_impl''s 'fct' parameter""" 
      # Use a simple number, which will 'show' itself 
      # in the 'factorial_impl' return value. 
      return 100 

     # Here we must get '1'. 
     self.assertEqual(factorial_impl(1, fct), 1) 
     # Here we must get 'n*100', note the ease of testing:) 
     self.assertEqual(factorial_impl(2, fct), 2*100) 
     self.assertEqual(factorial_impl(3, fct), 3*100) 

    def test(self): 
     """Test the 'factorial' function""" 
     self.assertEqual(factorial(1), 1) 
     self.assertEqual(factorial(2), 2) 
     self.assertEqual(factorial(3), 6) 

La sortie:

Finding files... 
['...py'] ... done 
Importing test modules ... done. 

Test the 'factorial' function ... ok 
Test the 'factorial_impl' function, ... ok 

---------------------------------------------------------------------- 
Ran 2 tests in 0.000s 

OK