我怎样才能改进我的junit测试

时间:2009-02-26 08:08:06

标签: java unit-testing spring junit

对我的junit测试看起来像一个长篇故事:

  • 我创建了4个用户
  • 我删除了1个用户
  • 我尝试使用已删除的用户登录并确保其失败
  • 我使用剩下的3个用户之一登录并验证我可以登录
  • 我从一个用户向另一个用户发送消息,并验证它是否显示在发件人的发件箱中以及收件人的收件箱中。
  • 我删除了消息
  • ...
  • ...

优点: 测试非常有效(非常擅长检测错误)并且非常稳定,因为它们只使用API​​,如果我重构代码,那么测试也会被重构。由于我不使用“脏技巧”,例如在给定状态下保存和重新加载数据库,我的测试无视模式更改和实现更改。

缺点: 测试变得难以维护,测试中的任何更改都会影响其他测试。测试运行8-9分钟,这对于持续集成非常有用,但对开发人员来说有点令人沮丧。测试不能孤立运行,你可以做的最好的事情是在你感兴趣的测试运行后停止 - 但是你必须运行之前的所有测试。

你会如何改进我的考试?

9 个答案:

答案 0 :(得分:11)

首先,了解您所拥有的测试是集成测试(可能访问外部系统并访问各种类)。单元测试应该更加具体,这对已经构建的系统来说是一个挑战。实现这一目标的主要问题通常是代码的构建方式:

即。类紧密耦合到外部系统(或其他类)。为了能够这样做,您需要以这样的方式构建类,以便在单元测试期间实际避免命中外部系统。

更新1:阅读以下内容,并认为最终的设计将允许您实际测试加密逻辑而无需访问文件/数据库 - http://www.lostechies.com/blogs/gabrielschenker/archive/2009/01/30/the-dependency-inversion-principle.aspx(不是在java中,而是ilustrates这个问题很好)...还要注意你可以为读者/作者做一个非常集中的集成测试,而不是一起测试它们。

我建议:

  • 逐步在您的系统中包含实际单元测试。您可以在进行更改和开发新功能时进行相应的重构。
  • 执行上述操作时,请在适当的位置添加重点集成测试。确保您能够运行与集成测试分开的单元测试。
  • 考虑您的测试接近于测试整个系统,因此与自动验收测试的不同之处仅在于它们在API的边界上运行。考虑到这一点,请考虑与产品API的重要性相关的因素(如果它将在外部使用),以及您是否具有良好的自动验收测试覆盖率。这可以帮助您了解在您的系统上使用这些内容的价值,以及它们自然需要这么长时间的原因。决定是否要在接口级别或接口+ api级别上对整个系统进行测试。

更新2:根据其他答案,我想澄清一些关于做TDD的事情。让我们说你必须检查一些给定的逻辑是否发送电子邮件,将信息记录在一个文件上,将数据保存在数据库中,并调用一个Web服务(不是我所知道的,但你开始为每个服务添加测试) 。在每次测试中,您不想访问外部系统,您真正想要测试的是逻辑是否会调用您期望它执行的那些系统。因此,当您编写检查在创建用户时发送电子邮件的测试时,您测试的是逻辑是否调用依赖性来执行此操作。请注意,您可以编写这些测试和相关逻辑,而无需实际发送电子邮件的代码(然后必须访问外部系统以了解发送的内容...)。这将帮助您专注于手头的任务,并帮助您获得分离的系统。它还可以简化测试发送到这些系统的内容。

答案 1 :(得分:5)

现在,您正在一种方法中测试很多东西(违反每次测试一次断言)。这是一件坏事,因为当这些事情发生变化时,整个测试都会失败。这导致它不能立即明显为什么测试失败以及需要修复的内容。此外,当您有意更改系统的行为时,您需要更改更多测试以对应更改的行为(即测试是脆弱的)。

要知道什么样的测试是好的,有助于阅读有关BDD的更多信息:http://dannorth.net/introducing-bdd http://techblog.daveastels.com/2005/07/05/a-new-look-at-test-driven-development/ http://jonkruger.com/blog/2008/07/25/why-behavior-driven-development-is-good/

为了改进您提到的测试,我将使用这些上下文和测试方法名称将其拆分为以下三个测试类:

创建用户帐户

  • 在创建用户之前
    • 用户不存在
  • 创建用户时
    • 用户存在
  • 删除用户时
    • 用户不再存在

登录

  • 当用户存在时
    • 用户可以使用正确的密码登录
    • 用户无法使用错误的密码登录
  • 当用户不存在时
    • 用户无法登录

发送消息

  • 当用户发送消息时
    • 邮件显示在发件人的发件箱中
    • 消息显示在收件人的收件箱中
    • 该消息未出现在任何其他消息框中
  • 删除邮件时
    • 消息不再存在

您还需要提高测试速度。你应该有一个覆盖良好的单元测试套件,它可以在几秒钟内运行。如果运行测试花费的时间超过10-20秒,那么每次更改后您都会犹豫不决,并且会丢失一些运行测试的快速反馈。 (如果它与数据库对话,它不是一个单元测试,而是一个系统或集成测试,它有它们的用途,但速度不够快,不能连续执行。)你需要打破被测试类的依赖关系{ {3}}他们。同样根据您的描述,您的测试似乎不是孤立的,而是测试取决于先前测试引起的副作用 - 这是禁忌。好的测试是mocking or stubbing

答案 2 :(得分:5)

单元测试应该 - 理想情况下 - 独立,并且能够以任何顺序运行。所以,我建议你:

  • 将您的测试分解为独立
  • 考虑使用内存数据库作为测试的后端
  • 考虑将每个测试或套件包装在最后回滚的事务中
  • 描述单元测试以查看时间进度,并专注于

