如何提高我的JUnit测试

10

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

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

优点: 这些测试非常有效(非常擅长检测错误),并且非常稳定,因为它们仅使用API。如果我重构代码,则测试也会进行重构。由于我不使用“卑鄙的伎俩”,例如在给定状态下保存和重新加载数据库,因此我的测试对架构更改和实施更改视而不见。

缺点: 维护测试变得越来越困难,对任何测试的更改都会影响其他测试。测试用例运行8-9分钟,这对于持续集成非常好,但对开发人员来说有点沮丧。无法隔离地运行测试,最好的方法是在运行您感兴趣的测试后停止,但是必须绝对运行先前所有的测试。

您如何改进我的测试?


阅读其他人的代码,看看他们是如何做的。 - Paco
9个回答

11
首先,了解你所拥有的测试是整合测试(可能访问外部系统并涉及广泛的类)。单元测试应该更具体,这对已经构建好的系统来说是个挑战。实现这一点的主要问题通常是代码结构的方式:
即类与外部系统(或其他相关类)紧密耦合。为了能够做到这一点,您需要以这样的方式构建类,以便在单元测试期间可以避免访问外部系统。
更新1:阅读以下内容,并考虑所得到的设计将使您能够在不访问文件/数据库的情况下测试加密逻辑 - http://www.lostechies.com/blogs/gabrielschenker/archive/2009/01/30/the-dependency-inversion-principle.aspx(不是Java,但非常清楚地说明了问题)...还请注意,您可以对读者/编写器进行真正专注的集成测试,而无需全部测试。
我建议:
逐步在您的系统上包含真实的单元测试。在进行更改和开发新功能时,适当地重构,并包含相关的集成测试。确保能够将单元测试与集成测试分开运行。请注意,考虑到测试接近测试整个系统,因此与自动接受测试仅在操作API的边界上不同。基于这一点,请思考与产品中API的重要性相关的因素(例如是否会被外部使用),以及是否已经具备了自动接受测试的良好覆盖率。这有助于您理解在系统上拥有这些测试的价值,以及为什么它们自然需要很长时间。在界面级别或界面+API级别上决定是否将对整个系统进行测试。

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


如果单元测试需要运行8分钟或更长时间,那么它们绝对不是单元测试——在我的当前项目中,大约500个单元测试在小于10秒内运行。 - matt b

6

单元测试应该是独立的,并且能够以任何顺序运行。因此,我建议您:

  • 将测试拆分为独立的
  • 考虑使用内存数据库作为测试的后端
  • 考虑在每个测试或测试套件中包装一个事务,在结束时回滚
  • 对单元测试进行性能分析,查看时间花费在哪里,并集中精力解决问题

如果创建几个用户并发送几条消息需要8分钟,那么性能问题可能不在测试中,而是系统本身存在性能问题的症状 - 只有您的分析工具才知道!

[注意:我不认为这些测试是“集成测试”,尽管我可能是少数派;我认为这些测试是功能的单元测试,类似TDD]


Steven,你的意思是要改变整个测试方法,但你声称现有的测试不是集成测试?-它们正在测试与外部资源(即数据库)的集成,并且是影响系统的几个部分的相互关联的故事。 - eglasius
你修改的版本鼓励测试更具体的功能,避免与外部系统交互并且没有副作用以避免与其他测试产生相互关系(回滚)。当你说你不认为它们是集成测试时,你心中只有你自己的那个版本。 - eglasius
@[Freddy Rios]: 抱歉,我不理解你的评论。Esko建议的测试更符合TDD的原则。 - Steven A. Lowe
你说你不认为安迪现有的测试是集成测试,但是又建议修改使它们成为单元测试,我认为这可能会误导人们,话虽如此,我没有给你的回答点踩,因为我认为它提供了很好的额外信息。 - eglasius
关于TDD,请查看我回答的第二个更新。我们并不是在说完全不同的事情,只是添加了不同的信息片段。 - eglasius

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秒的时间,那么你会在每次更改后犹豫是否运行它们,并且你失去了运行测试所提供的快速反馈的一部分。(如果它涉及到数据库,那么它不是一个单元测试,而是一个系统或集成测试,它们有其用途,但执行起来不够快速。) 你需要通过模拟或存根来打破被测试类的依赖关系。此外,从你的描述中看,你的测试并不是孤立的,而是依赖于前面测试引起的副作用 - 这是不可取的。好的测试应该遵循FIRST原则。

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开发人员之一。


