Question

Depuis que j'utilise rspec, la notion de fixture me pose problème. Mes principales préoccupations sont les suivantes:

  1. J'utilise des tests pour révéler un comportement surprenant. Je ne suis pas toujours assez intelligent pour énumérer tous les cas possibles des exemples que je teste. Utiliser des fixtures codées en dur semble limitant car cela teste uniquement mon code avec les cas très spécifiques que j'ai imaginés. (Certes, mon imagination limite également les cas dans lesquels je teste.)

  2. J'utilise testing comme une forme de documentation du code. Si j'ai des valeurs de fixture codées en dur, il est difficile de révéler ce qu'un test particulier essaie de démontrer. Par exemple:

    describe Item do
      describe '#most_expensive' do
        it 'should return the most expensive item' do
          Item.most_expensive.price.should == 100
          # OR
          #Item.most_expensive.price.should == Item.find(:expensive).price
          # OR
          #Item.most_expensive.id.should == Item.find(:expensive).id
        end
      end
    end
    

    L’utilisation de la première méthode n’indique pas au lecteur quel est le produit le plus cher, mais seulement que son prix est de 100. Les trois méthodes demandent au lecteur de penser que l’appareil : Cher est le plus cher répertorié dans fixtures / items.yml . Un programmeur négligent pourrait interrompre les tests en créant un élément dans avant (: all) ou en insérant un autre appareil dans fixtures / items.yml . S'il s'agit d'un fichier volumineux, le problème peut prendre longtemps.

J'ai commencé à ajouter une méthode #generate_random à tous mes modèles. Cette méthode est uniquement disponible lorsque j'exécute mes spécifications. Par exemple:

class Item
  def self.generate_random(params={})
    Item.create(
      :name => params[:name] || String.generate_random,
      :price => params[:price] || rand(100)
    )
  end
end

