使用MSpec(BDD指南)干燥ASP.NET MVC控制器操作的非常类似的规范

时间:2010-05-14 13:26:51

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

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

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

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

  3. 我仍然掌握着BDD(以及MSpec),很多时候我不知道应该采用哪种方式,或者BDD的最佳实践或指南是什么。任何帮助将不胜感激。

3 个答案:

答案 0 :(得分: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

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

  • 用户第一次投票
  • 用户第一次投票
  • 用户第二次投票
  • 用户第二次投票

对于四种不同场景中的每一种,我都会创建一个单独的上下文,用于描述系统的行为方式。四个上下文类是很多重复的代码,这将我们带到你的第一个问题。

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

答案 1 :(得分: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类。)

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

答案 2 :(得分:0)

您可以通过分解测试的设置来分解大部分重复。没有任何真正的理由为什么upvote规范应该从0投票到1投票而不是10到11投票,所以你很可能只有一个设置例程。仅这一点就可以在3行代码中进行测试(如果需要手动调用设置方法,则为4行)。

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

更新(详见评论)

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

这可行吗?