2010-05-14 6 views
5

J'ai deux spécifications très similaires pour deux actions de contrôleur très similaires: VoteUp (int id) et VoteDown (int id). Ces méthodes permettent à un utilisateur de voter un post en haut ou en bas; un peu comme la fonctionnalité de vote haut/bas pour les questions StackOverflow. Les spécifications sont les suivantes:DRY-ing spécifications très similaires pour l'action du contrôleur ASP.NET MVC avec MSpec (directives BDD)

VoteDown:

[Subject(typeof(SomeController))] 
public class When_user_clicks_the_vote_down_button_on_a_post : SomeControllerContext 
{ 
    Establish context =() => 
    { 
     post = PostFakes.VanillaPost(); 
     post.Votes = 10; 

     session.Setup(s => s.Single(Moq.It.IsAny<Expression<Func<Post, bool>>>())).Returns(post); 
     session.Setup(s => s.CommitChanges()); 
    }; 

    Because of =() => result = controller.VoteDown(1); 

    It should_decrement_the_votes_of_the_post_by_1 =() => suggestion.Votes.ShouldEqual(9); 
    It should_not_let_the_user_vote_more_than_once; 
} 

VoteUp:

[Subject(typeof(SomeController))] 
public class When_user_clicks_the_vote_down_button_on_a_post : SomeControllerContext 
{ 
    Establish context =() => 
    { 
     post = PostFakes.VanillaPost(); 
     post.Votes = 0; 

     session.Setup(s => s.Single(Moq.It.IsAny<Expression<Func<Post, bool>>>())).Returns(post); 
     session.Setup(s => s.CommitChanges()); 
    }; 

    Because of =() => result = controller.VoteUp(1); 

    It should_increment_the_votes_of_the_post_by_1 =() => suggestion.Votes.ShouldEqual(1); 
    It should_not_let_the_user_vote_more_than_once; 
} 

J'ai donc deux questions:

  1. Comment dois-je aller sur DRY-ment ces deux spécifications ? Est-il même conseillé ou devrais-je avoir une spécification par action de contrôleur? Je sais que je le devrais normalement, mais j'ai l'impression de me répéter beaucoup.

  2. Existe-t-il un moyen d'implémenter le second It dans la même spécification? Notez que le It should_not_let_the_user_vote_more_than_once; nécessite la spécification pour appeler deux fois le controller.VoteDown(1). Je sais que le plus simple serait de créer une spécification séparée pour elle aussi, mais ce serait de copier-coller le même code encore une fois ...

Je reçois toujours le coup de BDD (et MSpec) et de nombreuses fois, je ne vois pas dans quelle direction je devrais aller, ni quelles sont les meilleures pratiques ou lignes directrices pour BDD. Toute aide serait appréciée.

Répondre

8

Je commencerai par votre deuxième question: MSpec contient une fonctionnalité qui faciliterait la duplication des champs It, mais dans ce scénario, je déconseille de l'utiliser. La fonction est appelée et va Behaviors quelque chose comme ceci:

[Subject(typeof(SomeController))] 
public class When_user_clicks_the_vote_up_button_on_a_post : SomeControllerContext 
{ 
    // Establish and Because cut for brevity. 

    It should_increment_the_votes_of_the_post_by_1 = 
     () => suggestion.Votes.ShouldEqual(1); 

    Behaves_like<SingleVotingBehavior> a_single_vote; 
} 

[Subject(typeof(SomeController))] 
public class When_user_clicks_the_vote_down_button_on_a_post : SomeControllerContext 
{ 
    // Establish and Because cut for brevity. 

    It should_decrement_the_votes_of_the_post_by_1 = 
     () => suggestion.Votes.ShouldEqual(9); 

    Behaves_like<SingleVotingBehavior> a_single_vote; 
} 

[Behaviors] 
public class SingleVotingBehavior 
{ 
    It should_not_let_the_user_vote_more_than_once = 
     () => true.ShouldBeTrue(); 
} 

