As mentioned by others, it shouldn't matter what the private method does from the perspective of the unit test. All you care about is that if you poke or prod the object in the right way, it ends up in the right state.
Here is how you could achieve that using interfaces and Moq.
Firstly, extract the interface that represents the properties and methods you require to perform the action. I have abstracted out your Console.WriteLine because it makes testing much easier (and even opens other opportunities for that code to be used in a non-console application). We don't actually need a "dice" per se. What we actually need is an object we can ask to Roll() and get an int. Players probably have their own business rules about them, so extracting to an IPlayer interface allows my tests of JailTile to ignore such things.
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;
}
}
Secondly, here are the concrete implementations of a Dice and a ConsoleLogger. You would pass in these in your production code rather than the mocks that I use in the test cases
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);
}
}
Thirdly, here are your Tile and JailTile classes slightly modified to use constructor injection
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++;
}
}
}
Finally, here is a test case to prove that your jailTile.LandedOnTile()
method causes the right changes to Player and logs the right message to console given a certain set of preconditions
[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());
}
Now you probably want to think a bit more about the design, and whether it is really the tile's responsibility to be updating these properties, or whether it should call something on a jail object that can separately be tested, but this shows how you can use mocks to abstract calls to random etc out of your code to make it testable.