【问题标题】:DRY-ing very similar specs for ASP.NET MVC controller action with MSpec (BDD guidelines)使用 MSpec 干燥非常相似的 ASP.NET MVC 控制器操作规范(BDD 指南)
【发布时间】:2010-05-14 13:26:50
【问题描述】:

对于两个非常相似的控制器操作,我有两个非常相似的规范:VoteUp(int id) 和 VoteDown(int id)。这些方法允许用户向上或向下投票;有点像 StackOverflow 问题的投票赞成/反对票功能。规格是:

投票:

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

投票:

[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. 我应该如何干燥这两个规格?它甚至是可取的,还是我实际上应该为每个控制器操作提供一个规范?我知道我通常应该这样做,但这感觉像是在重复自己。

  2. 有没有办法在同一规范中实现第二个It?请注意,It should_not_let_the_user_vote_more_than_once; 要求我调用 controller.VoteDown(1) 两次。我知道最简单的方法也是为它创建一个单独的规范,但它会复制和粘贴相同的代码再次...

我仍然掌握 BDD(和 MSpec)的窍门,而且很多时候不清楚我应该走哪条路,或者 BDD 的最佳实践或指南是什么。任何帮助将不胜感激。

【问题讨论】:

    标签: c# asp.net-mvc bdd mspec


    【解决方案1】:

    我将从您的第二个问题开始: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

    我建议不要使用行为,因为您的示例实际上包含四个上下文。当我从“业务意义”的角度考虑您试图用代码表达的内容时,出现了四种不同的情况:

    • 用户首次投票
    • 用户第一次投反对票
    • 用户第二次投票
    • 用户第二次投反对票

    对于四种不同场景中的每一种,我都会创建一个单独的上下文来详细描述系统的行为方式。四个上下文类是很多重复代码,这就引出了您的第一个问题。

    在下面的“模板”中,有一个基类,其中的方法具有描述性名称,说明调用它们时会发生什么。因此,不要依赖于 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().
    

    【讨论】:

    • 再次感谢。我知道行为(确实来自 de MSpec 源代码示例),但感觉就像我不得不将它硬塞到我的场景中;感觉不是很自然。精彩的答案,感谢一百万。
    【解决方案2】:

    @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 类。)

    您的回答使我朝着正确的方向前进。我仍然希望有更多的答案来收集关于这个主题的其他观点...... :)

    【讨论】:

      【解决方案3】:

      您可能只需排除测试的设置即可排除大部分重复。没有真正的理由为什么 upvote 规范应该从 0 到 1 票而不是 10 到 11 票,所以你可以很好地拥有一个单一的设置例程。仅此一项就可以将两个测试留在 3 行代码中(或者 4 行,如果您需要手动调用 setup 方法......)。

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

      更新(详见 cmets)

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

      这行得通吗?

      【讨论】:

      • 好点。问题是,这些规范继承自 SomeControllerContext,其中主要设置发生在(在一个大的 Establish context = () =&gt; {} 中)并且两个规范中发生的 session.Setup(...) 不能放在主建立中,因为它会干扰其他规范继承来自 SomeControllerContext。我猜如果我执行以下操作,您的建议会起作用:public class VoteSetup : SomeControllerContext { Establish ...},然后是public class When_user_clicks_the_vote_down_button_on_a_post : VoteSetup { //spec }。链上的所有Establish都会被执行吗?
      • @Spapaseit,我不是 MSpec 大师(我从未真正使用过它,但我越来越感兴趣......)但在我看来,你应该能够使用 lambda 表达式的值定义一个私有字段(如果需要,则为静态)。我将在我的帖子中编辑一个代码示例...
      猜你喜欢
      • 1970-01-01
      • 2023-02-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-12-03
      相关资源
      最近更新 更多