【问题标题】:Unit Testing - Arranging objects that are inaccessible to the test project单元测试 - 安排测试项目无法访问的对象
【发布时间】:2011-12-30 20:03:36
【问题描述】:

我正在写一个纸牌游戏来尝试学习 Silverlight。

我有游戏的域表示,它作为 WCF 服务暴露给 Silverlight,我的游戏对象上唯一的公共方法是“PlayCards”。这是一个看起来像这样的方法

public void PlayCards(Guid playerId, List<Card> cards)

当调用此方法时,Game 对象会更新并使用双工绑定传输给所有客户端

我需要对很多场景进行单元测试(通过 UI 进行测试变得非常乏味,尤其是因为有很多场景并不经常自然发生)。但是,这样做意味着公开各种真正不需要的属性(除了我的测试目的)

这里有一些细节:

我有一个包含玩家列表的游戏类:

public class Game
{       
 public List<Player> Players
 {
    get { return _players; }
 }

 ...

Player 类有一个 Hand

 public class Player
 {
    public Hand Hand { get; internal set; }

 ...

Hand 类有不同的部分

public class Hand
    {
        public List<Card> FaceDownCards { get; internal set; }
        public List<Card> FaceUpCards { get; internal set; }
        public List<Card> InHandCards { get; internal set; }

现在,在我的单元测试中,我想设置各种场景,其中一个玩家有某些牌,而下一个玩家有某些牌。然后让第一个玩家玩一些牌(通过公共 PlayCards 方法),然后断言游戏状态

当然,由于 Players 是只读属性,我不能这样做。同样,手(在播放器上)是不可访问的。手的部分也是如此。

有什么想法可以设置吗?

我猜这可能意味着我的设计是错误的?

谢谢

编辑:

下面是一些类的样子,让您了解我想在打出纸牌后断言的一些属性

在我尝试 InternalsToVisible 时,我暂时将私有设置器更改为 internal,但我对纯 TDD 的做事方式很感兴趣

public class Game
    {
        public List<Player> Players { get; internal set; }
        public Deck Deck { get; internal set; }
        public List<Card> PickUpPack { get; internal set; }
        public CardList ClearedCards { get; internal set; }

        public Player CurrentPlayer
        {
            get
            {
                return Players.SingleOrDefault(o => o.IsThisPlayersTurn);
            }
        }

        internal Direction Direction { get; set; }

..

public class Player
    {
        public Guid Id { get; internal set; }
        public Game CurrentGame { get; internal set; }
        public Hand Hand { get; internal set; }
        public bool IsThisPlayersTurn { get; internal set; }
        public bool IsAbleToPlay { get; internal set; }
        public string Name { get; internal set; }
...

 public class Hand
    {
        public List<Card> FaceDownCards { get; internal set; }
        public List<Card> FaceUpCards { get; internal set; }
        public List<Card> InHandCards { get; internal set; }

...

【问题讨论】:

  • 哎呀 - 我最初严重误读了这个问题。如何初始化Game_players?以及如何初始化Player 的手(以此类推)?
  • 游戏有一个 AddPlayer 方法,当客户端加入游戏时,该方法通过 WCF 服务层调用......当有正确数量的玩家时,调用 GameStart 方法,该方法洗牌并分发以特定方式的卡片。因此无需公开访问玩家或手牌

标签: c# unit-testing


【解决方案1】:

错的不是你的设计,而是你的实现可能错了。试试这个,接口只有getters 的属性声明:

public class Game : IGame
{       
    public List<IPlayer> Players
    {
        get { return _players; }
    }
}

public class Player : IPlayer
{
    public IHand Hand { get; internal set; }
}

public class Hand : IHand
{
    public List<Card> FaceDownCards { get; internal set; }
    public List<Card> FaceUpCards { get; internal set; }
    public List<Card> InHandCards { get; internal set; }
}

如果您以这种方式构建您的课程,您可以模拟您想要测试的手,或以编程方式使用这些手构建玩家,以验证所有不同级别的逻辑。

编辑

模拟 IPlayer 的示例(在本例中使用 Moq)注入特定(扑克)手 -

//Mmmm...Full House
List<Card> inHandCards = new List<Card> { ...Cards...}; //2 Kings?
List<Card> faceDownCards = new List<Card> { ...Cards...}; 
List<Card> faceUpCards = new List<Card> { ...Cards...};  //3 4s?

Mock<IHand> hand = new Mock<IHand>();
hand.SetupGet(h => FaceDownCards).Returns(faceDownCards);
hand.SetupGet(h => FaceUpCards).Returns(faceUpCards);
hand.SetupGet(h => InHandCards).Returns(inHandCards);

Mock<IPlayer> player = new Mock<IPlayer>();
player.SetupGet(p => p.Hand).Returns(hand.Object);

//Use player.Object in Game

编辑 2

我刚刚意识到为什么 Jeff 评论说他误读了这个问题,并意识到我只回答了部分问题。为了扩展你在 cmets 中所说的内容,听起来你有这样的东西:

public class Game
{
    public Player NextPlayer { get; private set; }
    public bool NextPlayerCanPlay { get; private set; }
    ...
}

这不是一个非常可测试的类,因为您的测试不能设置任何东西。我发现解决此问题的最佳方法是构建几个接口 IGameITestableGame 以不同方式公开属性:

public interface IGame
{
    IPlayer NextPlayer { get; }
    bool NextPlayerCanPlay { get; }
}

internal interface ITestableGame
{
    IPlayer NextPlayer { get; set; }
    bool NextPlayerCanPlay { get; set; }
}

public class Game : IGame, ITestableGame
{
    public IPlayer NextPlayer { get; set; }
    public bool NextPlayerCanPlay { get; set; }
}

然后,在您的服务中,只需向客户发送一个IGame 对象,然后将ITestableGame 接口设为内部并将其作为InternalsVisibleTo(TestAssembly) 进行营销,或者将其公开并且不要在您的外部使用它测试。

编辑 3

仅根据您发布的内容,听起来您的控制流设置如下:

public class Player 
{
    public void Play(List<Card> cardsToPlay) 
    {
        hand.InHandCards.Remove(cardsToPlay);
        currentGame.PickUpPack.Add(cardsToPlay);
        hand.Add(currentGame.Deck.Draw(cardsToPlay.Count));
        currentGame.SelectNextPlayer();
    }
}

这相当准确吗?

【讨论】:

