提升你的 Rails Specs 性能 10 倍
人们疏于在Rails开发应用中去驾驭规范的一个基本的原因是运行的规范套件所需要的时间。很多工具可以用来缓和这个麻烦,比如 Spork , Zeus 和 Spring。事实上,Rails 4.1将会在春季推出标准。不幸的是,这些工具仅仅是解决问题症状的 一个拐杖,而不是解决问题本身。实际的问题是书写耦合度高的代码需要有一个完整的Rails的架构支撑,这个架构会缓慢启动。
开发解耦代码
一种解决方法是:书写的代码是独立的,元件尽可能的与系统分离。用另外的话说,就是写SOLID Rails 代码。举一个特殊的例子,可以直接写一个类模块去创建一个事例。而不是使用依赖的插入的方法去去除涉及到类的硬编码。我们仅仅需要去保证:我们安全的采用模块符号或者懒惰的评价去得到默认的引用。以下是一个服务,它需要在ActiveRecord模块中创建一个小工具。我们采用懒惰的评价去介入的方法来替换直接的引用工具类。这可以解耦我们的代码,同时不需要ActiveRecord载入。
# A tightly coupled class. We don't want this. class MyService def create_widget(params) Widget.create(params) end end # We can inject in the initializer class MyService attr_reader :widget_factory def initialize(dependencies={}) @widget_factory = dependencies.fetch(:widget_factory) { Widget } end def create_widget(params) widget_factory.create(params) end end # Or we can explictly inject via a setter with a lazy reader class MyService attr_writer :widget_factory def widget_factory @widget_factory ||= Widget end def create_widget(params) widget_factory.create(params) end end # A specification injecting the dependency using the second method describe MyService do subject(:service) { MyService.new } let(:widget_factory) { double 'widget_factory', create: nil } before { service.widget_factory = widget_factory } it 'creates a widget with the factory' do service.create_widget({name: 'sprocket'}) expect(widget_factory).to have_received(:create).with({name: 'sprocket'}) end end
当你采用这种方式写代码时,你可以开始重新组织怎么建立自己的规范和最小化环境需求来运行这些规范和满足规则需求的代码。典型spec_helper.rb会有一个如下的一行代码:
require File.expand_path("../../config/environment", __FILE__)
这个将会载入整个的Rails程序且降低测试运行速度。为了让规范达到更快的速度,可以使用一个不含有上面那行代码的配置文件。那么让我们开始创建一个轻量级的rb包:base_sepc_helper.rb:
ENV["RAILS_ENV"] ||= 'test' require 'rubygems' RAILS_ROOT = File.expand_path('../..', __FILE__) Dir[File.join(RAILS_ROOT, 'spec/support/**/*.rb')].each {|f| require f} RSpec.configure do |config| config.mock_with :rspec config.order = 'random' # Your prefered config options go here end require 'active_support' require 'active_support/dependencies'
我们通过请求active_support和active_support/dependencies包来访问Rails使用的自动装载机,实际上并没有导入所有的Rails。它是相当的轻量级并且方便性超过了损耗。在每个需要这个base包的helper里,我们将会添加我们程序相对应的部分到ActiveSupport::Dependencies.autoload_paths中。
简单的Ruby对象说明
取决于你指定的应用程序部分,你可以在任意一个上下文中创建一个你所需要的辅助细则。例如,最简单的是指定一个任意类型的Ruby纯类作为服务类。如下面services_spec_helper.rb例子
require 'base_spec_helper' Dir[File.join(RAILS_ROOT, "spec/support_services/**/*.rb")].each {|f| require f} ActiveSupport::Dependencies.autoload_paths << "#{RAILS_ROOT}/app/services"
装饰说明
于你的装饰而言,你可能会选择布商,你的decorators_spec_helper.rb就如以下所看到的。
require 'base_spec_helper' require 'draper' Draper::ViewContext.test_strategy :fast Dir[File.join(RAILS_ROOT, "spec/support_decorators/**/*.rb")].each {|f| require f} ActiveSupport::Dependencies.autoload_paths << "#{RAILS_ROOT}/app/decorators"
模块规范
测试模块还需要做一点事情. 假设你现在正在用ActiveRecord你会需要建立一个和数据库的连接. 我们并不需要将defactory_girl或者database_cleaner加入你的测试中,而且并不会真的创建对象. 实际上,唯一需要进行创建数据库对象的地方就是当你进行特定对象测试的时候.当你确实需要创建一些对象的时候,你只需要手动的进行清理和转换. 这就是一个样例models_spec_helper.rb:
require 'base_spec_helper' require 'active_record' # RSpec has some nice matchers for models so we'll pull them in require 'rspec/rails/extensions/active_record/base' Dir[File.join(RAILS_ROOT, "spec/support_models/**/*.rb")].each {|f| require f} # Manually connect to the database ActiveRecord::Base.establish_connection( YAML.load(File.read(RAILS_ROOT + '/config/database.yml'))['test'] ) ActiveSupport::Dependencies.autoload_paths << "#{RAILS_ROOT}/app/models"
特点说明
最后, 当我们创建特色应用时, 我们会需要Rails全套知识并且feature_spec_helper.rb看起来就和spec_helper.rb差不多了.
作为总结
我自己也开始在项目中加入这些改变并且这也让我能用更加简单的代码去完成一个项目. 你们可以在Github上找到:https://github.com/Originate/rails_spec_harness
当在项目中引入这些变化时候,我发现速度至少增长了8-12倍. 变化最大的一个项目竟然增长了27倍同时也包括了这些对应的编程效率上的提高.举个例子,我开始写一个含有4个简单例子的Ruby类. 然后我使用time命令行工具去衡量运行的效率,并且之后我能得到如下的结果,FULL Rails VS MINIMAL:
Spec Helper | Real | User | Sys | RSpec Reported |
---|---|---|---|---|
Full Rails | 4.913s | 2.521s | 1.183s | 0.0706s |
Minimal | 0.492s | 0.407s | 0.080s | 0.0057s |
写牛逼的代码,隔离你的单独模块,然后,享受编码的乐趣吧。