如果创建一些用户并发送一些消息需要8分钟,性能问题可能不在测试中,而这可能是系统本身性能问题的症状 - 只有您的探查器确切知道!

[警告:我不认为这些类型的测试是'整合测试',尽管我可能属于少数;我认为这些类型的测试是功能的单元测试,一个TDD]

答案 3 :(得分:3)

减少测试之间的依赖关系。这可以通过使用Mocks来完成。 Martin Fowler在Mocks aren't stubs中谈到它,特别是为什么模拟减少了测试之间的依赖关系。

答案 4 :(得分:3)

您可以使用JExample,它是JUnit的扩展,允许测试方法具有其他测试重用的返回值。 JExample测试使用Eclipse中的普通JUnit插件运行,并且还与正常的JUnit测试并行工作。因此,迁移应该没有问题。 JExample使用如下

@RunWith(JExample.class)
public class MyTest {

    @Test
    public Object a() { 
        return new Object();
    }

    @Test
    @Given("#a")
    public Object b(Object object) { 
        // do something with object
        return object; 
    }

    @Test
    @Given("#b")
    public void c(Object object) { 
        // do some more things with object
    }

}

免责声明,我是JExample开发人员之一。

答案 5 :(得分:2)

如果您使用TestNG,则可以通过各种方式注释测试。例如,您可以将上面的测试注释为长时间运行。然后,您可以配置自动构建/持续集成服务器来运行这些服务器,但标准的“交互式”开发人员构建不会(除非他们明确选择)。

这种方法取决于开发人员定期检查您的持续构建,以便测试确实运行!

某些测试将不可避免地需要很长时间才能运行。这个帖子中的评论重新开始。表现都是有效的。但是,如果您的测试需要很长时间,那么实用的解决方案就是运行它们,但不要让它们耗时的性质影响开发人员以避免运行它们。

注意:您可以通过(比方说)以不同的方式命名测试并使您的连续构建运行特定的测试类子集来执行与JUnit类似的操作。

答案 6 :(得分:1)

通过测试你描述的故事,你会有非常脆弱的测试。如果只有一小部分功能发生变化,那么整个测试可能会搞砸。然后,您可能会更改受此更改影响的所有测试。

事实上,您所描述的测试更像是功能测试或组件测试,而不是单元测试。因此,您使用单元测试框架(junit)进行非单元测试。在我看来,使用单元测试框架进行非单元测试没有错,如果(并且只有)你知道它。

所以有以下选项:

  • 选择另一个支持“故事讲述”式测试方式的测试框架,就像其他用户已经建议的那样。您必须评估并找到合适的测试框架。

  • 让您的测试更像“单元测试”。因此,您需要分解测试并更改当前的生产代码。为什么?因为单元测试旨在测试小单元代码(单元测试纯粹主义者建议一次只有一个类)。通过这样做,您的单元测试变得更加独立。如果更改一个类的行为,则只需更改相对少量的单元测试代码。这使您的单元测试更加稳健。在此过程中,您可能会发现当前代码不能很好地支持单元测试 - 主要是因为类之间存在依赖关系。这就是您还需要修改生产代码的原因。

如果您在某个项目中并且时间不够用,那么这两个选项可能对您没有任何帮助。那么你将不得不忍受这些测试,但你可以尝试减轻你的痛苦:

  • 删除测试中的代码重复:就像在生产代码中一样,消除代码重复并将代码放入辅助方法或辅助类中。如果有变化,您可能只需要更改辅助方法或类。这样你就会收敛到下一个建议。

  • 在测试中添加另一个间接层:生成在更高抽象级别上运行的辅助方法和辅助类。它们应该作为测试的API。这些助手正在调用您的生产代码。你的故事测试应该只调用那些助手。如果有变化,您只需更改API中的一个位置,而无需触及所有测试。

API的示例签名:

createUserAndDelete(string[] usersForCreation, string[] userForDeletion);
logonWithUser(string user);
sendAndCheckMessageBoxes(string fromUser, string toUser);

对于一般的单元测试,我建议您查看XUnit Test Patterns from Gerard Meszaros

要在生产测试中打破依赖关系,请查看Working Effectively with Legacy Code from Michael Feathers

答案 7 :(得分:0)

除了上述内容之外,还可以阅读一本关于TDD的好书(我可以推荐“适用于Java开发人员的TDD和验收TDD”)。即使它从TDD的角度来看,也有很多关于编写正确的单元测试的有用信息。

找到在该地区拥有大量知识的人,并使用它们来弄清楚如何改进测试。

加入邮件列表以提问并阅读流量。雅虎的JUnit列表(类似groups.yahoo.com/junit)。 JUnit世界中的一些推动者和振动者都在该名单上并积极参与。

获取单元测试的黄金规则列表并将它们粘贴在您的(和其他)隔间墙上,例如:

  • 您永远不会访问外部系统
  • 您只能测试测试中的代码
  • 你只能一次测试一件事 等

答案 8 :(得分:0)

由于其他人都在谈论结构,我会选择不同的观点。这听起来像是一个很好的机会来分析代码以找到瓶颈并通过代码覆盖来运行它以查看您是否遗漏了任何东西(考虑到运行它所花费的时间,结果可能很有趣)。

我个人使用Netbeans分析器,但其他IDE中也有一些,也可以单独使用。

对于代码覆盖率,我使用Cobertura,但EMMA也有效(EMMA有一个Cobertura没有的烦恼......我忘了它是什么,它可能不再是一个问题) 。这两个是免费的,有付费的也是免费的。