2010-05-05 3 views
16

Fondamentalement, j'ai été la programmation pendant un petit moment et après avoir terminé mon dernier projet peut pleinement comprendre combien il aurait été plus facile si j'avais fait TDD. Je suppose que je ne le fais pas encore strictement car j'écris toujours du code puis j'écris un test, je ne comprends pas vraiment comment le test devient avant le code si vous ne savez pas quelles structures et comment vos données de stockage etc. ... mais de toute façon ...Tests unitaires - Est-ce que je le fais bien?

Un peu difficile à expliquer, mais disons par exemple que j'ai un objet Fruit avec des propriétés comme id, color et cost. (Tous les fichiers stockés dans le fichier texte ignorent complètement toute logique de base de données, etc.)

FruitID FruitName FruitColor FruitCost 
    1   Apple  Red   1.2 
    2   Apple  Green  1.4 
    3   Apple  HalfHalf 1.5 

Ceci est juste un exemple. Mais disons que j'ai ceci est une collection de Fruit (c'est un List<Fruit>) objets dans cette structure. Et ma logique dira de réorganiser les fruits dans la collection si un fruit est supprimé (c'est juste comme ça que la solution doit être).

E.g. si 1 est supprimé, l'objet 2 prend le fruit id 1, l'objet 3 prend le fruit id2.

Maintenant, je veux tester le code écrit ive qui fait la remise en ordre, etc.

Comment puis-je configurer cela pour faire le test?


Voici où j'ai jusqu'à présent. Fondamentalement, j'ai classe fruitManager avec toutes les méthodes, comme deletefruit, etc. Il a la liste habituellement mais j'ai changé sa méthode pour le tester afin qu'il accepte une liste, et l'information sur le fruit à supprimer, puis retourne la liste.

Test de l'unité sage: Est-ce que je fais cela fondamentalement de la bonne manière, ou est-ce que je me suis trompé d'idée? puis je teste la suppression de différents objets/ensembles de données valorisés pour garantir le bon fonctionnement de la méthode.


[Test] 
public void DeleteFruit() 
{ 
    var fruitList = CreateFruitList(); 
    var fm = new FruitManager(); 

    var resultList = fm.DeleteFruitTest("Apple", 2, fruitList); 

    //Assert that fruitobject with x properties is not in list ? how 
} 

private static List<Fruit> CreateFruitList() 
{ 
    //Build test data 
    var f01 = new Fruit {Name = "Apple",Id = 1, etc...}; 
    var f02 = new Fruit {Name = "Apple",Id = 2, etc...}; 
    var f03 = new Fruit {Name = "Apple",Id = 3, etc...}; 

    var fruitList = new List<Fruit> {f01, f02, f03}; 
    return fruitList; 
} 
+0

Je ne voudrais pas réaffecter des IDs si j'étais – UpTheCreek

+0

pour l'amour de cette question, ou disons que le champ de valeur se met à jour quand un fruit est supprimé par exemple ... quelque chose comme ça – baron

+0

Dans CreateFruitList(), j'obtiendrais débarrassez-vous des variables fXX et ajoutez simplement de nouveaux Fruits directement à la liste ('fruitList.add (nouveau Fruit (...))'). Juste un petit problème. –

Répondre

12

Si vous ne voyez pas à quel test vous devriez commencer, c'est probablement que vous n'avez pas pensé à ce que votre fonctionnalité devrait faire en termes simples. Essayez d'imaginer une liste hiérarchisée des comportements de base qui sont attendus.

Quelle est la première chose que vous attendez d'une méthode Delete()? Si vous deviez expédier le «produit» Supprimer en 10 minutes, quel serait le comportement non négociable inclus? Eh bien ... probablement qu'il supprime l'élément.

Alors:

1) [Test] 
public void Fruit_Is_Removed_From_List_When_Deleted() 

Lorsque ce test est écrit, passer par la boucle entière TDD (exécution de test => rouge écrire un code juste assez pour faire passer => vert; refactor => vert)

La prochaine chose importante liée à ceci est que la méthode ne devrait pas modifier la liste si le fruit passé en tant qu'argument n'est pas dans la liste. Donc, prochain test pourrait être:

2) [Test] 
public void Invalid_Fruit_Changes_Nothing_When_Deleted() 

