2

我喜欢 mspec。它非常适合以易于与非技术人员交流的方式提供关键示例,但有时我发现它提供了不必要的冗长,特别是类的爆炸式增长。

举个例子。

我想模拟国际象棋中骑士棋子的运动。假设马不在任何其他棋子或棋盘边界附近,马有 8 种可能的移动方式,我想涵盖每一种可能性,但坦率地说,我懒得写 8 个单独的规范(8 个类)。我知道我可以很聪明地处理行为和继承,但是因为我想涵盖 8 个有效动作,所以我看不出没有 8because秒我怎么能做到,因此有 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次。

4

3 回答 3

3

老实说,我无法告诉你在 MSpec 中做到这一点的最佳方法。但是在类似情况下使用 MSpec 时,我遇到了类似的类爆炸问题。我不知道你是否曾经尝试过 RSpec。在 RSpec 中,上下文和规范是在可执行代码的范围内构建的。这意味着您可以创建一个数据结构,对其进行迭代,并使用一个代码块创建多个上下文和规范。当您尝试指定基于数学的事物的行为方式(质数因子、井字游戏、国际象棋等)时,这变得特别方便。可以为一组给定值和期望值的每个成员指定单一的行为模式。

这个例子是用 NSpec 编写的,它是一个以 RSpec 为模型的 C# 上下文/规范框架。我故意留下了一个失败的规范。我刚刚深入了解了这个 kata,找到了一个使用迭代的地方。失败的规范迫使您解决幼稚实现的缺点。

这是素数 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; }
}
于 2011-04-30T02:58:40.913 回答
1

只需按照您想要的方式使用它。它应该能够从这里移动到那里,它应该能够从这里(2)移动到那里(2),等等。在 rspec 中非常常见的模式,但在 MSpec 中并不多,因为它通常被过度使用,所以没有人谈论关于它,因为害怕引导错误的方式。不过,这是使用它的好地方。您正在描述骑士移动的行为。

您可以通过更具体地描述它来更好地描述它。它应该能够向上移动两个并向右移动一个,它应该能够向上移动两个向左移动。它应该不能移动到友方棋子上,等等。

是的,您需要在您的 It 中放置多行代码,但这没关系。至少在我看来。

于 2011-04-27T02:56:29.773 回答
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 方法进行一次测试,看看它是否失败给定一个无效的位置。

于 2011-04-27T07:18:45.197 回答