2010-09-27 65 views
15

我正在学习测试驱动的开发,我注意到它强制松耦合的对象,这基本上是一件好事。但是,这有时也会迫使我为通常不需要的属性提供访问器,并且我认为大多数人都赞同访问器通常是不良设计的标志。在进行TDD时这是不可避免的吗?如何避免测试驱动开发中的访问者?

下面是一个例子,一个实体的简化绘图代码不TDD:

class Entity { 
    private int x; 
    private int y; 
    private int width; 
    private int height; 

    void draw(Graphics g) { 
     g.drawRect(x, y, width, height); 
    } 
} 

实体知道如何绘制自己,这是很好的。所有在一个地方。但是,我正在做TDD,所以我想检查我的实体是否被我将要实现的“fall()”方法正确移动。下面是测试情况可能是什么样子:

@Test 
public void entityFalls() { 
    Entity e = new Entity(); 
    int previousY = e.getY(); 
    e.fall(); 
    assertTrue(previousY < e.getY()); 
} 

我得看看对象的内部(很好,至少在逻辑上)的状态,看看位置是正确更新。因为它实际上是在路上(我不希望我的测试用例依赖于我的图形库),我感动的绘制代码的类“渲染”:

class Renderer { 
    void drawEntity(Graphics g, Entity e) { 
     g.drawRect(e.getX(), e.getY(), e.getWidth(), e.getHeight()); 
    } 
} 

松耦合,不错。我甚至可以用一种以完全不同的方式显示实体的渲染器。但是,我必须公开实体的内部状态,即所有属性的访问器,以便渲染器可以读取它。

我觉得这是TDD特别强迫的。我能做些什么呢?我的设计可以接受吗? Java是否需要C++中的“friend”关键字?

更新:

感谢您的宝贵意见为止!但是,我担心我选择了一个不好的例子来说明我的问题。这是完全编造的,我现在将展示一个更接近我的实际代码:

@Test 
public void entityFalls() { 
    game.tick(); 
    Entity initialEntity = mockRenderer.currentEntity; 
    int numTicks = mockRenderer.gameArea.height 
        - mockRenderer.currentEntity.getHeight(); 
    for (int i = 0; i < numTicks; i++) 
     game.tick(); 
    assertSame(initialEntity, mockRenderer.currentEntity); 
    game.tick(); 
    assertNotSame(initialEntity, mockRenderer.currentEntity); 
    assertEquals(initialEntity.getY() + initialEntity.getHeight(), 
       mockRenderer.gameArea.height); 
} 

这是一个基于游戏循环实现游戏的一个实体可以掉下来,等等。如果碰到地面,就会创建一个新的实体。

“mockRenderer”是接口“Renderer”的模拟实现。这个设计部分是由TDD强制的,但也是因为我要在GWT中编写用户界面,并且浏览器中没有明确的绘图(但是),所以我认为这是不可能的实体类承担责任。此外,我想保留未来将游戏移植到本地Java/Swing的可能性。

更新2:

想到这里多一些,也许这是好是怎么回事。也许可以将实体和图形分开,并且实体可以告诉其他对象足以描绘自己。我的意思是,我还能如何实现这种分离?没有它,我真的不知道如何生活。即使是伟大的面向对象的程序员有时也会使用getter/setter对象,特别是对于像实体对象这样的东西。也许getter/setter并不全是邪恶的。你怎么看?

+1

这样看待:如果'Entity'没有'getY',你需要测试它吗?从理论上讲,任何其他代码都无法访问的内容是“实体”的内部特征,对用户而言并不重要,并且通过扩展测试。 – 2010-09-27 21:45:26

+0

但我可能会补充说游戏(这看起来是)对于单元测试来说是一件特别困难的事情。 – 2010-09-27 21:46:25

+0

java的“friend关键字”是默认访问修饰符,同一个包中的每个类都可以访问没有访问修饰符(不公开,私有或受保护)的字段和方法。只要它们使用相同的包结构,测试就可以访问这些字段,因此您可以将这些详细信息保留为包。 – josefx 2010-09-27 21:59:19

回答

3