La prochaine chose que vous avez spécifié est que ids doivent être réorganisés lorsqu'un fruit est supprimé:

3) [Test] 
public void Fruit_Ids_Are_Reordered_When_Fruit_Is_Deleted() 

Que mettre dans ce test? Eh bien, mettez en place un contexte basique mais représentatif qui prouvera que votre méthode se comporte comme prévu. Par exemple, créez une liste de 4 fruits, supprimez le premier et vérifiez un par un que les 3 ids de fruits restants sont réorganisés correctement. Cela couvrirait plutôt bien le scénario de base.

Ensuite, vous pouvez créer des tests unitaires pour les cas d'erreur ou borderline:

4) [Test] 
public void Fruit_Ids_Arent_Reordered_When_Last_Fruit_Is_Deleted() 

5) [Test] 
[ExpectedException] 
public void Exception_Is_Thrown_When_Fruit_List_Is_Empty() 

...

7

Avant de commencer réellement à écrire votre premier test, vous êtes censé avoir une idée approximative de la structure/conception de votre application, les interfaces, etc. La phase de conception est souvent sorte de implicite avec TDD . Je suppose que pour un développeur expérimenté, c'est un peu évident, et en lisant une spécification de problème (s), il commence immédiatement à visualiser la conception de la solution dans sa tête, c'est peut-être la raison pour laquelle de pris pour acquis. Cependant, pour un développeur moins expérimenté, l'activité de conception devra peut-être être une entreprise plus explicite.

Dans les deux cas, après que la première esquisse de conception est prête, TDD peut être utilisé à la fois pour vérifier le comportement et vérifier la validité/l'utilisabilité de la conception elle-même. Vous pouvez commencer à écrire votre premier test unitaire, puis vous rendre compte "oh, il est en fait assez difficile de le faire avec l'interface que j'ai envisagée" - alors vous revenez en arrière et remodelez l'interface. C'est une approche itérative. Josh Bloch parle de ceci dans "Coders at Work" - il écrit habituellement beaucoup de cas d'utilisation pour ses interfaces même avant commençant à implémenter n'importe quoi. Il esquisse donc l'interface, puis écrit du code qui l'utilise dans tous les différents scénarios auxquels il peut penser. Il n'est pas encore compilable - il l'utilise simplement pour se faire une idée du fait que son interface aide vraiment à accomplir les choses facilement.

+0

mais dites-vous que vous avez et encore pas sûr sur les structures de données etc et comment vous allez réellement conduire l'interface, alors vous ne savez pas comment écrire le test? – baron

+0

Commencer à faire plus de sens mais redessiner l'interface pour des tests unitaires? cela semble être un peu un sacrifice à faire? à moins de le faire, vous êtes plus susceptible de produire une meilleure interface de toute façon ... – baron

+4

@baron, je voulais dire exactement pour re-concevoir l'interface pour le rendre meilleur en général. Les tests unitaires sont un client spécifique à cet égard. Si une interface est difficile à utiliser pour un test unitaire, cela (presque toujours) signifie qu'il est difficile à utiliser pour d'autres clients. –

1

Vous ne serez jamais certain que votre test d'unité couvre toutes les éventualités, c'est donc plus ou moins votre mesure personnelle quant à la façon dont vous testez intensivement et aussi quoi exactement. Votre test unitaire devrait au moins tester les cas limites, ce que vous ne faites pas là. Que se passe-t-il lorsque vous essayez de supprimer un Apple avec un identifiant invalide? Que se passe-t-il si vous avez une liste vide, si vous supprimez le premier/dernier élément, etc.

En général, je ne vois pas l'intérêt de tester un seul cas particulier comme vous le faites ci-dessus. Au lieu de cela, j'essaie toujours courir un tas de tests, ce qui dans votre cas d'exemple suggère une approche légèrement différente:

  • D'abord, écrire une méthode de vérificateur. Vous pouvez le faire dès que vous savez que vous aurez une liste de fruits et que dans cette liste tous les fruits auront des identifiants successifs (c'est comme tester si la liste est triée). Aucun code pour la suppression ne doit être écrit pour cela, plus vous pouvez le réutiliser plus tard. dans le code d'insertion de test d'unité. Ensuite, créez un tas de listes de tests différentes (peut-être aléatoires) (taille vide, taille moyenne, grande taille). Cela ne nécessite également aucun code préalable pour la suppression. Enfin, exécutez des suppressions spécifiques pour chacune des listes de tests (suppression avec un ID invalide, suppression de l'ID 1, suppression du dernier ID, suppression de l'ID aléatoire) et contrôlez le résultat avec votre méthode de vérification. À ce stade, vous devez au moins connaître l'interface de votre méthode de suppression, mais il n'est pas nécessaire d'avoir déjà été écrit.

