Search code examples
c#asp.net-mvcbddmspec

DRY-ing very similar specs for ASP.NET MVC controller action with MSpec (BDD guidelines)


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.


Solution

  • 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().