简体   繁体   中英

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) . 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. I don't need to know what's the value of particular Cell outside this class. I just need an information if neighbor cells are empty.

1. How can I test Reset method?

Technically I should create a Table with mocked array of cells. Call Reset and then assert if every cell has empty 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? I've read that "It's not!", but Reset resets all cells, so I have to somehow check every cell.

EDIT: Option 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). With TDD you should write unit tests by using only public API of class under test.

When you test Reset method you should think how it affect on results of other public methods. 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.

For Reset method you need to set cells so that AreNeighborCellsSet returns true . Then execute Reset and assert that now AreNeighborCellsSet returns 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.

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.

The answer of Ignas my be a workaround for the problem but I feel a need to clarify some design issues here:

Basically there is no need to check if loop iterates through whole collection. That is tested by the framework team in MS. What you need to do is to check if your new type (in this case Cell ) behaves properly.

In my opinion you're violating the SRP. There is really no need for Table class to know how to reset this particular implementation of 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.

Start with abstracting Cell to an interface. Then just add method Reset() to the Cell and call it in the loop in Table class for every cell.

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 :-)

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:

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.

  • 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.
  • 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. Nuget package .

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.
  2. Ideally you want to have a single assert per test, if possible.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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