3

减少测试之间的依赖。这可以通过使用模拟实现。马丁·福勒在模拟不是存根 中详细介绍了为什么模拟能够减少测试之间的依赖。


2
如果您使用TestNG,您可以以多种方式注释测试。例如,您可以将上面的测试注释为长时间运行。然后,您可以配置自动化构建/持续集成服务器来运行这些测试,但标准的“交互式”开发人员构建不会运行它们(除非他们明确选择)。
这种方法取决于开发人员定期检入您的连续构建,以便测试确实得到运行!
一些测试不可避免地需要很长时间运行。关于性能的评论都是有效的。但是,如果您的测试确实需要很长时间,则务实的解决方案是运行它们,但不要让它们耗费太多开发人员的时间,以至于他们避免运行它们。
注意:您可以通过(例如)以不同方式命名测试并使您的持续构建运行特定的测试类子集来使用JUnit进行类似的操作。

1
通过像您描述的测试故事,您会得到非常脆弱的测试。如果只有一个小功能发生了变化,整个测试可能会出现问题。然后您可能需要更改所有受该更改影响的测试。
实际上,您所描述的测试更像是功能测试或组件测试,而不是单元测试。因此,您正在使用单元测试框架(junit)进行非单元测试。在我看来,如果您知道这一点,使用单元测试框架进行非单元测试并没有什么问题。
因此,有以下选项:
  • 选择另一个更好支持“讲故事”式测试的测试框架,就像其他用户已经建议的那样。您需要评估并找到一个合适的测试框架。

  • 使您的测试更像“单元测试”。因此,您需要拆分测试,并可能更改当前的生产代码。为什么?因为单元测试旨在测试小的代码单元(单元测试纯粹主张一次只测试一个类)。通过这样做,您的单元测试变得更加独立。如果您更改了一个类的行为,您只需要更改相对较少的单元测试代码。这使得您的单元测试更加健壮。在此过程中,您可能会发现您当前的代码不太支持单元测试--主要是由于类之间的依赖关系。这就是您还需要修改生产代码的原因。

如果您正在进行项目并且时间不足,这两个选项可能无法帮助您进一步。然后,您将不得不接受这些测试,但您可以尝试减轻痛苦:

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

  • 为您的测试添加另一层间接性:生成操作于更高抽象级别的辅助方法和辅助类。它们应该作为您测试的API。这些辅助程序正在调用您的生产代码。您的故事测试应该只调用这些辅助程序。如果有什么变化,您只需要更改API中的一个地方,而不需要触及所有测试。

您的API示例签名:

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

对于一般的单元测试,我建议参考Gerard Meszaros的XUnit测试模式

如果想要在生产测试中打破依赖关系,请参考Michael Feathers的《高效处理遗留代码》


0
除了上述内容,还可以找一本关于TDD的好书(我可以推荐《Java开发人员的TDD和验收TDD》)。即使它是从TDD的角度来看待问题,但仍有很多有用的关于编写正确类型的单元测试的信息。
找一个在这个领域有很多知识的人,并利用他们的经验来改进你的测试。
加入邮件列表来提问并阅读相关信息。例如雅虎的JUnit列表(类似于groups.yahoo.com/junit)。JUnit世界中的一些重要人物都在该列表上积极参与。
列出单元测试的黄金法则,并将它们贴在你(和其他人)的隔间墙上,例如:
  • 不得访问外部系统
  • 只能测试被测代码
  • 一次只能测试一件事情等。

0

既然其他人都在谈论结构,那我就选取不同的要点。这听起来像是一个很好的机会来对代码进行剖析,找出瓶颈,并通过代码覆盖率运行它,以查看是否遗漏了任何东西(考虑到运行所需的时间,结果可能非常有趣)。

我个人使用 Netbeans 分析器,但其他 IDE 也有分析器,也有单独的分析器。

对于代码覆盖率,我使用 Cobertura,但 EMMA 也可以(EMMA 有一个麻烦问题,而 Cobertura 没有...我忘记了具体是什么问题,而且现在可能已经没有影响了)。这两个是免费的,还有一些付费的也不错。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接