Dry-ing具有MSPEC的ASP.NET MVC控制器作用非常相似的规格(BDD指南)
-
26-09-2019 - |
题
对于两个非常相似的控制器操作,我有两个非常相似的规格:投票(INT ID)和投票(INT ID)。这些方法允许用户向上或向下投票; Kinda类似于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;
}
所以我有两个问题:
这两个规格我应该如何干燥?是否建议,或者我应该每个控制器操作实际上有一个规格?我知道我通常应该这样做,但这感觉就像重复了很多。
有什么方法可以实施第二个
It
在同一规格中?请注意It should_not_let_the_user_vote_more_than_once;
需要我的规格要打电话controller.VoteDown(1)
两次。我知道最简单的是为其创建一个单独的规格,但是它会复制和粘贴相同的代码 再次...
我仍然可以掌握BDD(和MSPEC)的掌握,很多时候尚不清楚我应该走哪种方式,或者BDD的最佳实践或指南是什么。任何帮助,将不胜感激。
解决方案
我将从您的第二个问题开始: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().
其他提示
@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
班级。)
您的答案使我朝着正确的方向发展。我仍然希望有更多答案来收集有关该主题的其他观点... :)
您可能会考虑出来的大部分重复 设置 测试。没有真正的理由将UPVote Spec从0投票而不是10投票到11到11,因此您可以拥有一个单一的设置例程。仅此单独的测试将在3行代码(如果需要手动调用设置方法...)上留下两行测试。
突然,您的测试仅包括执行操作并验证结果。而且,无论是否感觉重复,我都会强烈建议您每次测试测试一件事,仅仅是因为您想确切地知道,当您在一个月内重构某件事并运行解决方案中的所有测试时,测试会失败。
更新 (有关详细信息,请参见评论)
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;
...
这个可以工作吗?