这篇文章是作为《测试性能优化建议》的补充,在社区朋友和同事的指导下尝试了一些新的方案。今天就来盘点一下这几个新的测试优化策略,在它们的帮助下我一度把测试总时间降低到一分钟左右。

选择合适的ActiveJob::QueueAdapters

一般来说我们的业务都会有用到ActiveJob,用来让一些耗时较长的任务能够以异步的方式去执行,而不会堵塞主任务的流程。线上任务我们一般都会采用SideKiq来完成这个事情,然而测试呢?

如果你像笔者一样用的是这个配置

# huiliu-web/config/environments/test.rb

Rails.application.configure do
  ...
  config.active_job.queue_adapter = :async
end

那你就要注意安全了,这个是Rails原生提供的队列适配器ActiveJob::QueueAdapters::AsyncAdapter。以下是对它的说明

The adapter uses a Concurrent Ruby thread pool to schedule and execute jobs. Since jobs share a single thread pool, long-running jobs will block short-lived jobs. Fine for dev/test; bad for production.

稍微可能要注意一下的是异步不代表不占用资源,它们只是延后运行,依旧会占用系统资源,而且依赖的还是Ruby的并发库(目前应该并发不怎么高吧,需要另外研究)。由于笔者的测试里面有大量的异步Jobs(ActiveStorage里面销毁图片其实都是通过异步的方式),这种Jobs一旦多起来,而且在测试过程中与测试代码交替运行,占用掉了一部分系统资源,测试就慢了。笔者无意中把它改成

Rails.application.configure do
  ...
  config.active_job.queue_adapter = :sidekiq
end

发现测试时间减少了一分钟,现在在Macbook上大概是1分40秒左右能跑完800个测试。

test-in-1m-40s.png

所以当你的测试很慢的时候,可以检查一下上面这个配置,因为有很多Jobs其实并不需要在测试过程中去完成,直接丢弃掉就好。把适配器配置改成sidekiq,其实就是把一些异步的Jobs放在Redis里面,等启动了Sidekiq服务之后才会去执行。不过在测试环境用:sidekiq做配置其实是一个错误示范。

为什么测试里面不应该用Sidekiq?

测试以轻量级为准,依赖的服务越少越好,这也是官方提供config.active_job.queue_adapter = :async这个配置的原因。然而用这个的时候要想一下,那些异步任务对于测试来说真的重要吗?其实大部分都没那么重要,那些Jobs其实可有可无,因为测试过程中就会去清理数据库的数据,那些遗留的静态文件,可以通过别的手段去删除。因此这些Jobs完全丢弃掉也没事

我现在用的配置是

Rails.application.configure do
  config.active_job.queue_adapter = :test
end

就没有任何Jobs产生了,可以看看这篇文章

另外就是如果是本地启动sidekiq服务,那么很可能会出现你的Jobs都会运行在development环境,而不是期望的test环境。因为我们一般是这样去启动sidekiq的

bundle exec sidekiq

这其实sidekiq相关的job都会访问开发环境的数据库,而不是测试环境的,你会发现一堆莫名且秒的错误。另外,类似此类代码的判断语句就没用了

module Sms
  def to(mobile, content)
    return if Rails.env.test?

    res = @conn.post('/v1/send.json') do |req|
      req.body = {
        mobile: mobile,
        message: normalize(content)
      }
    end
  end
end

代码会往下执行,调用螺丝帽的短信接口发送大量短信.....

自己写的测试导致的短信轰炸。

笔者因此造成了公司百来块钱的损失。如果你的测试不依赖于Jobs所带来的副作用,干脆关掉的好config.active_job.queue_adapter = :test

非要用sidekiq还得想办法区分两个环境(用不同的redis连接),不然可能会相互干扰。而且边跑测试sidekiq这边的jobs也会被执行,虽说比用:async好一些,但还不如用:test直接把大量没用的Jobs抛弃掉的好。

这个优化带来的效益还是挺高的,终于能够在2分钟内跑完测试了。所以篇幅稍微长了一些,也顺便分享一下自己遇到的坑吧。

并行测试

引入parallel_tests让用例可以并行运行的做法也带来了一些收益。不过这个方法我还没完全引入项目中,因为Redis数据库在并行环境下工作不太好协调,偶然会出现个别用例运行不通过的情况。不过它确实,确实,确实有些帮助,引入它之后总算能在50~60s左右跑完所有测试了。结果如下

> RAILS_ENV=test bundle exec rake parallel:spec
8 processes for 58 specs, ~ 7 specs per process

test-in-parallel.png

8 processes for 58 specs, ~ 7 specs per process,因为我电脑是8核,所以用8个进程来跑。不过这个也是可以自己去配置的,它的原理大概是

ParallelTests splits tests into even groups (by number of lines or runtime) and runs each group in a single process with its own database.

为了更好的数据隔离,它会根据我们设定的进程数来创建对应数量的数据库,然后把测试分割到不同的数据库里面跑。理论上应该会有所加速。对于一般的ActiveRecord数据是没多大问题,不过Redis中的数据就很容易发生冲突了。不同的CPU核心如果访问同一个Redis链接,你写我读,很容易数据就紊乱了,目前还在调试阶段,等哪天解决了Redis的访问问题再集成到项目中去。

PS: 不过在初始化的时候要创建8个数据库,在CI流程里面是不是也是一种耗时操作?

Test Prof

TestProf is a collection of different tools to analyze your test suite performance.

大佬的推荐下,我给项目引入了test-prof。主要用于检测测试的性能瓶颈。作用跟RSpec的--profile有点像。从文档来看,它检测的东西会更底层且更细粒度一些。除此之外它还能够集成ruby-prof以及stackprof这些常见的优化器,能够根据不同的指令检测不同“部件”的性能问题。比如你可以检测出数据库访问方面最慢的那些测试

EVENT_PROF=sql.active_record rspec

[TEST PROF INFO] EventProf results for sql.active_record

Total time: 00:05.045
Total events: 6322

Top 5 slowest suites (by time):

也可以单独检测数据构造得最慢的那些测试

FDOC=1 rspec

[TEST PROF INFO] FactoryDoctor report

Total (potentially) bad examples: 2
Total wasted time: 00:13.165

据说已经有不少的开源项目都受益于它,大大降低了测试时间

test-prof-for-opensource.png

可惜的是,受限于笔者自身的能力以及工作时间,暂时无法立即在项目中深度使用该工具,也只能先写个简单的介绍了。毕竟一项项地去检测并优化还是比较耗时间的,无法一蹴而就。只好先在这里埋下伏笔,等日后深度使用之后再写一篇相关的使用心得了。

尾声

这篇文章是对《测试性能优化建议》的简要补充,优化这种东西如果真要做的话是永无止境的。从目前的结果来看,关闭异步任务对性能影响比较大,能够提速一分多钟(1m40s左右跑完)。引入并行之后再能进一步把速度缩短到1分钟之内(50s左右),只不过目前还没找到好的方法来解决依赖Redis的相关测试在并行场景下偶发的问题。

目前比较期待的是test-prof在性能优化方面的表现。哎,无奈时间有限,要深度使用估计要等过年之后了,希望到时候也能够发一篇总结性的文章吧。