(Les détails de cette opération sont en fait un peu plus propres. J'ai une classe qui gère la génération et le nettoyage de tous les modèles, mais ce code est suffisamment clair pour mon exemple.) Ainsi, dans l'exemple ci-dessus, pourrait tester comme suit. Un avertissement pour la feinte du cœur: mon code repose fortement sur l’utilisation de avant (: tout) :

describe Item do
  describe '#most_expensive' do
    before(:all) do
      @items = []
      3.times { @items << Item.generate_random }
      @items << Item.generate_random({:price => 50})
    end

    it 'should return the most expensive item' do
      sorted = @items.sort { |a, b| b.price <=> a.price }
      expensive = Item.most_expensive
      expensive.should be(sorted[0])
      expensive.price.should >= 50      
    end
  end
end

Ainsi, mes tests révèlent un comportement surprenant. Lorsque je génère des données de cette manière, je tombe parfois sur un cas extrême où mon code ne se comporte pas comme prévu, mais que je n'aurais pas compris si je n'utilisais que des fixtures. Par exemple, dans le cas de #most_expensive , si j'oubliais de gérer le cas particulier où plusieurs articles partagent le prix le plus cher, mon test échouait parfois au premier si . Voir les échecs non déterministes dans AutoSpec me dirait que quelque chose ne va pas. Si je n'utilisais que des fixtures, la découverte d'un tel bogue pourrait prendre beaucoup plus de temps.

Mes tests permettent également de démontrer légèrement dans le code le comportement attendu. Mon test montre clairement que trié est un tableau d'éléments triés par ordre décroissant de prix. Puisque je m'attends à ce que #most_expensive soit égal au premier élément de ce tableau, le comportement attendu de most_expensive est encore plus évident.

Alors, est-ce une mauvaise pratique? Ma peur des rencontres est-elle irrationnelle? L’écriture d’une méthode generate_random pour chaque modèle est-elle trop laborieuse? Ou est-ce que ça marche?

Était-ce utile?

La solution

Ceci est une réponse à votre deuxième point:

  

(2) J'utilise testing comme une forme de documentation du code. Si j’ai des valeurs d’appareil codées en dur, il est difficile de révéler ce qu’un test particulier tente de démontrer.

Je suis d'accord. Idéalement, les exemples devraient être compréhensibles par eux-mêmes. L'utilisation de fixtures est problématique, car elle divise les conditions préalables de l'exemple des résultats attendus.

Pour cette raison, de nombreux utilisateurs de RSpec ont complètement cessé d'utiliser les appareils. Construisez plutôt les objets nécessaires dans l'exemple de spécification lui-même.

describe Item, "#most_expensive" do
  it 'should return the most expensive item' do
    items = [
      Item.create!(:price => 100),
      Item.create!(:price => 50)
    ]

    Item.most_expensive.price.should == 100
  end
end

Si vous vous retrouvez avec beaucoup de code standard pour la création d’objets, vous devriez jeter un coup d’œil sur les nombreuses bibliothèques de fabriques d’objets tests, telles que factory_girl , Machiniste , ou < a href = "http://replacefixtures.rubyforge.org/" rel = "nofollow noreferrer"> FixtureReplacement .

Autres conseils

Je ne suis surpris par personne dans ce sujet ou dans le Jason Baker lié à mentionné Tests Monte Carlo . C'est la seule fois où j'ai largement utilisé des entrées de test aléatoires. Cependant, il était très important de rendre le test reproductible en prévoyant une valeur de départ constante pour le générateur de nombres aléatoires pour chaque cas de test.

Nous avons beaucoup réfléchi à cette question lors d’un projet récent. Au final, nous avons opté pour deux points:

  • La répétabilité des cas de test est d’une importance primordiale. Si vous devez écrire un test au hasard, soyez prêt à le documenter de manière détaillée, car s'il échoue, vous devez savoir exactement pourquoi.
  • L'utilisation du hasard comme béquille pour la couverture de code signifie soit que vous n'avez pas une bonne couverture, soit que vous ne comprenez pas suffisamment le domaine pour savoir ce qui constitue des scénarios de test représentatifs. Déterminez ce qui est vrai et corrigez-le en conséquence.

En résumé, l’aléatoire peut souvent représenter plus de problèmes qu’il ne vaut. Réfléchissez bien si vous allez l'utiliser correctement avant d'appuyer sur la gâchette. Nous avons finalement décidé que les tests aléatoires étaient une mauvaise idée en général et qu’ils devaient être utilisés avec parcimonie, voire pas du tout.

Beaucoup de bonnes informations ont déjà été publiées, mais voir aussi: Test Fuzz . Selon la rumeur, Microsoft utilise cette approche pour bon nombre de leurs projets.

Mon expérience des tests porte principalement sur des programmes simples écrits en C / Python / Java. Je ne sais donc pas si cela est tout à fait applicable, mais chaque fois que j'ai un programme pouvant accepter n'importe quel type de saisie utilisateur, j'inclus toujours un test avec des données d'entrée aléatoires, ou au moins des données d'entrée générées par l'ordinateur de manière imprévisible, car vous ne pouvez jamais émettre d'hypothèses sur ce que les utilisateurs vont saisir. Ou bien, vous pouvez , mais si vous le faites, un pirate informatique qui ne prend pas cette hypothèse peut bien trouver un bogue que vous avez totalement oublié. Les informations générées par machine sont la meilleure (seule?) Méthode que je connaisse pour que les préjugés humains soient totalement exclus des procédures de test. Bien entendu, pour reproduire un test ayant échoué, vous devez enregistrer l’entrée de test dans un fichier ou l’imprimer (s’il s’agit de texte) avant d’exécuter le test.

Les tests aléatoires sont une mauvaise pratique tant que vous n’avez pas de solution au problème d’Oracle , c’est-à-dire déterminer quel est le résultat attendu de votre logiciel en fonction de son apport.

Si vous résolvez le problème d'Oracle, vous pouvez aller plus loin que la simple génération d'entrées aléatoires. Vous pouvez choisir des distributions d’entrée de telle sorte que des parties spécifiques de votre logiciel s’exercent davantage que par simple aléatoire.

Vous passez ensuite d'un test aléatoire à un test statistique.

if (a > 0)
    // Do Foo
else (if b < 0)
    // Do Bar
else
    // Do Foobar

Si vous sélectionnez a et b de manière aléatoire dans la plage int , vous exercez Foo dans 50% des cas. , Bar 25% du temps et Foobar 25% du temps. Il est probable que vous trouverez plus de bogues dans Foo que dans Bar ou Foobar .

Si vous sélectionnez a de telle sorte qu'il soit négatif 66,66% du temps, Bar et Foobar seront plus exercés que lors de votre première distribution . En effet, les trois branches s’exercent chacune 33,33% du temps.

Bien sûr, si votre résultat observé est différent de votre résultat attendu, vous devez enregistrer tout ce qui peut être utile pour reproduire le bogue.

Je suggérerais de jeter un coup d'œil à Machinist:

  

http://github.com/notahat/machinist/tree/master

Machinist générera des données pour vous, mais elles sont répétables. Par conséquent, chaque série de tests contient les mêmes données aléatoires.

Vous pouvez faire quelque chose de similaire en configurant le générateur de nombres aléatoires de manière cohérente.

Un problème avec les cas de test générés aléatoirement est que la validation de la réponse doit être calculée par code et vous ne pouvez pas être sûr qu'elle ne contient pas de bugs:)

Vous pouvez également consulter le sujet suivant: Méthodes recommandées pour le test avec des entrées aléatoires .

L’efficacité de tels tests dépend en grande partie de la qualité du générateur de nombres aléatoires que vous utilisez et de l’exactitude du code qui traduit le résultat de RNG en données de test.

Si le générateur de ressources aléatoires ne produit jamais de valeurs entraînant le blocage du code dans votre code, celui-ci ne sera pas traité. Si votre code traduisant la sortie du générateur de ressources nucléaires en entrée du code que vous testez est défectueux, il peut arriver que même avec un bon générateur, vous ne rencontrez toujours pas tous les cas extrêmes.

Comment allez-vous tester pour cela?

Le problème de l’aléatoire dans les cas de test est que le résultat est, de manière aléatoire,

.

L’idée derrière les tests (en particulier les tests de régression) est de vérifier que rien n’est cassé.

Si vous trouvez quelque chose qui est cassé, vous devez inclure ce test à chaque fois par la suite, sinon vous ne disposerez pas d'un ensemble de tests cohérent. De plus, si vous exécutez un test aléatoire qui fonctionne, vous devez inclure ce test, car il est possible que vous cassiez le code pour que le test échoue.

En d'autres termes, si vous avez un test qui utilise des données aléatoires générées à la volée, je pense que c'est une mauvaise idée. Si, toutefois, vous utilisez un ensemble de données aléatoires que vous stockez et réutilisez, cela peut être une bonne idée. Cela pourrait prendre la forme d’un ensemble de graines pour un générateur de nombres aléatoires.

Ce stockage des données générées vous permet de trouver la réponse "correcte" à ces données.

Donc, je vous recommanderais d'utiliser des données aléatoires pour explorer votre système, mais d'utiliser des données définies dans vos tests (qui peuvent avoir été à l'origine générées de manière aléatoire)

L'utilisation de données de test aléatoires est une excellente pratique: les données de test codées en dur testent uniquement les cas auxquels vous avez explicitement pensé, tandis que les données aléatoires effacent vos suppositions implicites qui pourraient être fausses.

Je recommande fortement d’utiliser Factory Girl et ffaker pour cela. (N'utilisez jamais les fixations Rails pour quoi que ce soit en toutes circonstances.)

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top