[UnitTest] Testowanie klasy, która jest zależna od innej.

0

Uczę się pisać testy jednostkowe i napotkałem na problem. Mam dwie klasy pierwsza reprezentuje mapę gry, którą przekazuję do drugiej klasy która sprawdza czy nastąpiła wygrana. Chciałem przetestować jednostkowo klasę WinAlgorithm ale nie wiem jak się za to zabrać przez to, że ta klasa jest zależna od Board. Czy mógłbym prosić o wskazówki jak to ugryźć albo jeden test, który pokaże mi jak się coś takiego testuje?

public class Board
{
	private const int DefaultSize = 3;

	public Cell[,] Cells { get; }
	public int BoardSize { get; }
	public int MoveCount { get; private set; }

	public Board(Options options)
	{
		Guard.ThrowIfNull(options, nameof(options));

		BoardSize = options.BoardSize >= DefaultSize
			? options.BoardSize
			: DefaultSize;

		Cells = new Cell[BoardSize, BoardSize];
		CreateCells();
	}

	public Cell GetCell(int row, int col)
	{
		ThrowIfInvalid(row, col);

		return Cells[row, col];
	}

	public void SetCell(int row, int col, Player player)
	{
		ThrowIfInvalid(row, col);

		if (!IsEmpty(GetSell(row, col))
			throw new InvalidOperationException();

		Cells[row, col] = player == Player.Cross
			? Cell.CreateCrossCell()
			: Cell.CreateCircleCell();

		MoveCount++;
	}

	public bool IsEmpty(Cell cell)
	{
		return cell.CellType == CellType.Empty;
	}
		
	// other methods
}
public class WinAlgorithm
{
	private readonly Board _board;

	public WinAlgorithm(Board board)
	{
		Guard.ThrowIfNull(board, nameof(board));

		_board = board;
	}

	public bool IsRowWinner(int row)
	{
		var cell = _board.GetCell(row, 0);

		if (_board.IsEmpty(cell))
			return false;

		for (var i = 1; i < _board.BoardSize; i++)
			if (_board.GetCell(row, i) != cell)
				return false;

		return true;
	}

	public bool IsColWinner(int col)
	{
		var cell = _board.GetCell(0, col);

		if (_board.IsEmpty(cell))
			return false;

		for (var i = 1; i < _board.BoardSize; i++)
			if (_board.GetCell(i, col) != cell)
				return false;

		return true;
	}

	// other methods
}
1

Jeśli klasa jest zależna od Board, to pewnie najwygodniej zmockować ten Board. Tu http://www.altcontroldelete.pl/artykuly/mockowanie-obiektow-w-praktyce-z-biblioteka-moq/ masz przykład . Ale wydaje mi się, żeby mock mógł zwracać jakieś fejkowe dane to jego metody muszą być virtualne lub musisz zmockować interfejs, który będzie implementowany przez Board.

0

Teraz pytanie czym jest test jednostkowy

Tak, tutaj wiele osob bedzie sie niezgadzac ze mna. Ale opisze obie sytuacje

  • Testujesz jedynie klase WinAlgorithm a dokladniej konkretne tej klasy metody. Mockujesz wszystkie zaleznosci (polecam darmowy Moq do tego). W takim przypadku potrzebujesz miec interfejs do IBoard (nie ze potrzebujesz, ale darmowy moq nie potrafi zamockowac nieinterfejsu). Przygotowywujesz mock tak by robil to co chcesz i sprawdzasz swoj wynik. Dla mnie to troche testowanie kodu, ktory sie napisalo, a nie jednostki