@Update par rapport au commentaire: La méthode de vérificateur est plus d'un contrôle de cohérence sur la structure de données. Dans votre exemple, tous les fruits de la liste ont des ID successifs, donc c'est vérifié. Si vous avez une structure DAG, vous pouvez vérifier son acyclicité, etc.

Si la suppression de l'ID x a fonctionné, cela dépend de sa présence dans la liste et si votre application distingue le cas d'un échec. suppression due à un identifiant invalide d'un identifiant réussi (dans les deux cas, il n'y a plus d'ID à la fin). De toute évidence, vous voulez également vérifier qu'un ID supprimé n'est plus présent dans la liste (bien que cela ne fasse pas partie de ce que je voulais dire avec la méthode checker - à la place, je pensais qu'il était assez évident d'omettre).

+0

En supprimant le fruit avec fruitid 1 j'essayais de tester la suppression du premier article. En ce qui concerne le point 1 et la méthode du vérificateur, comment cela fonctionne-t-il exactement pour vérifier les choses comme vous mentionnez (supprimer l'ID invalide, supprimer l'ID 1, le dernier identifiant, etc ...) – baron

1

Puisque vous utilisez C#, je suppose que NUnit est votre framework de test. Dans ce cas, vous disposez d'une série de déclarations Assert [..].

En ce qui concerne les spécificités de votre code: Je ne voudrais pas réaffecter les ID, ou modifier la composition des objets Fruit restants de quelque façon que ce soit lors de la manipulation de la liste. Si vous avez besoin de l'identifiant pour garder une trace de la position de l'objet dans la liste, utilisez .IndexOf() à la place. Avec TDD, je trouve souvent difficile d'écrire le test en premier - je finis par écrire le code en premier (code ou chaîne de hacks). Une bonne astuce est de prendre ce "code" et de l'utiliser comme test. Ensuite, écrivez votre code réel à nouveau, légèrement différemment. De cette façon, vous aurez deux morceaux de code différents qui accomplissent la même chose - moins de chance de faire la même erreur dans le code de production et de test. De plus, devoir trouver une deuxième solution pour le même problème peut vous montrer des faiblesses dans votre approche originale, et conduire à un meilleur code.

+0

J'aime votre mention d'utiliser le premier code comme test code pour écrire un meilleur code pour le second. Personnellement, réécrire les choses une seconde fois rend toujours le code beaucoup plus facile à lire et à comprendre. Cependant, peut-être que la réaffectation d'un identifiant était un mauvais exemple, je viens juste de dire de rendre l'exemple plus facile. Mais disons par exemple que la suppression d'une pomme doit entraîner un recalcul des coûts pour les autres pommes, donc je dois changer la composition des objets fruits en manipulant la liste. – baron

+1

Je préfère avoir des tests qui vérifient exactement une chose, donc pour un méthode qui supprime un élément d'un ensemble Je voudrais avoir un test pour quand l'élément est dans l'ensemble avec d'autres éléments, un pour quand il n'est pas dans un ensemble non-vide, un pour quand il est le seul élément dans un ensemble, et un pour quand l'ensemble est vide. –

+0

et votre dire aussi; et ainsi de suite comme un test pour savoir si la valeur de coût recalculé correctement sous x paramètres, .. sous y params etc etc? – baron

1
[Test] 
public void DeleteFruit() 
{ 
    var fruitList = CreateFruitList(); 
    var fm = new FruitManager(fruitList); 

    var resultList = fm.DeleteFruit(2); 

    //Assert that fruitobject with x properties is not in list 
    Assert.IsEqual(fruitList[2], fm.Find(2)); 
} 

