如何对私有财产进行单元测试?

时间:2017-04-14 19:01:08

标签: c# .net unit-testing testing tdd

我对TDD很陌生,我很难理解如何测试类的私人成员(我知道!它是私有的,不应该测试 - 但请继续阅读)。我们可能有一个公共函数,它设置私有属性和其他公共函数,它们返回基于私有属性的“东西”。

让我告诉你一个基本的例子:

public class Cell
{
    public int X { get; set; }
    public int Y { get; set; }
    public string Value { get; set; }
}

public class Table
{
    private Cell[,] Cells { get; }
    public Table(Cell[,] cells)
    {
        Cells = cells;
    }

    public void SetCell(int x, int y, string value)
    {
        Cells[x, y].Value = value;
    }

    public void Reset()
    {
        for (int i = 0; i < Cells.GetLength(0); i++)
        {
            for (int j = 0; j < Cells.GetLength(1); j++)
            {
                Cells[i, j].Value = "";
            }
        }
    }

    public bool AreNeighborCellsSet(int x, int y)
    {
        bool areNeighborCellsSet = false;

        // checking...

        return areNeighborCellsSet;
    }
}

在此示例中,Cells是私有的,因为没有理由将它们公开。我不需要知道这个课程之外的特定Cell的价值是多少。如果相邻单元格为空,我只需要一个信息。

1。如何测试Reset方法?

从技术上讲,我应该使用模拟的单元格数组创建一个表。调用Reset然后断言每个单元格是否为空Value。但我实际上无法检查它们是否为空。

2。在这种情况下,我会多次拨打Assert(对于每个小区) - 这是一个好习惯吗?我读过“它不是!”,但重置会重置所有单元格,所以我必须以某种方式检查每个单元格。

编辑: 选项2:

public class Table
{
    private Cell[,] Cells { get; }

    public Table(int height, int width, ICellFactory cellFactory)
    {
        Cells = new ICell[height, width];
        for (int i = 0; i < Cells.GetLength(0); i++)
        {
            for (int j = 0; j < Cells.GetLength(1); j++)
            {
                Cells[i, j].Value = cellFactory.Create(i, j);
            }
        }
    }

    // Rest is the same...
}

4 个答案:

答案 0 :(得分:2)

你的班级有三种公共方法

void SetCell
void Reset
bool AreNeighborCellsSet

因此,只应通过这些方法并在构造函数输入参数的帮助下测试所有功能。

我担心你没有做TDD,因为你试图测试已经实现的逻辑(内部成员的for循环)。使用TDD,您应该仅使用被测类的公共API来编写单元测试。

当您测试Reset方法时,您应该考虑它如何影响其他公共方法的结果。 Table类只有一个返回我们可以观察到的值的方法 - bool AreNeighborCellsSet - 所以看起来这是我们可以执行断言的唯一方法。

对于Reset方法,您需要设置单元格,以便AreNeighborCellsSet返回true。然后执行Reset并声明现在AreNeighborCellsSet返回false。

[Test]
public void AfterResetGivenCellShouldNotHaveNeighbors()
{
    // Arrange
    var cell = new Cell { X = 1, Y = 1, Value = "central" };
    var neighborCell = new new Cell { X = 1, Y = 2, Value = "neighbor" };
    var table = new Table(new[] { cell, neighborCell });

    // table.AreNeighborCellsSet(cell.X, cell.Y) - should return true at this moment
    // Act
    table.Reset();

    // Assert
    table.AreNeighborCellsSet(cell.X, cell.Y).Should().BeFalse();
}

这是TDD(测试驱动开发)的一个很好的例子,测试问题是设计出现问题的好迹象。

实际上,我认为,在您的情况下,您根本不需要Reset方法 - 只需在每次需要重置时创建Table的新实例。

答案 1 :(得分:1)

Ignas的答案是解决问题的方法,但我觉得有必要澄清一些设计问题:

基本上没有必要检查循环是否遍历整个集合。这是由MS的框架团队测试的。 您需要做的是检查您的新类型(在这种情况下为Cell)是否正常运行。

在我看来,你违反了SRP。 Table类实际上不需要知道如何重置Cell的这个特定实现。如果有一天你决定创建一个能够包含图片的单元格,那么你很可能会觉得需要以其他方式清除它,而不是将空字符串设置为它的Value属性。

首先将Cell抽象为接口。然后只需将方法Reset()添加到Cell,并在Table类的循环中为每个单元格调用它。

这将允许您为Cell的实现创建测试,并且您可以检查在调用Reset()单元格的值后是否真正变为空或空或者您需要的任何内容: - )

答案 2 :(得分:1)

有一些方法可以测试私有属性而无需更改代码或向测试类添加额外代码,您可以使用允许您这样做的测试工具。
例如,我使用Typemock更改表c的逻辑来创建一个填充表,并在调用reset方法后获取私有属性Cells:

public void TestMethod1()
{
    var handle = Isolate.Fake.NextInstance<Table>(Members.CallOriginal, context =>
    {
        var tempcells = context.Parameters[0] as Cell[,];

        for (int i = 0; i < tempcells.GetLength(0); i++)
        {
            for (int j = 0; j < tempcells.GetLength(1); j++)
            {
                tempcells[i, j] = cellFactory.Create(i, j);
            }
        }
        context.Parameters[0] = tempcells;

        //calling the original ctor with tempcells as the parameter
        context.WillCallOriginal();
    });

    // calling the ctor with the custom logic
    var testTable = new Table(new Cell[2,2]);


    testTable.Reset();
    // calling the private property
    var resTable = Isolate.Invoke.Method(testTable, "get_Cells") as Cell[,];

    // for asserting
    var emptyCell = new Cell { Value = string.Empty };
    for (int i = 0; i < 2; i++)
    {
        for(int j=0; j<2; j++)
        {
            Assert.AreEqual(emptyCell.Value, resTable[i, j].Value);
        }
    }
}

答案 3 :(得分:0)

正如Zegar在评论中提到的那样,可能存在一些代码设计注意事项,并且可能首先使用TDD编写测试也有助于不会遇到这种情况,但我认为还有一个简单的解决方法。

  • 您将数组作为引用传递到Table类而不是覆盖它,因此您可以访问Table类之外的数组,即使您正在其中修改它类。
  • 你不需要做很多断言,你只需要为断言安排一个预期的数组。使用FluentAssertions,特别是ShouldBeEquivalentTo(),这是一个非常好的数组比较解决方案。 Nuget package

以下样本测试。

    [TestMethod]
    public void TestMethod1()
    {
        // Arrange
        var expectedCells = new Cell[2, 2];
        expectedCells[0, 0] = new Cell { Value = string.Empty };
        expectedCells[0, 1] = new Cell { Value = string.Empty };
        expectedCells[1, 0] = new Cell { Value = string.Empty };
        expectedCells[1, 1] = new Cell { Value = string.Empty };

        var cells = new Cell[2,2];
        cells[0,0] = new Cell { Value = "00" };
        cells[0,1] = new Cell { Value = "01" };
        cells[1,0] = new Cell { Value = "10" };
        cells[1,1] = new Cell { Value = "11" };
        var table = new Table(cells);

        // Act
        table.Reset();

        // Assert
        cells.ShouldBeEquivalentTo(expectedCells); // using FluentAssertions 
    }

总结并回答您的问题。

  1. 测试传递给构造函数的cells数组。
  2. 理想情况下,如果可能,您希望每次测试都有一个断言。