2010-05-14 23 views
5

对于两个非常类似的控制器操作,我有两个非常相似的规格:VoteUp(int id)和VoteDown(int id)。这些方法允许用户向上或向下投票;有点像StackOverflow问题的投票上/下功能。该规范是:使用MSpec(BDD准则)干燥非常类似的ASP.NET MVC控制器操作规范

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; 
} 

所以我有两个问题:

  1. 我应该如何去DRY-ING这两种规格?这是甚至可取的,或者我应该每个控制器操作实际上有一个规格?我知道我通常应该,但是这感觉好像重复了很多。

  2. 有没有什么办法可以在同一规范内实现第二个It?请注意,It should_not_let_the_user_vote_more_than_once;要求我规范拨打controller.VoteDown(1)两次。我知道最简单的是为它创建一个单独的规范太多,但它会被复制并再次粘贴相同的代码...

我仍然得到BDD的窍门(和MSpec),很多时候我不清楚应该去哪个方向,或者BDD的最佳实践或指导方针。任何帮助,将不胜感激。

回答

8

我将从第二个问题开始:MSpec中有一项功能可以帮助复制It字段,但在这种情况下,我建议不要使用它。该功能被称为行为和是这样的:

[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(); 
} 

要在行为类主张上的任何字段需要是protected static的行为和上下文类两种。 MSpec源代码包含another example

我建议不要使用行为,因为您的示例实际上包含四个上下文。当我想你想用的“商业意义”方面的代码表达什么,四种不同的情况下出现:

  • 用户投票了首次
  • 用户投票下来,第一次
  • 用户投票了第二次
  • 用户投票下来,第二次

对于每四个不同的场景我会创建一个单独的背景下,准确地描述系统应该如何behav即四个上下文类是很多重复的代码,这将我们带到你的第一个问题。

在下面的“模板”中,有一个基类,其中包含描述性名称的方法,当您调用它们时会发生什么。因此,您不必依靠MSpec自动调用“继承”Because字段这一事实,而是在Establish中提供有关上下文重要内容的信息。根据我的经验,如果您在阅读规范时遇到问题,那么这将对您有所帮助。您不需要导航类层次结构,而是立即感受到发生的设置。

在相关说明中,第二个优点是您只需要一个基类,而不管具有特定设置的多少个不同上下文。

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

再次感谢。我知道行为(实际上来自MSpec源代码示例),但感觉就像我不得不把它放到我的场景中;它并不觉得自然合适。非常好的答案,谢谢。 – 2010-05-16 10:52:34

0

你可能通过分解测试的设置来分解大部分重复。 upvote规范应该从0到1投票而不是10到11,没有真正的理由,所以你可以有一个单一的安装例程。仅此一项就会在三行代码(或4,如果您需要手动调用安装方法...)进行测试。

突然,您的测试仅包含执行操作和验证结果。无论是否感觉重复,我强烈建议您在每次测试中测试一件事情,因为您想知道为什么当您在一个月内重构某些内容并在解决方案中运行所有测试时测试失败。

UPDATE(见注释详细信息)

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()); 
}; 

而在你的规格:

Establish context = vote_count_context; 
... 

莫非这项工作?

+0

好点。问题是,这些规范继承自'SomeControllerContext',其中主设置发生在两个规范中(在一个大的'Establish context =()=> {}')和'session.Setup(...)不能放在主要的建立中,因为它会干扰从SomeControllerContext继承的其他规格。我想如果我做了类似于'public class VoteSetup:SomeControllerContext {Establish ...}'然后'public class When_user_clicks_the_vote_down_button_on_a_post:VoteSetup {// spec}'的话,你的建议就会起作用。链中的所有建立都会被执行吗? – 2010-05-14 13:54:57

+0

@Spapaseit,我不是MSpec大师(我从来没有真正使用它,但我越来越感兴趣...)但在我看来,你应该能够定义一个私人领域(静态如果需要)与lambda表达式的值。我将编辑一个代码示例到我的帖子中... – 2010-05-14 17:04:26

1

@Tomas Lycken,

我没有MSpec大师下去,但我(作为尚未有限公司)与它的实际经验使我更多地转向更多的东西是这样的:

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; 
} 

哪个基本上我已经有了,但根据您的答案添加更改(我没有VoteSetup课程。)

您的回答引导我走向正确的方向。我仍然希望能有更多的答案来收集关于这个问题的其他观点......:)