【问题标题】:Refactoring phase of the TDD Traffic Light - how to get this right?TDD 交通灯的重构阶段 - 如何正确处理?
【发布时间】:2010-07-25 07:51:19
【问题描述】:

所以我对即将诞生的 Board 类进行了以下测试:

[TestMethod]
public void Set_The_Origin_As_Violet_And_The_Query_Confirms_It() {
    Board board = new Board(10, 10);

    Color expected = Color.Violet;
    board.SetColorAt(0, 0, expected);
    Color actual = board.GetColorAt(0, 0);
    Assert.AreEqual(expected, actual);
}

我尝试运行代码,但编译器提示Board 不存在。所以我创建了它。

我尝试再次运行代码,但无济于事,因为 SetColorAt() 和 GetColorAt() 方法都不存在。我创建了它们:

public void SetColorAt(int x, int y, Color color) {
}

public void GetColorAt(int x, int y) {
}

不过,并非一切都很好,因为我必须返回 Color.Violet。所以我将 GetColorAt() 更改为

public void GetColorAt(int x, int y) {
    return Color.Violet;
}

所以我第一次得到了绿灯。

我希望我的最终代码出现在课堂板上是以下形式:

public class Board
{
    private Color[,] board;

    public Board(int x, int y)
    {
        board = new Color[x, y];
    }

    public void SetColorAt(int x, int y, Color color) {
        board[x, y] = color;
    }

    public Color GetColorAt(int x, int y) {
        return board[x, y];
    }
}

我的第一个问题是......

如何到达那里?我是否可以考虑在上面显示的单元测试的“重构”阶段,删除重复时我最终会得到这段代码?

如果答案是肯定的,我觉得我的单元测试正在测试一些过于“本地化”的东西,而不是代码的实际作用。你看,测试只是检查 1 个像素和一种颜色,而代码本身要复杂得多。

也许解决方案是添加更多单元测试?你会建议做哪个?

我的第二个问题是……

我知道以后我会想要一个IBoard。我应该在上面的单元测试中表达吗?我应该让

Board board = new Board(10, 10);

照原样创建IBoard 接口?如何处理?

【问题讨论】:

  • 问题是你从“这是正确的解决方案”开始。 TDD 是一种指导您进行良好设计的技术,而不是预先确定的设计。
  • 好吧,我明白你的意思了。但我无法想象除此之外的任何其他解决方案。您有一组 (x, y) 点,并且您希望将它们存储在二维数组中。这是实现这一目标的最简单方法。
  • :) - 就像我说的......你已经决定了你想要最终结果是什么......你需要写“最简单的想法”,然后重构以删除重复。
  • @Oded 我认为你的意思是“最简单的工作”:)
  • @Sekhat - 确实如此。我责怪我的胖手指。

标签: c# java unit-testing tdd


【解决方案1】:
public void GetColorAt(int x, int y) {
    return Color.Violet;
}

这里的return Color.Violet语句可以认为是数据重复,所以可以重构掉这个位。可能可行的最简单的事情是在调用 SetColorAt 时设置一个单独的 Color 值

public class Board
{
    private Color theColor;

    public Board(int x, int y)
    {
    }

    public void SetColorAt(int x, int y, Color color) {
        theColor = color;
    }

    public Color GetColorAt(int x, int y) {
        return theColor;
    }
}

现在,您需要进行更多测试,以证明您可以将单独的单元格设置为不同的颜色。

【讨论】:

  • 嗯,你是对的!但是让我们假设我现在做了第二个测试,它是一样的,但不是将它分配给 (0, 0),而是分配给 (1,1) 而不是紫罗兰色,黑色。最简单的方法是实现我原帖末尾的代码,对吧?
  • 就个人而言,我会编写一个测试,将几个单元格设置为不同的颜色(甚至所有单元格)并断言它们都是正确的。那么对我来说,最简单的事情就是原始帖子末尾的代码。
  • 而在现实生活中,你会一开始就做那个测试,还是先做一个简单的测试,然后再做一个更复杂的测试?
  • 当我做过类似的事情时,(虽然我在做康威生命游戏,所以细胞只能是活的或死的),我最终选择了一条类似于我们刚才描述的路线。我有两个方法 SetCellAlive、SetCellDead,我首先编写了一个测试,显示 SetCellAlive 和 SetCellDead 使 GetCellStatus 返回 true 或 false,然后我编写了一个测试以显示我可以将单独的单元格设置为不同的存活或死亡状态。我远不是 TDD 专家,但因为我遵循可能可行的最简单的方法,所以检查多个单元格的测试可以设置为不同的值作为需要的测试。
  • 如果需要,将 Board board = new Board(10,10) 更改为 IBoard board = new Board。您无法测试是否需要接口,因此您必须做出设计决定。尽管可能值得等到您最终创建第二个 Board 实现后再这样做(无论您是为测试还是为您的应用程序创建第二个实现)然后将您的测试更改为使用接口。这是跟随 YAGNI(你不会需要它),在你需要做某事之前不要做某事。
【解决方案2】:

您对测试模式 Arrange-Act-Assert 很熟悉,对吧?这就是你的测试所做的。