  • 感谢您花时间扩展您的答案。游戏对象上有很多私有属性,例如Direction、Deck、PickUpPack、ClearedCards、ActivePlayer、NextPlayer、CanNextPlayerPlay 等(这不是 Poker BTW)……这些都需要进入游戏界面吗?他们不会在 Mock 对象中正确设置他们吗?由于 Mock 不会包含任何改变它们的逻辑......而正是这些属性是我需要在玩家放置一些卡片后断言的
  • 我将在 IGame 界面上进行备份 - 构建它的唯一原因(与此问题/答案相关)是它是否将被注入到更大的可测试单元中。如果您不打算将 IGame 提供给任何操作或查询,那么 IGame 是不必要的。听起来就是这样,我的示例旨在说明测试 Game 类,而不是可能需要 IGame 的东西。如果您愿意,我会更新 Game 类的签名以使其更清晰。
  • 干杯 - 对答案投了赞成票。暂时将其保持打开状态,以查看其他人是否有任何意见-
  • 在上面添加了一些部分类定义,让您了解每次调用 PlayCards 后我需要断言的内容。不确定使用 Mock 会有什么帮助,因为他们没有逻辑来更改我想要断言的属性,对吗?再次欢呼
  • 这样,负责Game 状态的唯一实体是Game 本身,这意味着您可以根据模拟的PlayerGame 提供起始状态s、Decks 等,然后更改一些变量并观察 Game 如何响应变化。
【解决方案2】:

我认为问题的症结在这里:

有很多场景我需要 单元测试(通过 UI 进行是 变得非常乏味,尤其是 有很多场景不 经常自然发生)。 然而, 这样做意味着制作各种 真正不公开的财产 需要(除了我的测试 目的)。

我建议不要太担心更改接口以支持测试。

务必将 set 访问器保留在内部。* 您可以限制对 API 的影响,因为您真正需要的是为您需要安排的对象提供额外的公共构造函数。例如:

public Player(Guid id, Hand hand, bool isThisPlayersTurn, string name) { ... }

您希望能够编写简单、清晰的测试用例:

Hand   hand   = new Hand(faceDownCards, faceUpCards, inHandCards);
Player player = new Player(Guid.NewGuid(), hand, true, "Christo");
Game   game   = new Game(player);

我从不后悔这样做,即使只是为了测试目的。 API 仍然足够清晰,人们可以使用正确的构造函数:通常,他们会使用默认构造函数(或者根本不使用构造函数 - 相反他们会调用 game.AddPlayer("Christo"))并且只使用扩展构造函数进行测试或反序列化(当合适)。

* 并考虑将Game.Players 等成员的公共接口更改为IEnumerable&lt;Player&gt;,以更好地传达只读语义。

【讨论】:

    【解决方案3】:

    我过去也遇到过同样的问题。

    查看InternalsVisibleToAttribute 类。

    这里有一个tutorial关于如何使用它。

    此外,您可以考虑将类中的方法从 private 更改为 protected。然后,您可以编写一个带有公共包装函数的子类来进行测试。通过这种方式,您可以获得封装的好处,并且能够以最少的更改轻松测试现有代码。

    根据经验,如果您确实需要测试私有方法,则可能需要对您的设计进行一些调整。我会考虑在不同的类之间分离功能以放松耦合。

    这样,您可以在类中使用公共方法来执行特定操作并单独测试它们以及它们之间的交互方式。

    这种方法可能开销更​​大,但我认为从长远来看它会有所帮助。

    【讨论】:

    • 是的,我知道这一点。谢谢。这可能是我要走的路。但我知道它从纯粹主义者那里得到了不好的报道。我想听听他们怎么说。即使是“你的设计都是错的”
    • 另外,这是否适用于没有 Setter 的属性?或者您是否需要为每个 Setter 添加一个私有 Setter 才能正常工作?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-18
    • 2018-01-21
    • 1970-01-01
    相关资源
    最近更新 更多