private static List<Fruit> CreateFruitList() 
{ 
    //Build test data 
    var f01 = new Fruit {Name = "Apple",Id = 1, etc...}; 
    var f02 = new Fruit {Name = "Apple",Id = 2, etc...}; 
    var f03 = new Fruit {Name = "Apple",Id = 3, etc...}; 

    return new List<Fruit> {f01, f02, f03}; 
} 

Vous pouvez essayer une injection de dépendance de la liste des fruits. L'objet gestionnaire de fruits est un magasin crud. Donc, si vous avez une opération de suppression, vous avez besoin d'une opération de récupération.

En ce qui concerne la réorganisation, voulez-vous que cela se produise automatiquement ou voulez-vous une opération de villégiature? Le automatiquement peut également être dès que l'opération de suppression se produit ou un paresseux uniquement lors de la récupération. C'est un détail d'implémentation. On peut en dire beaucoup plus à ce sujet. Un bon début pour maîtriser cet exemple spécifique serait d'utiliser Design By Contract.

[Modifier 1a]

vous pouvez également demander pourquoi vos tests pour les implémentations spécifiques de Fruit. FruitManager devrait gérer un concept abstrait appelé Fruit. Vous devez faire attention aux détails de mise en œuvre prématurée à moins que vous cherchiez à utiliser les DTO, mais le problème est que Fruit peut éventuellement changer d'un objet avec getters à un objet avec un comportement réel. Maintenant, non seulement vos tests pour Fruit échoueront, mais FruitManager échouera!

3

Test de l'unité sage: Suis-je fondamentalement faire de la bonne manière, ou ai-je une mauvaise idée?

Vous avez raté le bateau.

Je ne suis pas tout à fait comment le test devient avant le code si vous ne savez pas ce que les structures et la façon dont vous stockez des données

Ceci est le point que je pense que vous avez besoin de revenir à, si vous voulez que les idées aient un sens. Premier point: les structures de données et le stockage dérivent de ce que vous avez besoin du code à faire, et non l'inverse. Plus en détail, si vous partez de rien, il existe un certain nombre d'implémentations de structure/stockage que vous pouvez utiliser; En effet, vous devriez pouvoir échanger entre eux sans avoir besoin de changer vos tests.

Deuxième point: Dans la plupart des cas, vous consommez votre code plus souvent que vous ne le produisez. Vous l'écrivez une fois, mais vous (et vos collègues) l'appelez plusieurs fois. Par conséquent, la commodité d'appeler le code devrait avoir une priorité plus élevée que si vous écriviez votre solution uniquement de l'intérieur. Ainsi, lorsque vous vous retrouvez à écrire un test et que vous constatez que l'implémentation du client est laide/maladroite/inadaptée, il déclenche un avertissement avant même que vous ayez commencé à implémenter quoi que ce soit. De même, si vous vous trouvez en train d'écrire beaucoup de code d'installation dans vos tests, cela vous indique que vous n'avez pas vraiment vos préoccupations bien séparées. Lorsque vous vous dites "wow, ce test était facile à écrire", vous avez probablement une interface facile à utiliser.

Il est très difficile d'atteindre cet objectif lorsque vous utilisez des exemples orientés sur l'implémentation (comme écrire un test pour un conteneur). Ce dont vous avez besoin, c'est d'un problème de jouet bien délimité, indépendant de la mise en œuvre. Pour un exemple trivial, vous pouvez envisager un gestionnaire d'authentification - transmettre un identificateur et un secret, et savoir si le secret correspond à l'identificateur. Vous devriez donc pouvoir écrire trois tests rapides dès le début: vérifiez que le bon secret autorise l'accès, vérifiez qu'un secret incorrect interdit l'accès, vérifiez que lorsqu'un secret est modifié, seule la nouvelle version autorise l'accès.

Alors vous pouvez écrire des tests simples avec des noms d'utilisateur et des mots de passe. Et comme vous le faites, vous réalisez que les secrets ne doivent pas être limités aux chaînes, mais que vous devriez pouvoir faire un secret à partir de tout ce qui est sérialisable, et que l'accès n'est peut-être pas universel mais restreint peut-être pas) et oh vous aurez envie de démontrer que les secrets sont conservés en toute sécurité ....

