单元测试 - 排列测试项目无法访问的对象

时间:2010-10-05 20:11:05

标签: c# unit-testing

我正在写一个纸牌游戏,试图学习Silverlight。

我有游戏的域表示,它将Silverlight作为WCF服务公开,而我的Game对象上唯一的公共方法是“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方法),然后玩游戏状态

但是,当然,由于玩家是一个只读属性,我不能这样做。同样Hand(在播放器上)无法访问。同样是手的部分。

我有什么想法可以设置它吗?

我想这可能意味着我的设计错了?

感谢

修改

这是一些类看起来像是什么让你知道我想要在卡片播放后断言的一些属性

在我尝试使用InternalsToVisible时,我已经将私人制定者改为内部,但我对纯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; }

...

3 个答案:

答案 0 :(得分:2)

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

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

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

修改

Mocked 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

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

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();
    }
}

这是否相当准确?

答案 1 :(得分:1)

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

查看InternalsVisibleToAttribute课程。

这是关于如何使用它的tutorial

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

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

通过这种方式,您可以在具有特定功能的类中使用公共方法,并单独测试它们以及它们如何相互交互。

这种方法可能会增加开销,但我认为从长远来看这将有所帮助。

答案 2 :(得分:1)

我认为问题的关键在于:

  

我需要有很多场景   单元测试(通过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<Player>,以便更好地传达只读语义。