Как выполнить модульное тестирование метода, который изменяет ссылку на приватное поле - PullRequest
0 голосов
/ 20 апреля 2019

Я пишу юнит-тесты для класса в приложении игры из жизни.У меня есть метод, который создает двумерный массив bools, а затем изменяет ссылку на поле (другой двумерный массив bools) на новый массив.

У меня проблема с модульным тестированием класса.Я пытаюсь проверить правильность вывода, однако я не могу получить доступ к массиву после того, как он изменил ссылку.

Я нашел обходной путь, вместо создания новогомассив и назначая новую ссылку, я зацикливаю повторное заполнение исходного поля содержимым нового массива, но это добавляет ненужные вычисления только ради модульного тестирования, что не похоже на хорошую идею.

Как проверить правильность поведения метода 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 ]

4 голосов
/ 20 апреля 2019

A Модульный тест предназначен для проверки того, что функция выдает правильный вывод на основе предоставленного ввода, будь то правильный или ошибочный.Вы пытаетесь написать модульный тест для функции, которая не принимает ввод и не производит вывод.Все, что он делает - это мутирует внутреннее состояние, а внутреннее состояние вообще не допускается для модульного тестирования.

Итак, да, вам придется использовать какой-то обходной путь.Либо используйте свой код для тестирования, например, так:

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

, либо разработайте какой-либо другой метод надежного обнаружения измененного внутреннего состояния.Код, который вы пытаетесь выполнить, больше соответствует функциональному тестированию , чем модульному тестированию .

2 голосов
/ 20 апреля 2019

Как уже говорили другие, класс не предоставляет состояния, так что на самом деле нет ничего, что вы можете тестировать модулем так далеко, как идут входы и выходы.И, как вы заметили, вам не нужно слишком усложнять вашу реализацию ради модульного тестирования.

Вместо этого вы можете использовать косвенное обращение, абстрагируя реализацию методов «Evolve» в другой класс, который отвечает за сборку платы, например, «IBoardEvolver», который внедряется в конструктор вашего объекта «Board».

Этот «IBoardEvolver» будет иметь метод, скажем «Build» или «Evolve», который принимает ваш массив плат и возвращает его мутированную версию.Теперь у вас есть вход и выход, а также публичный метод, который можно протестировать модульно.Модульное тестирование реализации IBoardEvolver теперь возможно.

Для модульных тестов класса Board вы должны смоделировать этот конкретный интерфейс "IBoardEvolver" и убедиться, что при вызове "Board.Evolve", "IBoardEvolver.Build ()"Метод (или каким бы ни было имя метода) вызывается один раз.

1 голос
/ 20 апреля 2019

Если вы не хотите переделывать его так, чтобы ваш член _board был другим объектом, или добавить какой-либо способ получения / индексации, есть два варианта:

  1. Использовать отражение для доступа к приватномучленов Board или
    1. Это усложняет рефакторинг. Если вы измените имя своего частного участника, вам придется вручную обновить модульный тест
  2. Изменитьаксессором _board должно быть internal вместо private, а затем в сведениях о сборке проекта добавить InternalsVisibleTo из
0 голосов
/ 21 апреля 2019

Внутреннее состояние можно и нужно проверять с помощью открытого поведения объекта (публичный API).

У вас есть хорошо инкапсулированный класс, если вы не хотите раскрывать внутреннюю логику - не делайте этого даже ради тестов.

Похоже, вы не все нам показываете, потому что класс Board только с методами Set и Evolve бесполезен для потребителей этого класса.

Вы вводите IConsole в класс, поэтому, очевидно, он используется там, где вы нам его не показывали. Если это так - и эволюционировавшая доска перешла к IConsole для рисования, то используйте ее для тестирования развитой платы.

Например, Board класс имеет метод Draw, затем вы можете проверить поведение класса, вызвав все три метода.

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);
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...