Tous les champs que vous souhaitez faire valoir dans la classe de comportement doivent être protected static à la fois le comportement et la classe de contexte. Le code source MSpec contient another example.

Je déconseille l'utilisation de comportements car votre exemple contient en réalité quatre contextes. Quand je pense à ce que vous essayez d'exprimer avec le code en termes de « sens des affaires », quatre cas différents apparaissent:

  • utilisateur vote pour la première fois
  • utilisateur vote vers le bas pour la première fois
  • utilisateur vote pour la deuxième fois
  • utilisateur vote vers le bas pour la deuxième fois

pour chacun des quatre scénarios différents, je voudrais créer un contexte distinct qui décrit de près comment le système doit Behav e. Quatre classes de contexte sont beaucoup de code dupliqué, ce qui nous amène à votre première question.

Dans le "modèle" ci-dessous, il existe une classe de base avec des méthodes qui ont des noms descriptifs de ce qui se passera quand vous les appellerez. Ainsi, au lieu de compter sur le fait que MSpec appellera automatiquement les champs Because "hérités", vous mettez des informations sur ce qui est important pour le contexte dans le Establish. D'après mon expérience, cela vous aidera beaucoup plus tard quand vous lisez une spécification au cas où elle échouerait. Au lieu de naviguer dans une hiérarchie de classes, vous avez immédiatement une idée de la configuration qui a lieu. Sur une note connexe, le deuxième avantage est que vous n'avez besoin que d'une classe de base, quel que soit le nombre de contextes différents avec une configuration spécifique que vous dérivez.

public abstract class VotingSpecs 
{ 
    protected static Post CreatePostWithNumberOfVotes(int votes) 
    { 
     var post = PostFakes.VanillaPost(); 
     post.Votes = votes; 
     return post; 
    } 

    protected static Controller CreateVotingController() 
    { 
     // ... 
    } 

    protected static void TheCurrentUserVotedUpFor(Post post) 
    { 
     // ... 
    } 
} 

[Subject(typeof(SomeController), "upvoting")] 
public class When_a_user_clicks_the_vote_up_button_on_a_post : VotingSpecs 
{ 
    static Post Post; 
    static Controller Controller; 
    static Result Result ; 

    Establish context =() => 
    { 
     Post = CreatePostWithNumberOfVotes(0); 

     Controller = CreateVotingController(); 
    }; 

    Because of =() => { Result = Controller.VoteUp(1); }; 

    It should_increment_the_votes_of_the_post_by_1 = 
     () => Post.Votes.ShouldEqual(1); 
} 


[Subject(typeof(SomeController), "upvoting")] 
public class When_a_user_repeatedly_clicks_the_vote_up_button_on_a_post : VotingSpecs 
{ 
    static Post Post; 
    static Controller Controller; 
    static Result Result ; 

    Establish context =() => 
    { 
     Post = CreatePostWithNumberOfVotes(1); 
     TheCurrentUserVotedUpFor(Post); 

     Controller = CreateVotingController(); 
    }; 

    Because of =() => { Result = Controller.VoteUp(1); }; 

    It should_not_increment_the_votes_of_the_post_by_1 = 
     () => Post.Votes.ShouldEqual(1); 
} 

// Repeat for VoteDown(). 
+0

