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

发布于 2024-09-01 17:03:46 字数 1833 浏览 3 评论 0原文

对于两个非常相似的控制器操作,我有两个非常相似的规范: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) 两次。我知道最简单的方法也是为其创建一个单独的规范,但它会再次复制并粘贴相同的代码...

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

I have two very similar specs for two very similar controller actions: VoteUp(int id) and VoteDown(int id). These methods allow a user to vote a post up or down; kinda like the vote up/down functionality for StackOverflow questions. The specs are:

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

So I have two questions:

  1. How should I go about DRY-ing these two specs? Is it even advisable or should I actually have one spec per controller action? I know I Normally should, but this feels like repeating myself a lot.

  2. Is there any way to implement the second It within the same spec? Note that the It should_not_let_the_user_vote_more_than_once; requires me the spec to call controller.VoteDown(1) twice. I know the easiest would be to create a separate spec for it too, but it'd be copying and pasting the same code yet again...

I'm still getting the hang of BDD (and MSpec) and many times it is not clear which way I should go, or what the best practices or guidelines for BDD are. Any help would be appreciated.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

冷月断魂刀 2024-09-08 17:03:46

我将从你的第二个问题开始: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();
}

您想要在行为类中断言的任何字段都需要在行为类和上下文类中受静态保护。 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().

I'll start with your second question: There is a feature in MSpec that would help with the duplication of the It fields, but in this scenario I would advise against using it. The feature is called Behaviors and goes something like this:

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

Any fields you want to assert on in the behavior class need to be protected static in both the behavior and the context class. The MSpec source code contains another example.

I advise against using behaviors because your example actually contains four contexts. When I think about what you're trying to express with the code in terms of "business meaning", four different cases emerge:

  • User votes up for the first time
  • User votes down for the first time
  • User votes up for the second time
  • User votes down for the second time

For each of the four different scenarios I would create a separate context that closely describes how the system should behave. Four context classes are a lot of duplicate code, which brings us to your first question.

In the "template" below there is one base class with methods that have descriptive names of what will happen when you call them. So instead of relying on the fact that MSpec will call "inherited" Because fields automatically, you put information on what's important to the context right in the Establish. From my experience this will help you a lot later when you read a spec in case it is failing. Instead of navigating a class hierarchy you immediately get a feeling for the setup that takes place.

On a related note, the second advantage is that you only need one base class, no matter how many different contexts with specific setup you derive.

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().
痴情换悲伤 2024-09-08 17:03:46

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

您的答案引导我走向正确的方向。我仍然希望得到更多答案来收集有关该主题的其他观点......:)

@Tomas Lycken,

I'm no MSpec guru either, but my (as of yet limited) practical experience with it leads me more towards something more like this:

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

Which is basically what I already had but adding changes based on your answer (I didn't have the VoteSetup class.)

Your answer has lead me in the right direction. I'm still hoping for some more answers to gather other points of view on the subject... :)

演多会厌 2024-09-08 17:03:46

您可能可以通过仅分解测试的设置来分解大部分重复。没有真正的理由说明为什么赞成票规范应该从 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;
...

这可以工作吗?

You could probably factor out much of the repetition by just factoring out the setup of the tests. There is no real reason why the upvote spec should go from 0 to 1 vote rather than 10 to 11, so you can very well have one single setup routine. That alone will leave both test at 3 lines of code (or 4, if you need to call the setup method manually...).

Suddenly, your tests consist only of executing the action, and verifying the results. And whether it feels repetitive or not, I would strongly advise that you test one thing per test, simply because you want to know exactly why a test fails when you refactor something in a month and run all the tests in the solution.

UPDATE (see comments for details)

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

And in your specification:

Establish context = vote_count_context;
...

Could this work?

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文