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:
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.
Is there any way to implement the second
It
within the same spec? Note that theIt should_not_let_the_user_vote_more_than_once;
requires me the spec to callcontroller.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.
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().
@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... :)
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?
来源:https://stackoverflow.com/questions/2834461/dry-ing-very-similar-specs-for-asp-net-mvc-controller-action-with-mspec-bdd-gui