简体   繁体   中英

How to unit test a method that changes reference of a private field

I'm writing unit tests for a class in a game of life application. I have a method which creates a 2d array of bools and then changes the reference of a field (another 2d array of bools) to the new array.

I have trouble unit testing the class. I'm trying to test if the output is correct, however, there is no way for me to access the array after it has changed the reference.

I have found a workaround which is, instead of creating a new array and assigning new reference, I'm looping re-populating the original field with the contents of the new array, but that is adding an unnecessary computation just for the sake of unit testing which does not sound like a good idea.

How can I test the Evolve method for the correct behaviour?

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

A Unit Test is intended to verify that a function produces the proper output based on supplied input, whether correct or erroneous. You are trying to write a unit test for a function that accepts no input and produces no output. All it does is mutate internal state, and internal state is out-of-bounds for unit-testing, generally speaking.

So, yes, you will have to employ some sort of workaround. Either instrument your code for testing, like so:

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

or devise some other method of reliably detecting the mutated internal state. The code that you are attempting appears to align more with functional testing than with unit testing .

As others have said, the class exposes no state, so there's really nothing you can unit test as far as inputs and outputs go. And as you've noted, you shouldn't need to over complicate your implementation for the sake of unit testing.

Instead, you can employ indirection by abstracting out the "Evolve" methods implementation into another class which is responsible for building the board, say "IBoardEvolver", which is injected into your "Board" object's constructor.

This "IBoardEvolver" would have a method, say "Build" or "Evolve", which accepts your board array, and returns a mutated version of it. You now have an input and output, and a public method that can be unit tested. Unit testing the IBoardEvolver implementation is now possible.

For your Board class unit tests you would Mock this particular "IBoardEvolver" interface and verify that when "Board.Evolve" is called, the "IBoardEvolver.Build()" method (or whatever the name of the method might be) is called once.

If you don't want to rework it so that your _board member is another object, or add some way of getting/indexing, two options are

  1. Use reflection to access the private members of the Board or to
    1. This complicates refactoring, if you change the name of your private member, you have to manually update the unit test
  2. Change the accessor of _board to be internal instead of private and then in the assembly info of the project add InternalsVisibleTo of the

Internal state could and should be tested through exposed behaviour of the object(public API).

You have nicely encapsulated class, if you don't want to expose internal logic - don't do this, even for the sake of the tests.

Looks like you don't show us everything, because Board class with only methods Set and Evolve is useless for the consumers of this class.

You are injecting IConsole into a class, so apparently it is used somewhere you didn't show us. If so - and evolved board passed to the IConsole to draw the board, then use it for testing evolved board.

For instance Board class has method Draw , then you can test class behaviour by calling all three methods.

public Test_EvolveOnce()
{
    var fakeConsole = Substitute.For<IConsole>();
    var evolvedBoards = new List<bool[,]>();

    // Configure fake console to intercept evolved board
    fakeConsole.When(c => c.Write(Arg.Any<bool[,]>())
                .Do(call => evolvedBoards.Add(call.ArgAt[0]));

    var givenBoard = 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 }
    };
    var expectedBoard = new[,] 
    {
        { false, false, false, false, false },
        { false, false, true , false, false },
        { false, false, true , false, false },
        { false, false, true , false, false },
        { false, false, false, false, false }
    };
    var board = new Board(fakeConsole);

    board.Set(givenBoard);
    board.Evolve();
    board.Draw();

    evolvedBoards.Should().HaveCount(1);
    evolvedBoards.First().Should().BeEquivalentTo(expected);
}

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