【问题标题】:How do you speed up java unit tests?如何加快 Java 单元测试?
【发布时间】:2009-06-18 06:50:35
【问题描述】:

目前我们的项目有超过 3000 个单元测试,“ant testAll”需要 20 多分钟。 除了获得更好的硬件,还有什么方法可以加快速度?

【问题讨论】:

  • 您应该更具体地说明是什么阻碍了这些测试。
  • 我在两个月前才加入这个项目,它已经有了一个庞大的代码库。它不是一个网络应用程序,除了一堆 xml 配置文件之外,它也不经常执行文件 IO。我们正在使用我们定制的 TestRunner。但问题是我们只为系统开发插件,因此我们没有 TestRunner 的源代码。我假设 TestRunner 的实现者足够聪明,可以优化设置内容,因此,例如,它不必在每次测试运行时解析 xml 配置。
  • 您是否使用模拟对象来模拟单元测试中访问缓慢的对象?在工作中,我们有 6000 个单元测试,它们运行大约需要一分钟或两分钟。

标签: java performance unit-testing ant


【解决方案1】:

我建议有两个构建(增量构建在每次签入时运行,完整构建在夜间运行)

增量版本在大约 7 分钟内运行较短的测试,而完整版本在不到 40 分钟的时间内运行所有测试。

Clearcase 确实鼓励了分支噩梦,但您应该能够为每个开发人员构建两个版本。我会质疑让每个开发人员都在自己的分支上开发的价值,因为我相信让开发人员在同一个分支上(成对或更多)一起工作会有一些好处。

注意:一台持续集成服务器可以有任意数量的代理,如果您负担不起一台以上的服务器,您可以使用 PC 作为构建代理。 (你必须至少有 30 个)

【讨论】:

  • 你是对的。并非每个开发人员都在自己的分支上工作。只有修复错误的开发人员才能在他们的分支上工作。功能开发人员在同一个分支上工作。
【解决方案2】:

您是否在junit 通话中使用fork="yes"?如果是这样,请确保设置forkMode="once",否则 junit 任务将为每个 TestCase 类启动一个新 VM。有了 3000 个单元测试,这将产生巨大的影响。

http://ant.apache.org/manual/Tasks/junit.html

【讨论】:

  • 我看过这个。问题是我们没有使用 ant 的 junit TestRunner,而是在 jar 中定制的。我不知道它是如何实现的,因为我们没有源代码:(.
  • 请注意,带有像 jadclipse 这样的反编译器的 Eclipse 可以帮助您窥视这样的 jar。把它放在构建路径上,看看。
  • 查看 build.xml 文件,TestRunner 不会为每个测试用例启动一个 jvm。感谢您推荐jadclipse,我相信它将来会派上用场。
【解决方案3】:

开始:
a) 获取有关 Junit 测试运行时间的统计信息。您可能已经在测试报告中获取了这些信息。
b) 取出前 10 个测试课程(按时间计算)并尝试减少时间。您需要持续进行此操作。
c) 尝试通过重构甚至改变测试方法来减少运行时间。
我遇到的一个这样的案例是在一个用于 CRUD 测试用例的测试类中。更新测试用例首先创建功能然后更新。但是我们已经在单独的测试用例中测试过创建。所以在这些情况下,您可以链接您的测试用例,例如

@Test()
    public void testCreate() throws Exception
    {}
    @Test(dependsOnMethods = "testCreate")
    public void testAmend() throws Exception
    {}
    @Test(dependsOnMethods = "testAmend")
    public void testDelete() throws Exception
    {} 

因此,您可以省去重复测试。

d)我能够显着减少时间的另一个例子是。 我们有一个系统(inherietd),其中每个测试用例都调用 SetUp(Strating Spring Server 等)并在运行后关闭系统资源。这非常耗时,所以我重构它以在测试套件之前和整个套件之后启动 coomon 资源完成然后关闭它们。

e) 根据您的项目,它们可能是您可能需要解决的其他瓶颈。

How to manage Build time in TDD

