Как уже упоминалось другими, не должно иметь значения, что делает приватный метод с точки зрения модульного теста. Все, что вас волнует, это то, что, если вы будете толкать или подталкивать объект правильным образом, он окажется в нужном состоянии.
Вот как вы можете добиться этого, используя интерфейсы и Moq.
Во-первых, извлеките интерфейс, представляющий свойства и методы, необходимые для выполнения действия. Я абстрагировал ваш Console.WriteLine, потому что он значительно облегчает тестирование (и даже открывает другие возможности для использования этого кода в не консольном приложении). Нам на самом деле не нужны «кости» как таковые. На самом деле нам нужен объект, который мы можем попросить Roll () и получить int. Вероятно, у игроков есть свои собственные бизнес-правила, поэтому извлечение из интерфейса IPlayer позволяет моим тестам JailTile игнорировать такие вещи.
public interface ILogger
{
void LogMessage(string message);
}
public interface IDice
{
int Roll();
}
public interface IPlayer
{
string Name
{
get;
}
bool InJail
{
get;
set;
}
int TimeInJail
{
get;
set;
}
}
Во-вторых, вот конкретные реализации Dice и ConsoleLogger. Вы должны передать их в своем рабочем коде, а не в макетах, которые я использую в тестовых примерах
public class ConsoleLogger : ILogger
{
public void LogMessage(string message)
{
Console.WriteLine(message);
}
}
public class Dice : IDice
{
private readonly Random random = new Random();
public int Roll()
{
return this.random.Next(1, 6);
}
}
В-третьих, вот ваши классы Tile и JailTile, слегка модифицированные для использования инъекции в конструктор
public abstract class Tile
{
protected readonly IDice Dice;
protected readonly ILogger Logger;
protected Tile(ILogger logger, IDice dice)
{
this.Logger = logger;
this.Dice = dice;
}
public abstract int Location
{
get;
}
public abstract void LandedOnTile(IPlayer player);
}
public class JailTile : Tile
{
public JailTile(ILogger logger, IDice dice): base (logger, dice)
{
}
public override int Location => 3;
public override void LandedOnTile(IPlayer player)
{
if (player.InJail)
{
this.GetOutOfJail(player);
}
else
{
this.Logger.LogMessage($"{player.Name} is just visiting jail");
}
}
private void GetOutOfJail(IPlayer player)
{
int roll = this.Dice.Roll();
int turnsInJail = player.TimeInJail;
if (turnsInJail == 3)
{
player.InJail = false;
this.Logger.LogMessage($"{player.Name} has spent 3 turns in jail and is now out");
player.TimeInJail = 0;
}
else if (turnsInJail < 3 && roll > 2)
{
player.InJail = false;
this.Logger.LogMessage($"{player.Name} has rolled a 3 and it out of jail");
player.TimeInJail = 0;
}
else
{
this.Logger.LogMessage($"{player.Name} has rolled a lower than a 3 and is in jail for another turn");
player.TimeInJail++;
}
}
}
Наконец, вот тестовый пример, доказывающий, что ваш метод jailTile.LandedOnTile()
вызывает правильные изменения в Player и записывает правильное сообщение на консоль, учитывая определенный набор предварительных условий
[Test]
public void ShouldReleaseAfterThreeTurns()
{
// Arrange
Mock<ILogger> loggerMock = new Mock<ILogger>();
Mock<IDice> diceMock = new Mock<IDice>();
diceMock.Setup(s => s.Roll()).Returns(2);
Mock<IPlayer> playerMock = new Mock<IPlayer>();
playerMock.Setup(s => s.Name).Returns("Adam G");
playerMock.Setup(s => s.InJail).Returns(true);
playerMock.Setup(s => s.TimeInJail).Returns(3);
// Act
JailTile jailTile = new JailTile(loggerMock.Object, diceMock.Object);
jailTile.LandedOnTile(playerMock.Object);
// Assert
playerMock.VerifySet(v => v.InJail = false, Times.Once());
playerMock.VerifySet(v => v.TimeInJail = 0, Times.Once());
loggerMock.Verify(v => v.LogMessage("Adam G has spent 3 turns in jail and is now out"), Times.Once());
}
Теперь вы, вероятно, захотите немного больше подумать о дизайне и о том, действительно ли плитка несет ответственность за обновление этих свойств, или же она должна вызывать что-то для объекта тюрьмы, который можно отдельно протестировать, но это показывает, как вы можете использовать макеты для абстрагирования вызовов случайных и т. д. от кода, чтобы сделать его тестируемым.