5

对于两个非常相似的控制器操作,我有两个非常相似的规范: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 的最佳实践或指导方针是什么。任何帮助,将不胜感激。

4

3 回答 3

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 源代码包含另一个示例

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

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

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

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

@托马斯·莱肯,

我也不是 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上课。)

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

于 2010-05-14T18:38:41.920 回答
0

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

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

更新(详情见评论)

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

这能行吗?

于 2010-05-14T13:40:55.533 回答