-2

I'm trying to create unit tests for the GetOutOfJail method but I can't work out a way of getting to it as it private and apart from testing there is no need for it to be public. I can't change the signature of the LandedOnTile method as it inheriting a the abstract class Tile.

As you probably worked out, it is for a game of Monopoly I'm trying to make as a mini project.

public abstract class Tile
{
    public abstract int Location { get;}
    public abstract void LandedOnTile(Player player);
}

public class JailTile : Tile
{
    public override int Location { get; }
    Random dice = new Random();

    public JailTile()
    {
        Location = 3;
    }

    public override void LandedOnTile(Player player)
    {
        if (player.inJail)
        {
            GetOutOfJail(player);
        }
        else
        {
            Console.WriteLine(player.name + " is just visiting jail");
        }
    }

    private void GetOutOfJail(Player player)
    {
        int roll = dice.Next(1, 4);
        int turnsInJail = player.timeInJail;

        if (turnsInJail == 3)
        {
            player.inJail = false;
            Console.WriteLine(player.name + " has spent 3 turns in jail and is now out");
            player.timeInJail = 0;
        }
        else if (turnsInJail < 3 && roll > 2)
        {
            player.inJail = false;
            Console.WriteLine(player.name + " has rolled a 3 and it out of jail");
            player.timeInJail = 0;
        }
        else
        {
            Console.WriteLine(player.name + " has rolled a lower than a 3 and is in jail for another turn");
            player.timeInJail++;
        }
    }
}
marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
Stuart Bradley
  • 149
  • 2
  • 9
  • 2
    Why do you need to test the private method? Test it by making sure the right thing happens when you call the public method (`LandedOnTile`) that invokes it. – jonrsharpe Apr 29 '18 at 17:14
  • And what is it exactly you are trying to test? Call the public method and assert that the subject behaves as expected. Not seeing where the problem is. – Nkosi Apr 29 '18 at 17:14
  • read related discussion here: https://stackoverflow.com/questions/9122708/unit-testing-private-methods-in-c-sharp – Ofir Apr 29 '18 at 17:17
  • It's a wrong way to test `private` methods but if u need u can use the reflection. – Denis Sologub Apr 29 '18 at 17:20
  • So because getoutojail creates a random number when it is called I can't predict the outcome without knowing that random number.i want to test that when that number is 1 it does something. When it is 2 it does something etc. – Stuart Bradley Apr 29 '18 at 17:23
  • 4
    A tile shouldn't have to roll a die by itself. Abstract that away into a class that represents rolled dice and holds the rolled number, then you can test this tile with a mocked die. – CodeCaster Apr 29 '18 at 17:26

1 Answers1

0

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.

Adam G
  • 1,283
  • 1
  • 6
  • 15