  • Testujesz flow. WinAlgorithm nie moze dzialac bez board. Jezeli board bedzie mial jakis blad, to nie bedzie mialo znaczenia, ze ladnie zmockowales. Wiec tworzysz Board i tworzysz WinAlgorithm wszystkie zaleznosci ktore wychodza po za "Twoj kod" (baza danych, http call) mockujesz by zwracaly dane takie jak chcesz. Dzieki temu testujesz rzeczywiscie czy ta funkcja dziala jak powinna a nie sam kod

Zalezy co wolisz. Jak sa trudne algorytmy to pisze w pierwszy sposob, bo mnie interesuje tylko i wylacznie ta metoda, jezeli chce przetestowac czy moj feature dziala pisze testy w sposob drugi

0

Na razie wyodrębniłem interfejs z klasy Board i zrobiłem mock tak jak pisaliście. Napisałem dwa testy jeden testujący sam algorytm a drugi chyba flow. Mniej więcej tak ma to wyglądać? Tylko teraz nasuwają mi się wątpliwości czy powinienem testować metodę UpdateGameStatus jeśli algorytm działa poprawnie to, może SetWinState ustawić jako public i tylko to przetestować? Pisanie testów jest trudniejsze niż nauka programowania, nie wiem co mam testować... Czy ten drugi test nie podchodzi pod testy integracyjne?

public class Game
{
	// row and col are coordinates of last move
	public void UpdateGameState(int row, int col)
	{
		// check row
		if (_winAlgorithm.IsRowWinner(row))
			SetWinState();

		// check col
		else if (_winAlgorithm.IsColWinner(col))
			SetWinState();

		// check diagonal only if cell is on its
		else if (Board.IsCellOnDiagonal(row, col) && _winAlgorithm.IsDiagonalWinner())
			SetWinState();

		// check anti diagonal if cell is on its
		else if (Board.IsCellOnAntiDiagonal(row, col) && _winAlgorithm.IsAntiDiagonalWinner())
			SetWinState();

		// check for tie
		else if (_winAlgorithm.IsTie())
			State = State.Tie;
	}

	private void SetWinState()
	{
		State = State.Win;
		Winner = CurrentPlayer;
	}
}
[Fact]
public void IsRowWinner_WhenInRowIsEmptyCell_ReturnFalse()
{
	// arrange
	var board = Mock.Of<IBoard>(x =>
		x.GetCell(0, 0) == Cell.CreateCrossCell() &&
		x.GetCell(0, 1) == Cell.CreateCircleCell() &&
		x.GetCell(0, 2) == Cell.CreateEmptyCell() &&
		x.IsEmpty(It.Is<Cell>(c => c.CellType == CellType.Empty)) == true &&
		x.IsEmpty(It.Is<Cell>(c => c.CellType != CellType.Empty)) == false &&
		x.BoardSize == 3);

	var algorithm = new WinAlgorithm(board);

	// act
	var result = algorithm.IsRowWinner(0);

	// assert
	result.Should().BeFalse();
}
[Fact]
public void UpdateGameStatus_WhenIsWinner_ChangeStatus()
{
	// arrange
	var board = new Board(new Options { BoardSize = 3 });
	var winAlgorithm = new WinAlgorithm(board);
	var game = new Game(board, winAlgorithm, Player.Cross);

	game.Move(0, 0);
	game.Move(0, 1);
	game.Move(0, 2);

	// act
	game.UpdateGameState(0, 2);

	// assert
	game.State.Should().Be(State.Win);
	game.Winner.Should().Be(Player.Cross);
}
6

Po co mockować coś tak trywialnego jak Board? To jest tylko struktura danych, bez żadnej logiki, nie korzysta z żadnych zewnętrznych zasobów, mockując niczego nie zyskujemy.

Jaki to problem utworzyć obiekt Board w testach i przekazać go do WinAlgorithm? Przecież to będzie nawet mniej kodu niz tym mockiem.
To trochę tak jakby próbować mockować int na potrzeby testowania dodawania.

0

@somekind czy miałeś na myśli coś takiego?

Mam jeszcze pytanie czy powinienem testować algorytm dla macierzy o rozmiarze większym niż [3, 3] np. [5, 5]?

[Fact]
public void IsRowWinner_WhenInRowIsEmptyCell_ReturnFalse()
{
	// arrange
	var board = new Board(new Options { BoardSize = 3 });
	var algorithm = new WinAlgorithm(board);

	// act
	var result = algorithm.IsRowWinner(0);

	// assert
	result.Should().BeFalse();
}

[Theory]
[InlineData(Player.Cross)]
[InlineData(Player.Circle)]
public void IsRowWinner_WhenCellsInRowAreEquals_ReturnTrue(Player player)
{
	// arrange
	var board = new Board(new Options { BoardSize = 3 });
	var algorithm = new WinAlgorithm(board);

	board.SetCell(0, 0, player);
	board.SetCell(0, 1, player);
	board.SetCell(0, 2, player);

	// act
	var result = algorithm.IsRowWinner(0);

	// assert
	result.Should().BeTrue();
}
0

No na przykład (chociaż ja bym to inaczej zapisał, jak dla mnie Board powinien mieć porządny konstruktor, a nie publiczne Options czy SetCell).

1 użytkowników online, w tym zalogowanych: 0, gości: 1