在那个论坛上有一些关于单元测试的好帖子。以下是我在XNA中进行单元测试的个人方法:
这是一个测试示例,用于确认我的Update方法在Update()调用之间以正确的距离移动实体。 (我使用NUnit.) 我删除了一些具有不同移动向量的行,但您可以理解:您不需要使用游戏来驱动测试。
[TestFixture]
public class EntityTest {
[Test]
public void testMovement() {
float speed = 1.0f; // units per second
float updateDuration = 1.0f; // seconds
Vector2 moveVector = new Vector2(0f, 1f);
Vector2 originalPosition = new Vector2(8f, 12f);
Entity entity = new Entity("testGuy");
entity.NextStep = moveVector;
entity.Position = originalPosition;
entity.Speed = speed;
/*** Look ma, no Game! ***/
entity.Update(updateDuration);
Vector2 moveVectorDirection = moveVector;
moveVectorDirection.Normalize();
Vector2 expected = originalPosition +
(speed * updateDuration * moveVectorDirection);
float epsilon = 0.0001f; // using == on floats: bad idea
Assert.Less(Math.Abs(expected.X - entity.Position.X), epsilon);
Assert.Less(Math.Abs(expected.Y - entity.Position.Y), epsilon);
}
}
编辑:以下是一些来自评论的注释:
我的实体类: 我选择将所有的游戏对象封装在一个集中的实体类中,大致如下:
public class Entity {
public Vector2 Position { get; set; }
public Drawable Drawable { get; set; }
public void Update(double seconds) {
// Entity Update logic...
if (Drawable != null) {
Drawable.Update(seconds);
}
}
public void LoadContent(/* I forget the args */) {
// Entity LoadContent logic...
if (Drawable != null) {
Drawable.LoadContent(seconds);
}
}
}
这让我有很大的灵活性,可以创建实体(Entity)的子类(AIEntity、NonInteractiveEntity等),这些子类可能会重写Update()方法。同时,这也让我可以自由地创建Drawable的子类,而不必像 AnimatedSpriteAIEntity
、ParticleEffectNonInteractiveEntity
和 AnimatedSpriteNoninteractiveEntity
那样需要n²个子类。代替这些,我可以这样做:Entity torch = new NonInteractiveEntity();
torch.Drawable = new AnimatedSpriteDrawable("Animations\litTorch");
SomeGameScreen.AddEntity(torch);
// let's say you can load an enemy AI script like this
Entity enemy = new AIEntity("AIScritps\hostile");
enemy.Drawable = new AnimatedSpriteDrawable("Animations\ogre");
SomeGameScreen.AddEntity(enemy);
我的Drawable类:我有一个抽象类,所有绘制对象都是从该类派生的。我选择了抽象类,因为某些行为将被共享。如果你的代码不是这样的情况,定义为接口也是可以的。
public abstract class Drawable {
// my game is 2d, so I use a Point to draw...
public Point Coordinates { get; set; }
// But I usually store my game state in a Vector2,
// so I need a convenient way to convert. If this
// were an interface, I'd have to write this code everywhere
public void SetPosition(Vector2 value) {
Coordinates = new Point((int)value.X, (int)value.Y);
}
// This is overridden by subclasses like AnimatedSprite and ParticleEffect
public abstract void Draw(SpriteBatch spriteBatch, Rectangle visibleArea);
}
子类定义自己的绘制逻辑。在你的坦克示例中,你可以尝试以下几种方法:
这是一个忽略了如何管理列表本身的ListDrawable的示例实现。
public class ListDrawable : Drawable {
private List<Drawable> Children;
// ...
public override void Draw(SpriteBatch spriteBatch, Rectangle visibleArea) {
if (Children == null) {
return;
}
foreach (Drawable child in children) {
child.Draw(spriteBatch, visibleArea);
}
}
}
像MOQ和Rhino Mocks这样的框架并不需要特定的接口。它们也可以模拟任何非密封和/或抽象类。Game是一个抽象类,所以你不应该有任何问题来模拟它 :-)
至少这两个框架需要注意的是,要对方法或属性设置任何期望值,它们必须是虚拟的或抽象的。原因是生成的模拟实例需要能够进行覆盖。IAmCodeMonkey提到的typemock我相信有一种方法可以解决这个问题,但我认为typemock不是免费的,而我提到的这两个是免费的。
另外,您还可以查看我的一个项目,它可以帮助创建XNA游戏的单元测试,而无需创建模拟:http://scurvytest.codeplex.com/
你不必嘲弄它。为什么不创建一个假游戏对象呢?
从Game继承,并覆盖你想在测试中使用的方法,返回你需要的方法或属性的固定值或快捷计算。然后将该假对象传递给你的测试。
在模拟框架出现之前,人们自己编写模拟/存根/假对象 - 也许不那么快速简单,但你仍然可以这样做。
您可以使用一个名为TypeMock的工具,我相信它不需要您拥有一个接口。您另外一个更常用的方法是创建一个新类,该类继承自Game并实现一个与Game对象匹配的接口。然后,您可以针对该接口编写代码,并传入您的“自定义”Game对象。
public class MyGameObject : Game, IGame
{
//you can leave this empty since you are inheriting from Game.
}
public IGame
{
public GameComponentCollection Components { get; set; }
public ContentManager Content { get; set; }
//etc...
}
虽然有点繁琐,但它可以让你实现模拟。
如果您不介意的话,我想借用一下您的帖子,因为我的似乎不太活跃,而您已经把您的知名度摆在了那里 ;)
当我阅读您的帖子(在这里和XNA论坛上)时,我认为可能是框架更易于接近,也可能是我(我们)的设计存在缺陷。
框架可能设计得更易于扩展。我很难相信 Shawn关于接口性能损失的主要论点。他的同事说可以轻松地避免性能损失。
请注意,框架已经具有IUpdatable和IDrawable接口。何不一步到位呢?
对于这样的问题,我建议先参考XNA WinForms Sample。通过使用该示例作为模型,似乎在WinForm中可视化组件的一种方法是创建一个控件,其样式与示例中的SpinningTriangleControl相同。这演示了如何在没有Game实例的情况下呈现XNA代码。实际上,Game并不重要,重要的是它为您提供了什么。因此,您需要创建一个Library项目,其中包含组件的Load/Draw逻辑,并在其他项目中创建一个Control类和一个Component类,它们分别是其各自环境中库代码的包装器。这样,您测试的代码不会重复,也不必担心编写始终在两种不同情况下都可行的代码。