你说你觉得你想出的Renderer类是TDD“特别强迫”的。那么,让我们来看看TDD在哪里引领你。从负责其坐标和绘制自身的Rectangle类到具有维护其坐标的单一责任的Rectangle类以及具有单一责任的呈现器,以及渲染Rectangle。这就是我们说测试驱动时的意思 - 这种做法会影响您的设计。在这种情况下,它会将您推向更紧密地遵循Single Responsibility Principle的设计 - 这是一种您不需要进行测试的设计。我认为这是件好事。我认为你正在练习TDD,而且我认为它适合你。

5

语用程序员讨论tell, don't ask。你不想知道该实体,你希望它被绘制。告诉它自己绘制给定的图形。

您可以重构上面的代码,以便实体执行绘图,如果实体不是矩形,而是实际上是一个圆形,这很有用。

void Entity::draw(Graphics g) { 
    g.drawRect(x,y, width, height); 
} 

然后你会检查g是否在你的测试中调用了正确的方法。

+0

如上所述,我曾经有实体的绘制方法,但我故意将它移开以避免与图形系统紧密耦合。 – Noarth 2010-09-27 22:07:05

+0

我认为这是一个图形抽象的问题,而不是实体抽象。就像在MVC中一样,你需要在图形的具体和抽象之间进行抽象。 – 2010-09-28 00:22:03

0

你想测试的东西是你的对象将如何响应某些调用,而不是如何在内部工作。

因此,访问不可访问的字段/方法并不是真的必要(也不是一个好主意)。

如果您想查看方法调用和其中一个参数之间的交互,您应该嘲笑所述参数,以便能够测试该方法是否按照您的想法工作。

2

那么,如果你没有移动draw(Graphics)方法从Entity你有完美的测试代码。您只需要注入一个Graphics的实现,它将内部状态Entity报告给您的测试工具。只是我的意见。

1

首先,你是否知道java.awt.Rectangle这个类是如何在Java运行时库中处理这个确切的问题?其次,我相信TDD的真正价值在于它首先将你的注意力从“我该怎么做这个特定的细节与我所假设的数据这样做”转移到“我如何调用代码我期待什么结果“。传统的方法是“修复细节,然后我们会弄清楚如何调用代码”,反过来这样做可以让你在任何事情都无法完成的情况下更快地找到它。

这在设计API时非常重要,这很可能也是您发现结果松散耦合的原因。

第二个值是您的测试是“活的评论”,而不是古老的,未触动的评论。该测试显示应如何调用您的代码,并且您可以立即验证它的行为是否符合规定。这与你所要求的无关,但应该证明测试有更多的目的,然后只是盲目地调用你之前编写的代码。

0

我觉得你的例子有很多共同的例子在这里使用:

http://www.m3p.co.uk/blog/2009/03/08/mock-roles-not-objects-live-and-in-person/

使用你原来的例子,并与英雄代替实体,秋天()与jumpFrom(阳台),并绘制()作为moveTo(房间)它变得非常相似。如果你使用Steve Freeman建议的模拟对象方法,那么你的第一个实现并不是那么糟糕。我相信@Colin Hebert在指出这个方向时给出了最好的答案。这里没有必要暴露任何东西。您使用模拟对象来验证英雄的行为是否已经发生。

注意文章的作者已经合写了一本伟大的书,可以帮助你:

http://www.amazon.com/Growing-Object-Oriented-Software-Guided-Tests/dp/0321503627

有免费提供的,通过关于使用模拟对象,引导作家PDF一些好论文您在TDD中的设计。

+0

我不认为我可以使用模拟对象进行绘制,因为我将为GWT实现一个渲染器。我无法真正为GWT创建一个类似Graphics的对象,因为GWT中没有绘图,DOM元素只是修改,并且我不想在我的核心游戏逻辑包中使用DOM元素,所以这不起作用。 – Noarth 2010-09-28 18:21:03

+0

此外,我不认为draw方法与moveTo方法相同。我的绘制方法是低级显示绘制,而moveTo是一个逻辑运算,我会测试一个。 – Noarth 2010-09-28 21:00:11