【讨论】:

    【解决方案4】:

    我假设您已经完成了所有其他常用步骤,例如模拟数据库调用、优化测试设置阶段等.

    如果是这样,一种方法是多线程测试运行。Test-NG 非常支持这一点。将您的测试从 junit 转换为 test-ng 并不难,只需要完成一次。

    可以轻松标记必须按顺序运行的测试:

    @Test(sequential = true)
    public class ATest {
      ...
    

    在多核机器上,您会看到运行时的巨大改进。即使在单核上,您也会看到很好的改进,因为一些线程等待 io 操作。

    有关如何设置的详细信息,请参见此处:

    http://beust.com/weblog/archives/000407.html

    希望这会有所帮助。

    ....

    更多建议 - 我不敢相信您没有使用持续集成。相信我,30 名开发人员不会让你的 CI 服务器超载。即使您无法购买 CI,也可以在您自己的机器上安装 hudson - 设置需要 10 分钟,而且好处是巨大的。询问您的经理,每个开发人员都在等待单元测试完成或让服务器为您完成,哪个更糟糕。为破坏构建的人戴上一顶愚蠢的帽子通常足以说服开发人员运行他们的单元测试。

    如果签入的质量确实是一个大问题(不要忘记签入总是可以回滚)考虑Teamcity - 它会运行测试并且如果测试失败则不会提交代码。

    最后,一个可能适合您的用例的选项也是clover and bamboo最新版本会记录哪些代码由哪些测试进行测试,以及何时进行更改,它只运行相关测试。这可能非常强大。

    但请记住,像 test-ng、teamcity 和 clover 这样的聪明工具只会让您走这么远 - 好的测试不会自己编写!

    总结一下我的解决方案是尝试以下全部或部分步骤:

    1. 优化测试 - 模拟、通用设置等。
    2. 并行运行测试
    3. 获取其他东西来为您运行测试 - 使用 hudson 或类似工具使其成为离线任务
    4. 只运行需要运行的测试 - 将它们分类成包或使用三叶草和竹子。

    【讨论】:

      【解决方案5】:

      与加速任何其他代码的方式相同。找出哪些测试花费的时间最多,并了解如何优化它们。

      有很多操作可能会很慢,如果您执行 3000 次,就会累加起来。有时,在测试之间重用数据是值得的(即使您不应该在单元测试中这样做,如果这样做可以让您的测试以可接受的速度运行,那么这可能是必要的)。

      为您的测试计时。通常,它们中的 90% 几乎会立即执行,而最后 10% 几乎会占用所有时间。找到那 10% 的人,看看他们在做什么。

      通过探查器运行代码,并注意时间花在哪里。猜测是浪费时间。你认为测试运行者在做什么是没有意义的。 找出它在做什么,而不是猜测。然后你就会知道如何加快速度。

      【讨论】:

        【解决方案6】:

        一些想法:

        • 使用模拟框架来避免访问数据库(或进行 Web 服务调用等)。
        • 如果您为许多单独的测试进行相同或类似的设置,请尝试在测试夹具设置中进行(即每个夹具完成一次而不是每次测试一次)。
        • 我相信某些测试框架允许您并行运行测试

        【讨论】:

        • 我发现的两个最好的模拟框架是 Mockito 和 JMockit。 JMockit 允许模拟静态和最终方法,而 Mockito 非常适合快速模拟。结合这两个作品很棒。
        • 在盲目地开始模拟所有可模拟的东西之前,请先搜索一下模拟的缺点。我已经从事企业 Java 应用程序工作十年了,过度模拟不是你想要的。
        • @Marty 过度嘲笑是不好的。那是因为如果我们模拟太多,我们就会错过测试实际的真实对象交互(通常是一些集成测试)?还是有更好的理由?谢谢
        • 你真的不想在单元测试中测试交互,也许是你说的集成测试。您正在复制您模拟的对象的大部分内部逻辑。例如。在模拟上,您需要 .expect(mock.one()).expect(mock.two()).expect(mock.three()) 这需要了解该对象的内部工作原理。如果情况发生变化,您必须重构所有测试。一开始它很好也很容易,但在更广泛使用时应该加一点盐。也可以使用存根代替。
        • 我同意,如果我们在模拟被测对象的某些部分,应该特别小心。
        【解决方案7】:

        如果不进一步了解正在测试的内容,唯一容易呈现的两种方法是:

        • 使用更好的硬件(抱歉)
        • 简化测试逻辑

        您甚至可能想在测试运行中运行分析器,看看是否有任何特别低效的测试实现。

        【讨论】:

          【解决方案8】:

          您可能希望将单元测试拆分为套件。您的应用程序是模块化的吗?您真正需要多久运行一次所有测试?如果您的开发人员只运行与他们自己的模块相关的单元测试,并且您在夜间和/或在 CI 上运行一系列测试,这是否可以接受?

          是否有任何非常复杂的特定单元测试(我知道,我在这里滑入功能和集成测试,但这条线有时很模糊),但是它们中的任何一个都可以在 sanity-level 在开发过程中,然后在 CI 上用完?

          编辑:为了好玩,我将简要描述一下我以前的一个项目中的测试例程

          首先,测试系统的增长是有机的,这意味着它不是最初计划出来的,而是随着它的增长而修改和变化的。因此,它并不完美,而且随着时间的推移,一些命名约定已成为杜撰。

          • 在开发人员级别,我们使用了一个名为 CheckIn 的简单两分钟测试套件,它验证了代码足够健康,可以加入主干。
          • 除此之外,我们还在 CI 机器上连续运行了健全性测试。这些是更复杂的集成和功能测试、所有单元测试和所有回归测试的简化版本。
          • 复杂的测试套件(以小时为单位)在白天和晚上远程运行,并在第二天早上编译结果。

          自动测试 - 这是笨蛋的疯狂。

          【讨论】:

          • 这是个坏建议。您应该运行尽可能多的测试,以确保所有组件独立且协同工作。您永远不能假设系统的某个方面“正常工作”,因为您没有针对该领域进行开发。
          • “一起”的东西会经过其他一些测试,比如集成测试或其他东西。单元测试是关于单元的,所以单元应该独立工作。并不意味着我们不需要一起测试所有东西,但这不是单元测试,IMO。
          • @atc:那么您从未遇到过整个测试套件(集成测试和所有测试)需要几个小时的场景。在提交之前,开发人员所需要做的就是确保理智,即基本功能。此测试不应超过几分钟,否则您将有空闲的开发人员。确保一切正常都可以在晚上完成。
          • @Mike Kushner:我们正处于您描述的确切情况。最糟糕的情况是,当您等待单元测试完成将近 20 分钟时,有人将另一个变更集推送到存储库,然后您需要拉取变更并重新运行测试。
          【解决方案9】:

          显然你的测试中有一些东西需要很长时间。

          有时,您无法绕过缓慢的测试。例如,测试 Spring 是否可以读取其所有配置文件,测试 hibernate 映射是否有效,诸如此类。这些测试的好处是它们只需要在一个测试中运行,然后你可以全部模拟出来,但你也可以决定将它们作为集成测试的一部分运行,让构建服务器担心它。

          其余的测试很慢,要么是因为它们正在执行 IO,要么是因为它们过度占用 CPU。

          IO 可以是很多东西。 Web 服务和数据库调用可以被抽象出来和模拟,如果需要的话,你必须做的几个真正的调用可以移动到集成阶段。日志记录也可以真正减慢速度——尤其是 3.000 个测试用例。我想说的是完全关闭日志记录,并在测试失败时依靠你的大脑和调试器。

          在某些情况下,IO 本身就是被测试的单元。例如,如果您正在测试将表数据写入磁盘的数据库服务器部分。在这种情况下,尽量在内存中保留尽可能多的 IO。在 Java 中,许多 IO 抽象都有内存实现。

          CPU 边界测试也有不同的风格。纯性能和吞吐量测试应该在集成测试阶段。如果您正在启动一堆线程来尝试检查并发错误,那么您可能会将大型测试移至集成阶段,并在您的常规测试套件中保留一个“轻量级”版本。

          最后,分析器是你的朋友。很可能您的部分代码可以变得更有效率,并显着加快您的测试速度。

          【讨论】:

          • 它不是一个网络应用程序。所以我相当有信心它不受 IO 限制。如果真的是一些需要非常长时间的测试用例。我想这个问题更容易解决。谢谢,我试试 jprofiler。
          【解决方案10】:

          好吧,我不知道你的单元测试在做什么,但你需要问自己为什么它需要 20 分钟。 根据我的经验,通常有很多测试在几毫秒内运行,而剩下的所需时间很少。大多数情况下,这些测试涉及 IO/network/DB-Stuff。 例如,如果您因网络延迟而花费大量时间等待,则可以考虑并行运行测试。

          您可以搜索这些测试并寻求改进。但是让你的测试更快并不会让你的实际代码更好。 您可能要注意需要大量时间的测试,因为被测类不是最佳的。查明和改进此类情况很可能也会使您的产品更好/更快。

          【讨论】:

            【解决方案11】:

            我同意巴勃罗吉姆的观点。并行化您的测试。我们正在使用 clearcase 并从视图服务器传输所有内容确实减慢了速度。当我们在 Duelcore 上进行并行化时,我们得到了 6-8 倍的更短测试运行速度。

            我们正在使用 CPPUnit 框架,并且我们刚刚添加了一个 python 脚本来在不同的线程上启动不同的测试套件。

            我们还使用 clearmake 来并行化构建过程。我们的下一步可能是在开发人员的客户端上并行化测试。

            【讨论】:

            【解决方案12】:

            在连续集成引擎系统中移动完整的测试套件,因此开发人员不必每次都运行它们。这样的系统比开发者更有耐心。

            【讨论】:

            • 我建议了这种方法。但是,经理担心开发人员会变得懒惰,并且在提交代码之前根本没有人运行测试用例。为了保证主分支的质量,我们可以为每个分支设置一个持续构建。我们有大约 30 名开发人员,这可能会使 CI 服务器过载。但是,是的,这值得一试,看看效果如何。
            • 如果用户在签入前未能检查代码,那么您的经理应该与他们一起解决这个问题。我不建议您使用懒惰的管理器;) IntelliJ/TeamCity 有一个功能,允许您在服务器上执行构建,并且只有在所有测试都通过时才签入代码。持续集成可以节省大量时间,你真的应该拥有一个。
            • @James,看看 Hudson。它很容易扩展,因为它允许使用从属计算机来构建。如果目前运行单元测试很痛苦,那么就这样做,然后让破坏构建的人不得不为整个团队购买一些东西(只是一些小东西)。
            • @Peter,我不明白你的意思。您是认真的意思是所有开发人员都应该在每次签入前进行 20 分钟的测试吗?这意味着人们只在他们必须检查时才检查,而不是在每个逻辑操作(修复错误 A、提交、修复错误 B、提交等)之后。我想经理也不会喜欢的。
            【解决方案13】:

            我会像解决任何其他性能问题一样解决这个问题:

            1. 不要假设问题是什么
            2. 使用分析器分析测试执行以确定热点
            3. 一次分析一个热点,在每次代码更改后重新测试。

            您可能会发现最终必须深入研究该测试运行程序。您可以使用像cavaj 这样的反编译工具从类文件中生成源代码(虽然它显然比原始代码更难阅读)。您可能会发现测试运行器实现中的某些内容正在影响性能。例如,您已经提到读取 XML 配置文件作为测试运行程序执行的一项活动——这可能会影响性能。

            您最终可能会发现性能问题的另一个领域是自定义“基础”测试用例类。这些往往会增加很多便利性,但很难记住您的便利性添加行为可能会在一个大型项目中分摊到 10k 次测试中,无论每个测试是否需要便利行为。

            【讨论】:

              【解决方案14】:

              这是我将采取的方法。

              1. 查看您的测试用例,寻找任何多余的测试。通过 3000 次测试,您很可能会覆盖不必要的部分。
              2. 挑选你的“金丝雀”。这些是您要始终运行的测试,它们会在其他部分嗅出危险。它们很可能是测试组件之间使用的公共 API 接口的更高级别的测试用例。如果其中一个失败,您可以进入并运行该组件的完整测试套件。
              3. 开始迁移到 TestNG 之类的框架并开始对测试用例进行分类,然后通过每晚的完整测试仅运行您正在处理的内容的分类。

              【讨论】:

                【解决方案15】:

                加速大型测试套件的最有效方法是逐步运行它,以便仅重新执行自上次测试运行以来涉及代码更改的测试。毕竟,最快的测试总是那些没有执行的。 8^)

                困难的部分实际上是让它发挥作用。我目前正在为 JUnit 4 进行增量测试,它是 JMockit 开发人员测试工具包中“JMockit Coverage”工具的一部分。虽然还不成熟,但我相信它会很好用。

                【讨论】:

                  【解决方案16】:

                  数据库访问和网络延迟可能是需要检查的领域。如果您在集成测试中执行大量数据库访问,您可能想要探索使用像 HSQL、H2 或 Derby 这样的内存数据库,而不是像 Oracle 这样的“真实”数据库。如果您使用的是 Hibernate,您还必须更改 Hibernate 配置中的设置以使用特定于该数据库的方言(例如,HSQLDialect 而不是 OracleDialect)。曾经在一个项目中,每个完整构建最终都必须删除并重新创建整个 Oracle 模式并通过网络执行大量数据库测试,有时需要长达 20 分钟,然后你发现有人签入并且事情被破坏了再次。 :(

                  理想情况下,您只希望有一个可用于两个数据库的数据库脚本,但您最终可能不得不同步两个不同的数据库创建脚本 - 一个用于生产,一个用于集成测试。

                  同一 JVM 中的数据库与跨网络的数据库 - 可能会有所不同。

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2023-03-19
                    • 1970-01-01
                    • 1970-01-01
                    • 2017-10-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多