如何对改变私有字段引用的方法进行单元测试

时间:2019-04-20 14:54:43

标签: c# .net unit-testing

我正在为生活应用程序中的一个类编写单元测试。我有一个方法,该方法创建一个二维的布尔数组,然后将字段(另一个二维的布尔数组)的引用更改为新数组。

我在单元课上遇到了麻烦。我正在尝试测试输出是否正确,但是,在更改引用后,我无法访问该数组。

我发现了一种解决方法,不是用新数组的内容来循环填充原始字段,而不是创建新数组并分配新的引用,但这只是为数组添加了不必要的计算。出于单元测试的考虑,这听起来不是一个好主意。

如何测试Evolve方法的正确行为?

Board.cs

public class Board : IBoard
{
    private bool[,] _board;
    private int _noRows;
    private int _noColumns;
    private IConsole _console;

    public Board(IConsole console)
    {
        _console = console;
    }

    public void Set(bool[,] board)
    {
        _board = board;
        _noRows = board.GetLength(0);
        _noColumns = board.GetLength(1);
    }

    private IEnumerable<bool> GetNeighbours(int boardTileY, int boardTileX)
    {
        var neighbours = new List<bool>();

        for (var i = boardTileY - 1; i <= boardTileY + 1; i++)
        {
            for (var j = boardTileX - 1; j <= boardTileX + 1; j++)
            {
                if (i == boardTileY && j == boardTileX)
                {
                    continue;
                }
                //if neighbour out of bounds add as dead
                else if (i >= _noRows || i < 0 || j >= _noColumns || j < 0)
                {
                    neighbours.Add(false);
                }
                else
                {
                    neighbours.Add(_board[i, j]);
                }
            }
        }

        return neighbours;
    }

    public void Evolve()
    {
        var boardAfter = new bool[_noRows, _noColumns];

        for (var i = 0; i < _noRows; i++)
        {
            for (var j = 0; j < _noColumns; j++)
            {
                var aliveCounter = GetNeighbours(i, j).Count(n => n);

                switch (_board[i, j])
                {
                    // if dead tile has exactly 3 neighbours that are alive it comes to life
                    case false when aliveCounter == 3:
                        boardAfter[i, j] = true;
                        break;

                    // if alive tile has 0 or 1 neighbours (is lonely) or more than 3 neighbours (overcrowded) it dies
                    case true when (aliveCounter < 2 || aliveCounter > 3):
                        boardAfter[i, j] = false;
                        break;

                    default:
                        boardAfter[i, j] = _board[i, j];
                        break;
                }
            }
        }

        _board = boardAfter;
    }       
}

BoardTests.cs

[TestFixture]
public class BoardTests
{
    private Mock<IConsole> _fakeConsole;

    [SetUp]
    public void SetUp()
    {
        _fakeConsole = new Mock<IConsole>();
    }

    [Test]
    public void Evolve_Once_ReturnCorrectOutput()
    {
        //Arrange
        var board = new Board(_fakeConsole.Object);

        var boardArray = new[,] {
            {false, false, false, false, false},
            {false, false, false, false, false},
            {false, true , true , true , false},
            {false, false, false, false, false},
            {false, false, false, false, false}
        };

        //Act
        board.Set(boardArray);
        board.Evolve();

        //Assert
        Assert.That(boardArray[1, 1].Equals(false));
        Assert.That(boardArray[1, 2].Equals(true));
        Assert.That(boardArray[1, 3].Equals(false));
        Assert.That(boardArray[2, 1].Equals(false));
        Assert.That(boardArray[2, 2].Equals(true));
        Assert.That(boardArray[2, 3].Equals(false));
        Assert.That(boardArray[3, 1].Equals(false));
        Assert.That(boardArray[3, 2].Equals(true));
        Assert.That(boardArray[3, 3].Equals(false));            
    }

    [Test]
    public void Evolve_Twice_ReturnCorrectOutput()
    {          
        //Arrange
        var board = new Board(_fakeConsole.Object);

        var boardArray = new[,] {
            {false, false, false, false, false},
            {false, false, false, false, false},
            {false, true , true , true , false},
            {false, false, false, false, false},
            {false, false, false, false, false}
        };

        //Act
        board.Set(boardArray);
        board.Evolve();
        board.Evolve();

        //Assert
        Assert.That(boardArray[1, 1].Equals(false));
        Assert.That(boardArray[1, 2].Equals(false));
        Assert.That(boardArray[1, 3].Equals(false));
        Assert.That(boardArray[2, 1].Equals(true));
        Assert.That(boardArray[2, 2].Equals(true));
        Assert.That(boardArray[2, 3].Equals(true));
        Assert.That(boardArray[3, 1].Equals(false));
        Assert.That(boardArray[3, 2].Equals(false));
        Assert.That(boardArray[3, 3].Equals(false));
    }        
}    

4 个答案:

答案 0 :(得分:4)

单元测试旨在根据提供的输入(不管是正确的还是错误的)来验证函数是否产生了正确的输出。您正在尝试为不接受任何输入且不产生任何输出的函数编写单元测试。一般来说,它所做的只是改变内部状态,内部状态超出了单元测试的范围。

因此,是的,您将不得不采用某种解决方法。无论哪种方式都可以测试您的代码以进行测试,例如:

#if TEST
  public bool[,] Evolve() ...
#else
  public void Evolve() ...

或设计出其他一些方法来可靠地检测突变的内部状态。您尝试的代码似乎更符合功能测试,而不符合单元测试

答案 1 :(得分:2)

正如其他人所说,该类不公开任何状态,因此就输入和输出而言,您根本无法进行单元测试。就像您已经指出的那样,为了进行单元测试,您不必过于复杂地实现您的实现。

相反,您可以通过将“ Evolve”方法实现抽象到另一个负责构建板的类(例如“ IBoardEvolver”)中来使用间接,该类将注入“ Board”对象的构造函数中。

此“ IBoardEvolver”将具有一个方法,例如“ Build”或“ Evolve”,该方法接受您的电路板阵列,并返回其变异版本。现在,您具有输入和输出以及可以进行单元测试的公共方法。现在可以对IBoardEvolver实施进行单元测试。

对于您的Board类单元测试,您将模拟此特定的​​“ IBoardEvolver”接口,并验证在调用“ Board.Evolve”时是否使用了“ IBoardEvolver.Build()”方法(或该方法的名称可能是)被调用一次。

答案 2 :(得分:1)

如果您不想对其进行重新处理以使您的_board成员是另一个对象,或者添加某种获取/编制索引的方式,则两个选项是

  1. 使用反射来访问Board的私有成员或访问
    1. 这会使重构复杂化,如果您更改私有成员的名称,则必须手动更新单元测试
  2. _board的访问器更改为internal而不是private,然后在项目的程序集信息中添加
  3. InternalsVisibleTo

答案 3 :(得分:0)

内部状态可以并且应该通过对象(公共API)的公开行为进行测试。

如果您不想公开内部逻辑,则可以很好地封装类-即使为了测试也不要这样做。

好像您没有向我们显示所有内容,因为仅具有方法BoardSet的{​​{1}}类对该类的使用者没有用。

您正在将Evolve注入到一个类中,因此很显然它在您未向我们展示的地方使用。如果是这样,则将经过改进的板传递到IConsole来绘制板,然后将其用于测试经过改进的板。

例如,IConsole类具有方法Board,则可以通过调用所有三个方法来测试类的行为。

Draw