我喜欢mspec。非常适合提供易于与非技术人员沟通的关键示例,但有时我发现它提供了不必要的冗长,特别是类的爆炸。
采用以下示例。
我想用国际象棋模拟骑士棋子的动作。假设骑士不在任何其他棋子或棋盘边界附近,骑士可以拥有8种可能的动作,我想要涵盖这些可能性,但坦率地说,我懒得写8个单独的规格(8个等级)。我知道我可以聪明地处理行为和继承但是因为我想要覆盖8个有效的动作,我看不出我怎么能用8 because
来做,所以因此有8个单独的类。
使用mspec覆盖这些场景的最佳方法是什么?
一些代码。
public class Knight
{
public string Position {get; private set;}
public Knight(string startposition)
{
Position = startposition;
}
public void Move
{
// some logic in here that allows a valid move pattern and sets positions
}
}
我可能会做什么。
[Subject(typeof(Knight),"Valid movement")]
public class when_moving_the_knight
{
Establish that = () => knight =new Knight("D4");
Because of = ()=> knight.Move("B3");
It should_update_position = ()=> knight.Position.ShouldEqual("B3");
It should_not_throw;
/// etc..
}
但不是8次。
答案 0 :(得分:3)
老实说,我无法告诉你在MSpec中做到这一点的最好方法。但是在类似情况下使用它时,我遇到了与MSpec类似的类爆炸问题。我不知道你是否曾尝试过RSpec。在RSpec中,上下文和规范是在可执行代码的范围内构建的。这意味着您可以创建数据结构,迭代它,并使用一个代码块创建多个上下文和规范。当你试图指明基于数学的东西如何表现时(主要因素,tic tac toe,象棋等等),这变得特别方便。可以在一组给定值和期望值的每个成员中指定单个行为模式。
这个例子是用NSpec编写的,这是一个在RSpec之后建模的C#的上下文/规范框架。我有目的地留下了一个失败的规范。我刚刚离开这个卡塔足够远,找到一个使用迭代的地方。失败的规范迫使你解决天真实现的缺点。
这是素数因子kata的另一个例子:http://nspec.org/#dolambda
输出:
describe Knight
when moving 2 back and 1 left
when a knight at D4 is moved to B3
knight position should be B3
when a knight at C4 is moved to A3
knight position should be A3 - FAILED - String lengths are both 2. Strings differ at index 0., Expected: "A3", But was: "B3", -----------^
**** FAILURES ****
describe Knight. when moving 2 back and 1 left. when a knight at C4 is moved to A3. knight position should be A3.
String lengths are both 2. Strings differ at index 0., Expected: "A3", But was: "B3", -----------^
at ChessSpecs.describe_Knight.<>c__DisplayClass5.<when_moving_2_back_and_1_left>b__4() in c:\Users\matt\Documents\Visual Studio 2010\Projects\ChessSpecs\ChessSpecs\describe_Knight.cs:line 23
2 Examples, 1 Failed, 0 Pending
代码:
using System.Collections.Generic;
using NSpec;
class describe_Knight : nspec
{
void when_moving_2_back_and_1_left()
{
new Each<string,string> {
{"D4", "B3"},
{"C4", "A3"},
}.Do( (start, moveTo) =>
{
context["when a knight at {0} is moved to {1}".With(start,moveTo)] = () =>
{
before = () =>
{
knight = new Knight(start);
knight.Move(moveTo);
};
it["knight position should be {0}".With(moveTo)] = () => knight.Position.should_be(moveTo);
};
});
}
Knight knight;
}
class Knight
{
public Knight(string position)
{
Position = position;
}
public void Move(string position)
{
Position = "B3";
}
public string Position { get; set; }
}
答案 1 :(得分:1)
只需按照您想要的方式使用它。它应该能够从这里移动到那里,它应该能够从这里(2)移动到那里(2)等。在rspec中非常常见的模式,但在MSpec中没那么多,因为它通常被过度使用所以没有人会谈关于它,因为害怕指导错误的方式。这是一个使用它的好地方。你正在描述骑士移动的行为。
你可以通过更具体地描述它来描述它。它应该能够向上移动两个向右移动,它应该能够向上移动两个向左移动。它不应该能够转移到友好的作品等上。
是的,你需要在你的It中放置多行代码,但这没关系。至少在我看来。
答案 2 :(得分:1)
从我看到你的设计说明,如果移动到无效位置,骑士会抛出异常。在这种情况下,我认为你的方法有两个不同的职责,一个用于检查有效的移动,另一个用于执行正确的移动或投掷。我建议将你的方法分成两个不同的职责。
对于这种特定情况,我将提取一种方法来检查移动是否有效,然后从移动方法中调用它。这样的事情:
public class Knight
{
internal bool CanMove(string position)
{
// Positioning logic here which returns true or false
}
public void Move(string position)
{
if(CanMove(position))
// Actual code for move
else
// Throw an exception or whatever
}
}
这样你可以测试CanMove中的逻辑来测试给定Knight的有效位置(你可以用一个测试类和不同的“It”来做),然后只为Move方法做一个测试,看看是否给定无效职位时失败。