[TestMethod]
public void Set_The_Origin_As_Violet_And_The_Query_Confirms_It() {
    // Arrange
    Board board = new Board(10, 10);
    // Act
    board.SetColorAt(0, 0, expected);
    // Assert
    Color expected = Color.Violet;
    Assert.AreEqual(expected, board.GetColorAt(0, 0));
}

我喜欢使用稍微修改的形式,Arrange-AssertNot-Act-Assert。这个想法是我们验证行为本身是导致我们正在测试的条件的原因,因为我们在行为之前断言我们的条件没有得到满足。看起来是这样的:

[TestMethod]
public void Set_The_Origin_As_Violet_And_The_Query_Confirms_It() {
    // Arrange
    Board board = new Board(10, 10);
    // Assert
    Color expected = Color.Violet;
    Assert.AreNotEqual(expected, board.GetColorAt(0, 0));
    // Act
    board.SetColorAt(0, 0, expected);
    // Assert
    Assert.AreEqual(expected, board.GetColorAt(0, 0));
}

这迫使您在第一次编写一个稍微不那么简单的实现。您仍然需要额外的测试来驱动 GetColorAt() 的完整实现;为此,我觉得编写完整实现的时间比仅仅在另一个特殊情况下伪造(对于这个应用程序,我猜在 N=3 左右)更容易。

【讨论】:

  • 您修改后的模式看起来很不错。实际上,在一些测试中我已经这样做了,但现在我会尝试一直使用它。
【解决方案3】:

从您想要实现的内容的高级描述开始。

  • 一个有行列的方板。
  • 每个板子位置应该有一个 颜色。
  • 董事会用户应该能够 设置特定板的颜色 位置(像素)。
  • 董事会用户应该 能够得到a的颜色 具体董事会位置。

接下来,使用小型测试来推动实施。有时从边界条件开始会更容易。

When asked to get the color at a specific board position
- given a default board, and
  - the row value is less than zero
  - should throw argument exception

When asked to get the color at a specific board position
- given a default board, and
  - the row value is greater than the highest board row
  - should throw argument exception

“行值大于最高板行”迫使您有一些方法来设置最高板行。测试不关心你是通过构造函数还是属性设置器设置它。

When constructed with board dimensions
 - should set the highest board row

“应该设置最高的棋盘行”强制你有一些方法来访问设置的值。也许是一个属性获取器。

When asked to get the color at a specific board position
- given a default board, and
  - the column value is less than zero
  - should throw argument exception

When asked to get the color at a specific board position
- given a default board, and
  - the column value is greater than the highest board column
  - should throw argument exception

“列值大于最高板列”强制您为最高列复制最高行解决方案。

When constructed with board dimensions
 - should set the highest board column

等等

When asked to get the color at a specific board position
- given a default board, and
  - the row value is within the board space, and
  - the column is within the board space
  - should return the default color

“应该返回默认颜色”强制您在某处公开默认颜色。这不应该是只有测试知道的神奇值。在 Board 上将其公开为 const 或只读值。

When asked to get the color at a specific board position
- given a default board, and
  - the row value is within the board space, and
  - the column is within the board space, and
  - the requested position has a known color
  - should return the color of the board at the requested position

“请求的位置具有已知的颜色”迫使您建立设置棋盘位置颜色的能力。您现在可以这样做,因为您在某个位置获得颜色的测试失败。所以把它放在一边,然后构建 setter。

When asked to SET the color at a specific board position
- given a default board, and
  - the row value is less than zero
  - should throw argument exception

When asked to set the color at a specific board position
- given a default board, and
  - the row value is greater than the highest board row
  - should throw argument exception

When asked to set the color at a specific board position
- given a default board, and
  - the column value is less than zero
  - should throw argument exception

When asked to set the color at a specific board position
- given a default board, and
  - the column value is greater than the highest board column
  - should throw argument exception

When asked to set the color at a specific board position
- given a default board, and
  - the row value is within the board space, and
  - the column is within the board space
  - should set the color at the given position to the requested color

“应该将给定位置的颜色设置为请求的颜色”使用您的 get 实现来验证值是否设置正确。正确实施后,所有测试都将再次变为绿色。

继续一次一小部分地构建附加功能。

当您像这样以与实现无关的术语来处理测试时,您可以将实现更改为使用 IBoard 或 Ruby 或其他任何东西,并且不必更改测试描述。您必须更改一些测试实施细节,但测试的既定目标不必更改。

【讨论】:

    【解决方案4】:

    我认为你写了一个设置颜色并获得颜色的板子是过火了。是什么推动了董事会?什么需要电路板来存储颜色?板是颜色的存储库?

    董事会的实施细节太多。 IOWs,颜色是另一个对象的属性。您真的关心板存储颜色还是存储具有颜色属性的域对象。如果以后您也存储了过去的董事会职位历史怎么办。那么你打算使用两块板,一个是彩色板,一个是历史板。

    您确实需要将存储机制从实际依赖特定类型的颜色(紫色、蓝色、绿色等...)中抽象出来。

    从对问题域的理解开始的问题在于,它会使您的设计陷入困境。而且它不会那么健壮。

    对我来说,我会从一个问题开始:颜色是什么域对象的属性?然后我的测试会生成这个域对象。据我所知,域对象可能存在也可能不存在于板中。

    希望对你有所帮助。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-04-13
      • 1970-01-01
      • 1970-01-01
      • 2014-03-23
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多