Vous pouvez, bien sûr, prendre cette même approche pour les conteneurs. Mais je pense que vous trouverez plus facile de "l'obtenir" si vous démarrez à partir d'un problème utilisateur/entreprise, plutôt que d'un problème de mise en œuvre.

Les tests unitaires qui vérifient une implémentation spécifique ("Avons-nous une erreur post-clôture ici?") Ont une valeur. Le processus de création de ceux-ci ressemble beaucoup plus à «deviner un bogue, écrire un test pour vérifier le bogue, réagir si le test échoue». Cependant, ces tests ne contribuent généralement pas à votre conception: vous êtes beaucoup plus susceptible de cloner un bloc de code et de modifier certaines entrées. Cependant, lorsque les tests unitaires suivent la mise en œuvre, ils sont souvent difficiles à écrire et ont de gros coûts de démarrage ("Pourquoi ai-je besoin de charger trois bibliothèques et de démarrer un serveur web distant pour tester une erreur dans ma boucle for? ? ").

Lectures recommandées Freeman/Pryce, croissance du logiciel orienté objet, guidée par des tests

1

Démarrer avec l'interface, une mise en œuvre concrète squelette.Pour chaque méthode/propriété/événement/constructeur, il y a un comportement attendu. Commencez par une spécification pour le premier comportement, et le compléter:

[spécification] est identique à [TestFixture] [Il] est identique à [Test]

[Specification] 
When_fruit_manager_has_delete_called_with_existing_fruit : FruitManagerSpecifcation 
{ 
    private IEnumerable<IFruit> _fruits; 

    [It] 
    public void Should_remove_the_expected_fruit() 
    { 
    Assert.Inconclusive("Please implement"); 
    } 

    [It] 
    public void Should_not_remove_any_other_fruit() 
    { 
    Assert.Inconclusive("Please implement"); 
    } 

    [It] 
    public void Should_reorder_the_ids_of_the_remaining_fruit() 
    { 
    Assert.Inconclusive("Please implement"); 
    } 

    /// <summary> 
    /// Setup the SUT before creation 
    /// </summary> 
    public override void GivenThat() 
    { 
    _fruits = new List<IFruit>(); 

    3.Times(_fruits.Add(Mock<IFruit>())); 

    this._fruitToDelete = _fruits[1]; 

    // this fruit is injected in th Sut 
    Dep<IEnumerable<IFruit>>() 
       .Stub(f => ((IEnumerable)f).GetEnumerator()) 
       .Return(this.Fruits.GetEnumerator()) 
       .WhenCalled(mi => mi.ReturnValue = this._fruits.GetEnumerator()); 

    } 

    /// <summary> 
    /// Delete a fruit 
    /// </summary> 
    public override void WhenIRun() 
    { 
    Sut.Delete(this._fruitToDelete); 
    } 
} 

La spécification ci-dessus est juste adhoc et INCOMPLETE, mais c'est un comportement sympathique de TDD pour approcher chaque unité/spécification.

Voici ferait partie du SUT inappliquées lorsque vous commencez à travailler dessus:

public interface IFruitManager 
{ 
    IEnumerable<IFruit> Fruits { get; } 

    void Delete(IFruit); 
} 

public class FruitManager : IFruitManager 
{ 
    public FruitManager(IEnumerable<IFruit> fruits) 
    { 
    //not implemented 
    } 

    public IEnumerable<IFruit> Fruits { get; private set; } 

    public void Delete(IFruit fruit) 
    { 
    // not implemented 
    } 
} 

Comme vous pouvez le voir pas de code réel est écrit. Si vous voulez compléter cette première spécification "When _...", vous devez d'abord faire un [ConstructorSpecification] When_fruit_manager_is_injected_with_fruit() car les fruits injectés ne sont pas affectés à la propriété Fruits. Donc, voila, pas de code REAL est nécessaire pour mettre en œuvre au début ... la seule chose nécessaire maintenant est la discipline. Une chose que j'aime à ce sujet, c'est que si vous avez besoin de classes supplémentaires pendant l'implémentation du SUT actuel, vous n'avez pas besoin de les implémenter avant d'implémenter le FruitManager car vous pouvez simplement utiliser des mocks comme par exemple ISomeDependencyNeeded ... Lorsque vous avez terminé Fruit manager, vous pouvez aller travailler sur la classe SomeDependencyNeeded. Assez méchant.