Merci encore. Je connaissais les comportements (à partir de l'exemple de code source de MSpec), mais j'avais l'impression que je devais l'intégrer à mon scénario; ça ne me semblait pas naturel. Réponse brillante, merci un million. –

0

Vous pourriez probablement factoriser une grande partie de la répétition en prenant simplement en compte la configuration des tests. Il n'y a pas de raison pour que la spécification upvote passe de 0 à 1 vote plutôt que de 10 à 11, donc vous pouvez très bien avoir une seule routine d'installation. Cela seul laissera les deux tests à 3 lignes de code (ou 4, si vous avez besoin d'appeler la méthode d'installation manuellement ...). Soudainement, vos tests consistent seulement à exécuter l'action et à vérifier les résultats. Et que ce soit répétitif ou non, je vous conseille fortement de tester une chose par test, simplement parce que vous voulez savoir exactement pourquoi un test échoue lorsque vous refactorisez quelque chose en un mois et que vous exécutez tous les tests dans la solution.

MISE À JOUR (voir commentaires pour plus de détails)

private WhateverTheTypeNeedsToBe vote_count_context =() => 
{ 
    post = PostFakes.VanillaPost(); 
    post.Votes = 10; 

    session.Setup(s => s.Single(Moq.It.IsAny<Expression<Func<Post, bool>>>())).Returns(post); 
    session.Setup(s => s.CommitChanges()); 
}; 

Et dans votre cahier des charges:

Establish context = vote_count_context; 
... 

pourrait-il fonctionner?

+0

Bon point. Le fait est que ces spécifications héritent de 'SomeControllerContext', où l'installation principale se passe dans (dans un grand' Establish context =() => {} ') et le' session.Setup (...) 'se produit dans les deux spécifications ne peut pas être placé dans l'Etablissement principal car il interfère avec d'autres spécifications héritant de SomeControllerContext. Je suppose que ce que vous suggérez fonctionnerait si je faisais quelque chose comme: 'public class VoteSetup: SomeControllerContext {Establish ...}' et ensuite 'public class When_user_clicks_the_vote_down_button_on_a_post: VoteSetup {// spec}'. Est-ce que tous les établissements de la chaîne seront exécutés? –

+0

@Spapaseit, je ne suis pas un gourou MSpec (je ne l'ai jamais vraiment utilisé, mais je m'intéresse de plus en plus ...) mais il me semble que vous devriez pouvoir définir un champ privé (statique si besoin d'être) avec la valeur de l'expression lambda. Je vais éditer un exemple de code dans mon message ... –

1

@Tomas Lycken,

Je ne suis pas gourou MSpec soit, mais mon (pour l'instant limitée) une expérience pratique avec elle me conduit plus vers quelque chose comme ceci:

public abstract class SomeControllerContext 
{ 
    protected static SomeController controller; 
    protected static User user; 
    protected static ActionResult result; 
    protected static Mock<ISession> session; 
    protected static Post post; 

    Establish context =() => 
    { 
     session = new Mock<ISession>(); 
      // some more code 
    } 
} 

/* many other specs based on SomeControllerContext here */ 

[Subject(typeof(SomeController))] 
public abstract class VoteSetup : SomeControllerContext 
{ 
    Establish context =() => 
    { 
     post= PostFakes.VanillaPost(); 

     session.Setup(s => s.Single(Moq.It.IsAny<Expression<Func<Post, bool>>>())).Returns(post); 
     session.Setup(s => s.CommitChanges()); 
    }; 
} 

[Subject(typeof(SomeController))] 
public class When_user_clicks_the_vote_up_button_on_a_post : VoteSetup 
{ 
    Because of =() => result = controller.VoteUp(1); 

    It should_increment_the_votes_of_the_post_by_1 =() => post.Votes.ShouldEqual(11); 
    It should_not_let_the_user_vote_more_than_once; 
} 

[Subject(typeof(SomeController))] 
public class When_user_clicks_the_vote_down_button_on_a_post : VoteSetup 
{ 
    Because of =() => result = controller.VoteDown(1); 

    It should_decrement_the_votes_of_the_post_by_1 =() => post.Votes.ShouldEqual(9); 
    It should_not_let_the_user_vote_more_than_once; 
} 

Ce qui est fondamentalement ce que j'avais déjà mais en ajoutant des changements basés sur votre réponse (je n'ai pas eu la classe VoteSetup.)

Votre réponse m'a conduit dans la bonne direction. J'espère encore d'autres réponses pour rassembler d'autres points de vue sur le sujet ...:)

Questions connexes