【问题标题】:How can I improve my junit tests如何改进我的junit测试
【发布时间】:2010-10-10 00:43:03
【问题描述】:

是的,我的 junit 测试看起来很长:

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

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

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

您将如何改进我的测试?

【问题讨论】:

  • 阅读其他人的一些代码,看看他们是如何做到的。

标签: java unit-testing spring junit


【解决方案1】:

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

即类与外部系统(或其他类)紧密耦合。为了能够做到这一点,您需要以这样一种方式构建类,即您实际上可以避免在单元测试期间触及外部系统。

更新 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 个单元测试在
【解决方案2】:

您可以使用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】:

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

    • 将您的测试拆分为独立的
    • 考虑使用内存数据库作为测试的后端
    • 考虑将每个测试或套件包装在一个最终回滚的事务中
    • 分析单元测试以了解时间的去向,并专注于此

    如果创建几个用户并发送一些消息需要 8 分钟,则性能问题可能不在测试中,而这可能是系统本身性能问题的征兆 - 只有您的分析器才能确定!

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

    【讨论】:

    • Steven,所以您基本上是说要改变整个测试方法,但您声称现有的不是集成测试? - 他们正在测试与外部资源(即 db)的集成,并且是一个相互关联的故事,会影响系统的多个部分。
    • 相反,您的修改版本促进了测试更具体的功能,避免撞击外部系统并且没有副作用以避免与其他测试(回滚)的相互关系。当您说您不考虑集成测试时,您只是想到了您的测试版本;)
    • @[Freddy Rios]:对不起,我不懂你们的cmets。 Esko 提议的测试将更符合 TDD。
    • 您说您不考虑安迪现有的测试集成测试,但随后提出使它们成为单元测试的更改,我认为这会误导人们,说反对意见不是我考虑的那样你的回答提供了很好的额外信息
    • 关于 TDD,请查看我对答案的第二次更新。我们并没有说完全不同的东西,只是添加了不同的信息。
    【解决方案4】:

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

    实际上,您所描述的测试更像是功能测试或组件测试,而不是单元测试。因此,您正在使用单元测试框架(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

    【讨论】:

      【解决方案5】:

      现在你正在用一种方法测试很多东西(违反了每个测试一个断言)。这是一件坏事,因为当其中任何一个发生变化时,整个测试都会失败。这导致无法立即明确测试失败的原因以及需要修复的内容。此外,当您有意更改系统的行为时,您需要更改更多测试以对应更改的行为(即测试是脆弱的)。

      要了解什么样的测试是好的,请阅读更多关于 BDD 的内容:http://dannorth.net/introducing-bddhttp://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 秒,那么每次更改后您都会犹豫是否运行它们,并且您会失去运行测试为您提供的一些快速反馈。 (如果它与数据库对话,它不是单元测试,而是系统或集成测试,它们有其用途,但速度不够快,无法持续执行。)您需要通过 @ 打破被测类的依赖关系987654324@他们。同样从您的描述来看,您的测试似乎不是孤立的,而是测试取决于先前测试引起的副作用 - 这是一个禁忌。好的测试是FIRST

      【讨论】:

        【解决方案6】:

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

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

        有些测试不可避免地需要很长时间才能运行。此线程中的 cmets 重新。表现都是有效的。但是,如果您的测试确实需要很长时间,实用的解决方案是运行它们,但不要让它们耗时的性质影响到开发人员避免运行它们的地步。

        注意:您可以通过(例如)以不同方式命名测试并让您的连续构建运行特定的测试类子集,从而对 JUnit 做类似的事情。

        【讨论】:

          【解决方案7】:

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

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

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

          【讨论】:

            【解决方案8】:

            除了以上内容,再找一本关于TDD的好书(我可以推荐《Java Developers TDD and Acceptance TDD》)。即使从 TDD 的角度来看,它也有很多关于编写正确类型的单元测试的有用信息。

            找一个在该领域有很多知识的人,并用他们来弄清楚如何改进你的测试。

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

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

            • 您不得访问外部系统
            • 您只能测试被测代码
            • 您一次只能测试一件事 等

            【讨论】:

              【解决方案9】:

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

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2011-07-23
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2010-10-12
                • 2021-01-24
                相关资源
                最近更新 更多