今天就来盘点一下这几个新的测试优化策略,在它们的帮助下我一度把测试总时间降低到一分钟左右。原文发布于 https://www.lanzhiheng.com/posts/suggestion-of-test-optimization-next
这篇文章是作为《测试性能优化建议》的补充,在社区朋友和同事的指导下尝试了一些新的方案。今天就来盘点一下这几个新的测试优化策略,在它们的帮助下我一度把测试总时间降低到一分钟左右。
一般来说我们的业务都会有用到 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 个测试。
所以当你的测试很慢的时候,可以检查一下上面这个配置,因为有很多 Jobs 其实并不需要在测试过程中去完成,直接丢弃掉就好。把适配器配置改成 sidekiq,其实就是把一些异步的 Jobs 放在 Redis 里面,等启动了 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
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 流程里面是不是也是一种耗时操作?
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
据说已经有不少的开源项目都受益于它,大大降低了测试时间
可惜的是,受限于笔者自身的能力以及工作时间,暂时无法立即在项目中深度使用该工具,也只能先写个简单的介绍了。毕竟一项项地去检测并优化还是比较耗时间的,无法一蹴而就。只好先在这里埋下伏笔,等日后深度使用之后再写一篇相关的使用心得了。
这篇文章是对《测试性能优化建议》的简要补充,优化这种东西如果真要做的话是永无止境的。从目前的结果来看,关闭异步任务对性能影响比较大,能够提速一分多钟(1m40s 左右跑完)。引入并行之后再能进一步把速度缩短到 1 分钟之内(50s 左右),只不过目前还没找到好的方法来解决依赖 Redis 的相关测试在并行场景下偶发的问题。
目前比较期待的是test-prof在性能优化方面的表现。哎,无奈时间有限,要深度使用估计要等过年之后了,希望到时候也能够发一篇总结性的文章吧。