简体   繁体   English

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

[英]How to unit test private properties?

I'm pretty new to TDD and I have a hard time to understand how to test private members of the class (I know! It's private, shouldn't be tested - but please keep reading) . 我是TDD的新手,我很难理解如何测试班级的私有成员(我知道!它是私有的,不应该测试-但请继续阅读) We might have a public function which sets private property and other public function that returns "something" based on that private property. 我们可能有一个公共函数来设置私有财产,而其他公共函数会基于该私有财产返回“某物”。

Let me show you a basic example: 让我向您展示一个基本示例:

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;
    }
}

In this example Cells are private, because there's no reason to make them public. 在此示例中, Cells是私有的,因为没有理由将它们公开。 I don't need to know what's the value of particular Cell outside this class. 我不需要知道此类以外的特定Cell的值是什么。 I just need an information if neighbor cells are empty. 如果邻居单元格为空,我只需要一个信息。

1. How can I test Reset method? 1.如何测试Reset方法?

Technically I should create a Table with mocked array of cells. 从技术上讲,我应该使用模拟的单元格数组创建表。 Call Reset and then assert if every cell has empty Value . 调用Reset ,然后断言每个单元格的Value是否为空。 But I can't actually check if they are empty or not. 但是我实际上无法检查它们是否为空。

2. In this case I would call Assert many times (for every cell) - is it a good practice? 2.在这种情况下,我会(对于每个单元格)多次调用Assert -这是一个好习惯吗? I've read that "It's not!", but Reset resets all cells, so I have to somehow check every cell. 我读过“不是!”,但是“重置”会重置所有单元格,因此我必须以某种方式检查每个单元格。

EDIT: Option 2: 编辑:选项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...
}

Your class have three public methods 您的课程有三种公开方法

void SetCell
void Reset
bool AreNeighborCellsSet

So all functionality should be tested only through those methods and with possible help of constructor input arguments. 因此,应仅通过这些方法并在构造函数输入参数的可能帮助下测试所有功能。

I am afraid you are not doing TDD, because you are trying to test already implemented logic ( for loop of internal member). 恐怕您没有执行TDD,因为您正在尝试测试已实现的逻辑( for内部成员的循环)。 With TDD you should write unit tests by using only public API of class under test. 使用TDD时,您应仅使用被测类的公共API编写单元测试。

When you test Reset method you should think how it affect on results of other public methods. 在测试Reset方法时,您应该考虑它如何影响其他公共方法的结果。 Table class has only one method which return some value we can observe - bool AreNeighborCellsSet - so seems like this is the only method against which we can execute our asserts. Table类只有一个返回一些我们可以观察到的值的方法bool AreNeighborCellsSet因此看来这是唯一可以执行断言的方法。

For Reset method you need to set cells so that AreNeighborCellsSet returns true . 对于Reset方法,您需要设置单元格,以便AreNeighborCellsSet返回true Then execute Reset and assert that now AreNeighborCellsSet returns false. 然后执行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();
}

This is a good example of TDD (Test-Driven Development), where problems with testing is good sign that something wrong with design. 这是TDD(测试驱动开发)的一个很好的例子,其中测试问题表明设计存在问题。

Actually, I think, in your case you don't need Reset method at all - just create a new instance of Table every time you need to reset it. 实际上,我认为,就您而言,您根本不需要Reset方法-每次需要重设Table时,只需创建一个新的Table实例即可。

The answer of Ignas my be a workaround for the problem but I feel a need to clarify some design issues here: Ignas的答案是解决该问题的方法,但我认为有必要在此澄清一些设计问题:

Basically there is no need to check if loop iterates through whole collection. 基本上不需要检查循环是否遍历整个集合。 That is tested by the framework team in MS. 这是由MS中的框架团队测试的。 What you need to do is to check if your new type (in this case Cell ) behaves properly. 您需要做的是检查您的新类型(在本例中为Cell )是否运行正常。

In my opinion you're violating the SRP. 我认为您违反了SRP。 There is really no need for Table class to know how to reset this particular implementation of Cell . 实际上, Table类不需要知道如何重置Cell特定实现。 If some day you decide to create a cell able to contain a picture let's say, you'll most likely feel a need to clear it in some other way than by setting an empty string to it's Value property. 假设有一天,如果您决定创建一个能够包含图片的单元格,则很可能需要以其他方式清除它,而不是为其Value属性设置一个空字符串。

Start with abstracting Cell to an interface. 从将Cell抽象到接口开始。 Then just add method Reset() to the Cell and call it in the loop in Table class for every cell. 然后只需将方法Reset()添加到Cell并在Table类的循环中为每个单元调用它。

That would allow you to create tests for your implementation of Cell and there you can check if after calling Reset() cell's value truly becomes null or empty or whatever you need :-) 这将允许您为Cell的实现创建测试,然后可以在调用Reset()之后检查是否将cell的值真正变为null或为空,或者您需要什么:-)

There are ways to test private properties with no need for changing your code or adding extra code to your tested class, you can use testing tools that allows you to do so. 有多种方法可以测试私有属性,而无需更改代码或向测试的类添加额外的代码,您可以使用允许您这样做的测试工具。
for example i used Typemock to change the logic of the Table c'tor to create a populated table and to get the private property Cells after calling the reset method: 例如,在调用reset方法后,我使用Typemock更改了Table c'tor的逻辑以创建填充表并获取私有属性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);
        }
    }
}

As Zegar mentioned in comments there could be several code design considerations, and probably writing tests first aka using TDD would help not to even run into such situations, however I think there is a simple workaround as well. 正如Zegar在评论中提到的那样,可能有一些代码设计方面的考虑,并且可能首先使用TDD编写测试也可能不会出现这种情况,但是我认为也有一个简单的解决方法。

  • You are passing array as reference into Table class and not overriding it, therefore you can access the array outside of Table class even though you are modifying it inside the class. 您正在将数组作为引用传递到Table类中,而不是覆盖它,因此,即使您在类中进行了修改,也可以在Table类之外访问该数组。
  • You don't need to do many asserts, you just need to arrange an expected array for assertion. 您不需要执行许多断言,只需要安排一个预期的数组进行断言即可。 Use FluentAssertions and specifically ShouldBeEquivalentTo() which is a very nice solution for arrays comparison. 使用FluentAssertions ,特别是ShouldBeEquivalentTo() ,这是一个很好的数组比较解决方案。 Nuget package . Nuget包

Sample test below. 下面的样本测试。

    [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 
    }

To summarize and answer your questions. 总结并回答您的问题。

  1. Test cells array you pass into the constructor. 测试cells数组传递给构造函数。
  2. Ideally you want to have a single assert per test, if possible. 理想情况下,如果可能的话,您希望每个测试有一个断言。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM