如何避免测试驱动开发中的访问者?

时间:2010-09-27 21:33:49

标签: java tdd

我正在学习测试驱动的开发,我注意到它会强制松散耦合的对象,这基本上是一件好事。然而,这有时也会迫使我为通常不需要的属性提供访问器,我认为SO上的大多数人都认为访问器通常是设计糟糕的标志。做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());
}

我必须查看对象的内部(好的,至少逻辑上)状态并查看位置是否正确更新。因为它实际上是在路上(我不希望我的测试用例依赖于我的图形库),所以我将绘图代码移动到了一个“Renderer”类:

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中编写用户界面,并且浏览器中还没有明确的绘图(所以),所以我认为不可能Entity类承担责任。此外,我希望将来可以将游戏移植到原生Java / Swing。

更新2:

再考虑一下这个问题,也许它的确如此。也许可以将实体和绘图分开,并且实体告诉其他对象足够自己绘制。我的意思是,我怎么能实现这种分离呢?如果没有它,我真的不知道如何生活。甚至伟大的面向对象程序员有时也会使用带有getter / setter的对象,特别是像实体对象这样的东西。也许getter / setter并非都是邪恶的。你觉得怎么样?

6 个答案:

答案 0 :(得分:5)

务实的程序员讨论tell, don't ask。您不想知道实体,您希望它被绘制。告诉它在给定的Graphics上绘制自己。

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

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

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

答案 1 :(得分:3)

你说你觉得你提出的Renderer课程是由TDD“特别强迫”的。那么,让我们来看看TDD引领你的地方。从负责其坐标和绘制自身的Rectangle类,到具有维护其坐标的单一责任的Rectangle类和具有单一责任的渲染器,以及呈现矩形。当我们说测试驱动时,这就是我们的意思 - 这种做法会影响您的设计。在这种情况下,它驱使你进入一种更贴近Single Responsibility Principle的设计 - 一种你没有去过的设计,没有经过测试。我认为这是件好事。我认为你正在练习TDD,我觉得它对你有用。

答案 2 :(得分:2)

所以,如果你没有从draw(Graphics)移动Entity方法,那么你有完全可测试的代码。您只需要注入Graphics的实现,该实现向您的测试工具报告Entity的内部状态。只是我的意见。

答案 3 :(得分:1)

首先,您是否了解java.awt.Rectangle类是如何在Java运行时库中处理此确切问题的?

其次,我相信TDD的真正价值在于它首先将你的注意力从“如何根据我假设的数据做这个具体细节”转移到“如何调用代码和我期待什么结果“。传统方法是“修复细节,然后我们将弄清楚如何调用代码”,反之亦然,如果有任何事情无法完成,你可以更快地找到它。

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

第二个值是你的测试是“活的评论”,而不是古老的,未被触及的评论。测试显示了如何调用代码,并且您可以立即验证其行为是否符合指定。这与你的要求无关,但是应该证明测试有更多的目的,然后只是盲目地调用你曾经写过的一些代码。

答案 4 :(得分:0)

您要测试的是您的对象如何响应某些调用,而不是内部如何工作。

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

如果你想看到一个方法调用和一个参数之间的交互,你应该以一种方式模拟所述参数,以便能够测试该方法是否按照你的意愿工作。

答案 5 :(得分:0)

我认为您的示例与此处使用的示例有很多共同之处:

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

使用您的原始示例并使用Hero替换Entity,使用jumpFrom(Balcony)替换fall(),并使move()作为moveTo(Room)变得非常相似。如果你使用模拟对象的方法史蒂夫弗里曼建议,你的第一个实现毕竟不是那么糟糕。我相信@Colin Hebert在指出这个方向时给出了最好的答案。这里没有必要暴露任何东西。您可以使用模拟对象来验证英雄的行为是否已经发生。

请注意,该文章的作者共同撰写了一本可以帮助您的好书:

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

作者提供了一些关于使用模拟对象来指导TDD设计的PDF文件。