【问题标题】:I'm trying to create unit tests for a private method我正在尝试为私有方法创建单元测试
【发布时间】:2018-04-29 17:11:51
【问题描述】:

我正在尝试为 GetOutOfJail 方法创建单元测试,但我无法找到一种获取它的方法,因为它是私有的,除了测试之外,它不需要公开。我无法更改LandedOnTile 方法的签名,因为它继承了抽象类Tile

您可能已经猜到了,这是我正在尝试将其作为迷你项目制作的一款大富翁游戏。

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++;
        }
    }
}

【问题讨论】:

  • 为什么要测试私有方法?通过确保调用调用它的公共方法 (LandedOnTile) 时发生正确的事情来测试它。
  • 你到底想测试什么?调用公共方法并断言主题的行为符合预期。没有看到问题出在哪里。
  • 在此处阅读相关讨论:stackoverflow.com/questions/9122708/…
  • 测试private 方法是错误的,但如果你需要你可以使用反射。
  • 瓷砖不应该自己掷骰子。将其抽象为一个代表掷骰子并保存掷骰子的类,然后您可以使用模拟骰子测试此图块。

标签: c# unit-testing


【解决方案1】:

正如其他人所提到的,从单元测试的角度来看,私有方法的作用并不重要。您所关心的是,如果您以正确的方式戳或刺对象,它最终会处于正确的状态。

以下是使用接口和 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());
    }

现在您可能想多考虑一下设计,以及更新这些属性是否真的是 tile 的责任,或者它是否应该在可以单独测试的 jail 对象上调用某些东西,但这显示了如何您可以使用模拟从代码中抽象出对随机等的调用以使其可测试。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-08-10
    • 1970-01-01
    • 2013-11-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多