如何进行高效的Rails单元测试
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何进行高效的Rails单元测试相关的知识,希望对你有一定的参考价值。
参考技术A 登录En |
中文 |
日本 |
Fr |
Br
482,381 一月 独立访问用户
语言 & 开发
特别专题语言 & 开发
从概念到原型只需要24小时——开源硬件和3D打印是如何协助智能硬件开发的
硬件复兴正成为一个新的浪潮,各种智能硬件层出不穷。在这场新的浪潮
中,和时间赛跑就成为一个重要的竞争手段。在一个好的创意迸发以后,如何在最短时间内对创意进行实体化,加速0-1的过程?随着开源硬件和3D打印的发展
和完善,在24小时内完成0-1的过程已经成为可能。
浏览所有语言 & 开发
Java
.Net
云计算
移动
html 5
javascript
Ruby
DSLs
Python
php
PaaS
架构 & 设计
特别专题架构 & 设计
从概念到原型只需要24小时——开源硬件和3D打印是如何协助智能硬件开发的
硬件复兴正成为一个新的浪潮,各种智能硬件层出不穷。
在这场新的浪潮中,和时间赛跑就成为一个重要的竞争手段。在一个好的创意迸发以后,如何在最短时间内对创意进行实体化,加速0-1的过程?随着开源硬件和
3D打印的发展和完善,在24小时内完成0-1的过程已经成为可能。
浏览所有架构 & 设计
建模
性能和可伸缩性
领域驱动设计
AOP
设计模式
安全
云计算
SOA
过程 & 实践
特别专题过程 & 实践
对《敏捷方法介绍》一书的作者,Sondra Ashmore与Kristin Runyan的问答
由Sondra Ashmore与Kristin Runyan编写的《敏捷方法介绍》一书对敏捷方法的价值、原则与实践进行了简单而直观的介绍,书中的内容包括了对多位在敏捷转型方面享有盛名并且受人尊敬的专家的采访。作者Shane Hastie。
浏览所有过程 & 实践
Agile
领导能力
团队协作
敏捷技术
方法论
持续集成
精益
客户及需求
运维 & 基础架构
特别专题运维 & 基础架构
架构师(2015年1月)
本期内容推荐:解读2014之云计算篇:
有一种态度叫做“拥抱”(上);解读2014之ios篇:拥抱变化;解读2014之Docker篇:才气、运气、勇气;SDN落地的实践与思考:带着问题
找方案,别管定义啦;Swift编程语言;手机淘宝构架演化实践;手机QQ的移动化实践之路;物联网传输协议MQTT;从信息安全看未来物联网发展;赵海
峰:大数据决定互联网金融未来。
浏览所有运维 & 基础架构
性能和可伸缩性
大数据
DevOps
云计算
虚拟化
NoSQL
应用服务器
运维
企业架构
特别专题企业架构
架构师(2015年1月)
本期内容推荐:解读2014之云计算篇:有一种态度
叫做“拥抱”(上);解读2014之iOS篇:拥抱变化;解读2014之Docker篇:才气、运气、勇气;SDN落地的实践与思考:带着问题找方案,别
管定义啦;Swift编程语言;手机淘宝构架演化实践;手机QQ的移动化实践之路;物联网传输协议MQTT;从信息安全看未来物联网发展;赵海峰:大数据
决定互联网金融未来。
浏览所有企业架构
企业架构
业务流程建模
业务/IT整合
Integration (EAI)
治理
Web 2.0
SOA
移动
Docker
Node.js
云计算
大数据
架构师
QCon
ArchSummit
AWS
Azure
Helion
全部话题
您目前处于:
InfoQ首页
文章
如何进行高效的Rails单元测试
如何进行高效的Rails单元测试
作者
李冠德
发布于
2011年6月18日
|
2
讨论
分享到:
微博
微信
有道云笔记
邮件分享
稍后阅读
我的阅读清单
在笔者开发的系统中,有大量的数据需要分析,不仅要求数据分析准确,而且对速度也有一定的要求的。没有写测试代码之前,笔者用几
个很大的方法来实现这种需求。结果可想而知,代码繁杂,维护困难,难于扩展。借业务调整的机会,笔者痛定思痛,决定从测试代码做起,并随着不断地学习和应
用,慢慢体会到测试代码的好处。
改变思路:能做到从需求到代码的过程转换,逐步细化;
简化代码:力图让每个方法都很小,只专注一件事;
优化代码:当测试代码写不出来,或者需要写很长的时候,说明代码是有问题的,是可以被分解的,需要进一步优化;
便于扩展:当扩展新业务或修改旧业务时,如果测试代码没有成功,则说明扩展和修改不成功;
时半功倍:貌似写测试代码很费时,实际在测试、部署和后续扩展中,测试代码将节省更多的时间。
环境搭建
笔者采用的测试环境是比较流行通用的框架:RSpec + Factory Girl,并用autotest自
动工具。RSpec是一种描述性语言,通过可行的例子描述系统行为,非常容易上手,测试用例非常容易理解。Factory
Girl可以很好的帮助构造测试数据,免去了自己写fixture的烦恼。Autotest能自动运行测试代码,随时检测测试代码的结果,并且有很多的插
件支持,可以让测试结果显示的很炫。
第一步 安装rspec和rspec-rails
在命令行中执行如下命令:
$ sudo gem install rspec v = 1.3.0
$ sudo gem install rspec-rails v = 1.3.2
安装完成后,进入rails应用所在的目录,运行如下脚本,生成spec测试框架:
$ script/generate rspec
exists lib/tasks
identical lib/tasks/rspec.rake
identical script/autospec
identical script/spec
exists spec
identical spec/rcov.opts
identical spec/spec.opts
identical spec/spec_helper.rb
第二步 安装factory-girl
在命令行中执行如下命令:
相关厂商内容
百度 FEX 创立者刘平传分享全端精英团队打造心得
阿里高级搜索专家分享开放搜索服务系统架构
知道创宇首席安全官黑哥分享黑客传奇
触控科技运维总监萧田国分享高效运维诀窍
肖德时:基于Docker容器的云计算平台搭建实战
相关赞助商
全球软件开发大会,4月23-25日,北京,敬请期待!
$ sudo gem install factory-girl
在config/environment/test.rb中,加入factory-girl这个gem:
config.gem "factory_girl"
在spec/目录下,增加一个factories.rb的文件,用于所有预先定义的model工厂。
第三步 安装autotest
在命令行中执行如下命令:
$ sudo gem install ZenTest
$ sudo gem install autotest-rails
然后设置与RSpec的集成,在rails应用的目录下,运行如下的命令,就可以显示测试用例的运行结果。
RSPEC=true autotest or autospec
在自己的home目录下,增加一个.autotest设置所有的Rails应用的autotest插件。当然,也可以把这个文件加到每个应用的根目录下,这个文件将覆盖home目录下的文件设置。autotest的插件很多,笔者用到如下的plugin:
$ sudo gem install autotest-growl
$ sudo gem install autotest-fsevent
$ sudo gem install redgreen
设置.autotest文件,在.autotest中,加入如下代码。
require 'autotest/growl'
require 'autotest/fsevent'
require 'redgreen/autotest'
Autotest.add_hook :initialize do |autotest|
%w.git .svn .hg .DS_Store ._* vendor tmp log doc.each do |exception|
autotest.add_exception(exception)
end
end
测试经验
安装了必要的程序库以后,就可以写测试代码了。本例中,所有应用都是在Rails
2.3.4上开发的,RSpec采用的是1.3.0的版本。为了很好的说明问题,我们假定这样的需求:判断一个用户在一个时间段内是否迟到。写测试代码时
都是遵循一个原则,只关心输入和输出,具体的实现并不在测试代码的考虑范围之内,是行为驱动开发。根据这个需求,我们将会设计方法absence_at(start_time,end_time),有两个输入值start_time和end_time以及一个输出值,类型是boolean。对应的测试代码如下:
describe "User absence or not during [start_time,end_time]" do
before :each do
@user = Factory(:user)
end
it "should return false when user not absence " do
start_time = Time.utc(2010,11,9,12,0,0,0)
end_time = Time.utc(2010,11,9,12,30,0)
@user.absence_at(start_time,end_time).should be_false
end
it "should return true when user absence " do
start_time = Time.utc(2010,11,9,13,0,0,0)
end_time = Time.utc(2010,11,9,13,30,0)
@user.absence_at(start_time,end_time).should be_ture
end
end
测试代码已经完成。至于absence_at方法我们并不关心它的实现,只要这个方法的结果能让测试代码运行结果正确就可以。在此测试代码的基础上,就可以大胆地去完成代码,并根据测试代码的结果不断修改代码直到所有测试用例通过。
Stub的使用
写测试代码,最好首先从model开始。因为model的方法能很好与输入输出的原则吻合,容易上手。最初的时候,你会发现mock和stub很好
用,任何的对象都可以mock,并且在它的基础上可以stub一些方法,省去构造数据的麻烦,一度让笔者觉得测试代码是如此美丽,一步步的深入,才发现自
己陷入了stub的误区。还是引用上面的例子,我们的代码实现如下:
class User < ActiveRecord::Base
def absence_at(start_time,end_time)
return false if have_connection_or_review?(start_time,end_time)
return (login_absence_at?(start_time,end_time) ? true : false)
end
end
按照最初写测试代码的思路,本方法中存在三种情况,即需要三个用例,而且还调用了其他两个方法,需要对他们进行stub,于是就有了下面的测试代码。记得当时完成后还很兴奋,心中还想:这么写测试代码真有趣。
before(:each) do
@user = User.new
end
describe "method <absence_at(start_time,end_time)>" do
s = Time.now
e = s + 30.minutes
# example one
it "should be false when user have interaction or review" do
@user.stub!(:have_connection_or_review?).with(s,e).and_return(true)
@user.absence_at(s,e).should be_false
end
# example two
it "should be true when user has no interaction and he no waiting at platform" do
@user.stub!(:have_connection_or_review?).with(s,e).and_return(false)
@user.stub!(:login_absence_at?).with(s,e).and_return(true)
@user.absence_at(s,e).should be_true
end
# example three
it "should be false when user has no interaction and he waiting at platform" do
@user.stub!(:have_connection_or_review?).with(s,e).and_return(false)
@user.stub!(:login_absence_at?).with(s,e).and_return(false)
@user.absence_at(s,e).should be_false
end
end
上面的测试代码,是典型把代码的实现细节带到了测试代码中,完全是本末倒置的。当然这个测试代码运行的时候,结果都是正确的。那是因为用stub来假定所有的子方法都是对的,但是如果这个子方法have_connection_or_review?发生变化,它不返回boolean值,那么将会发生什么呢?这个测试代码依然正确,可怕吧!这都没有起到测试代码的作用。
另外,如果是这样,我们不仅要修改have_connection_or_review?的测试代码,而且还要修改absence_at的测试代码。这不是在增大代码维护量吗?
相比而言,不用stub的测试代码,不用修改,如果Factory的数据没有发生变化,那么测试代码的结果将是错误的,因为have_connection_or_review?没有通过测试,导致absence_at方法无法正常运行。
其实stub主要是mock一些本方法或者本应用中无法得到的对象,比如在tech_finish?方法中,调用了一个file_service来获得Record对象的所有文件,在本方法测试代码运行过程中,无法得到这个service,这时stub就起作用了:
class A < ActiveRecord::Base
has_many :records
def tech_finish?
self.records.each do |v_a|
return true if v_a.files.size == 5
end
return false
end
end
class Record < ActiveRecord::Base
belongs_to :a
has_files # here is a service in gem
end
所对应的测试代码如下:
describe "tech_finish?" do
it "should return true when A’s records have five files" do
record = Factory(:record)
app = Factory(:a,:records=>[record])
record.stub!(:files).and_return([1,2,3,4,5])
app.tech_finish?.should == true
end
it "should return false when A’s records have less five files" do
record = Factory(:record)
app = Factory(:a,:records=>[record])
record.stub!(:files).and_return([1,2,3,5])
app.tech_finish?.should == false
end
end
Factory的使用
有了这个工厂,可以很方便的构造不同的模拟数据来运行测试代码。还是上面的例子,如果要测试absence_at方法,涉及到多个model:
HistoryRecord:User的上课记录
Calendar:User的课程表
Logging:User的日志信息
如果不用factory-girl构造测试数据,我们将不得不在fixture构造这些测试数据。在fixture构造的数据无法指定是那个测试用例使用,但是如果用Factory的话,可以为这个方法专门指定一组测试数据。
Factory.define :user_absence_example,:class => User do |user|
user.login "test"
class << user
def default_history_records
[Factory.build(:history_record,:started_at=>Time.now),
Factory.build(:history_record,:started_at=>Time.now)]
end
def default_calendars
[Factory.build(:calendar),
Factory.build(:calendar)]
end
def default_loggings
[Factory.build(:logging,:started_at=>1.days.ago),
Factory.build(:logging,:started_at=>1.days.ago)]
end
end
user.history_records default_history_records
user.calendars default_calendars
user.loggings default_loggings
end本回答被提问者和网友采纳
如何在 Rails 中正确使用 ActiveStorage 进行模型测试?
【中文标题】如何在 Rails 中正确使用 ActiveStorage 进行模型测试?【英文标题】:How to properly do model testing with ActiveStorage in rails? 【发布时间】:2018-05-14 23:56:51 【问题描述】:我刚刚切换到在 rails 5.1.4 上使用 ActiveStorage,我是 TDD 新手,正在努力弄清楚如何测试 has_one_attached :avatar
的模型
require 'rails_helper'
RSpec.describe User, :type => :model do
let (:valid_user) FactoryBot.build(:user)
describe "Upload avatar" do
context "with a valid image" do
it "saves the image" do
valid_user.save!
saved_file = valid_user.avatar.attach(io: File.open("/home/ubuntu/workspace/spec/fixtures/files/avatar.jpg"), filename: "face.jpg", content_type: "image/jpg")
expect(saved_file).to be_an_instance_of(ActiveStorage::Attachment::One)
end
end
end
end
但我收到以下错误:
Failures:
1) User Upload avatar with a valid image saves the image
Failure/Error:
saved_file = valid_user.avatar.attach(io: File.open("/home/ubuntu/workspace/spec/fixtures/files/avatar.jpg"), filename: "face.jpg",
content_type: "image/jpg")
NoMethodError:
undefined method `upload' for nil:NilClass
Did you mean? load
# /usr/local/rvm/gems/ruby-2.3.4/gems/activestorage-0.1/lib/active_storage/blob.rb:48:in `upload'
# /usr/local/rvm/gems/ruby-2.3.4/gems/activestorage-0.1/lib/active_storage/blob.rb:21:in `block in build_after_upload'
# /usr/local/rvm/gems/ruby-2.3.4/gems/activestorage-0.1/lib/active_storage/blob.rb:16:in `tap'
# /usr/local/rvm/gems/ruby-2.3.4/gems/activestorage-0.1/lib/active_storage/blob.rb:16:in `build_after_upload'
# /usr/local/rvm/gems/ruby-2.3.4/gems/activestorage-0.1/lib/active_storage/blob.rb:26:in `create_after_upload!'
# /usr/local/rvm/gems/ruby-2.3.4/gems/activestorage-0.1/lib/active_storage/attached.rb:25:in `create_blob_from'
# /usr/local/rvm/gems/ruby-2.3.4/gems/activestorage-0.1/lib/active_storage/attached/one.rb:9:in `attach'
# ./spec/models/user_spec.rb:47:in `block (4 levels) in <top (required)>'
有什么提示吗?
【问题讨论】:
提示:expect(valid_user.image.attached?).to be true
因为您不需要测试 ActiveStorage 的内部结构。
expect(valid_user.image).to be_attached
是一样的,但 RSpec 更惯用一点
【参考方案1】:
我解决了
FactoryBot.define do
factory :culture do
name 'Soy'
after(:build) do |culture|
culture.image.attach(io: File.open(Rails.root.join('spec', 'factories', 'images', 'soy.jpeg')), filename: 'soy.jpeg', content_type: 'image/jpeg')
end
end
end
之后
describe '#image' do
subject create(:culture).image
it is_expected.to be_an_instance_of(ActiveStorage::Attached::One)
end
【讨论】:
这会给你一个可能的误报,因为图像总是 ActiveStorage::Attached::One 的一个实例,即使它是 nil。在控制台中尝试以下操作:User.new.avatar
。请注意,它是 ActiveStorage::Attached::One 的一个实例。【参考方案2】:
问题解决了。在将错误跟踪到 ActiveStorage::Blob::upload 方法后,它说:Uploads the io to the service on the key for this blob.
我意识到我没有为测试环境设置 active_storage.service。简单来说就是加:
config.active_storage.service = :test
到 config/environments/test.rb 文件
【讨论】:
不高兴地尝试了另一个答案 - 这对我有用,谢谢 如果某些型号有回形针和其他 Activestorage,有什么方法可以设置?【参考方案3】:我是这样解决的
# model
class Report < ApplicationRecord
has_one_attached :file
end
# factory
FactoryBot.define do
factory :report, class: Report do
any_extra_field 'its value'
end
end
# spec
require 'rails_helper'
RSpec.describe Report, type: :model do
context "with a valid file" do
before(:each) do
@report = create(:report)
end
it "is attached" do
@report.file.attach(
io: File.open(Rails.root.join('spec', 'fixtures', 'file_name.xlsx')),
filename: 'filename.xlsx',
content_type: 'application/xlsx'
)
expect(@report.file).to be_attached
end
end
end
希望对你有帮助
【讨论】:
这对我来说是更好的测试,您实际检查该项目是否已附加。谢谢。【参考方案4】:在 config/environments/test.rb 中
config.active_storage.service = :test
在你的规范中
it expect(valid_user.avatar).to be_attached
【讨论】:
【参考方案5】:我遇到了类似的 nil 错误(Module::DelegationError: to_model delegated to attachment,但 attachment 是 nil),但它只发生在多个测试使用同一个工厂时。
问题似乎是上一个测试拆解中的清理是在下一次工厂调用附加该文件后删除该文件。关键是将 ActiveJob 队列适配器更新为内联,以便立即删除文件。
将这两个设置添加到 config/environments/test.rb:
# Use inline job processing to make things happen immediately
config.active_job.queue_adapter = :inline
# Store uploaded files on the local file system in a temporary directory.
config.active_storage.service = :test
【讨论】:
【参考方案6】:为什么不:
RSpec.describe User, type: :model do
describe 'relations' do
it is_expected.to have_one(:avatar_attachment)
end
end
【讨论】:
【参考方案7】:在这些帖子的帮助下,我对 Rails 6.1 的主动存储进行了 rspec 测试的更新
# .\spec\models\activestorage_spec.rb
require 'rails_helper'
RSpec.describe User, :type => :model do
before(:each) do
@user = FactoryBot.create(:user)
@user.save!
saved_file = @user.pictures.attach(io: File.open("./storage/test.jpg"), filename: "test.jpg", content_type: "image/jpg")
end
describe "Upload picture" do
context "with a valid picture" do
it "saves the picture" do
expect(@user.pictures).to be_attached
end
end
end
end
现在您只能添加更多测试
【讨论】:
以上是关于如何进行高效的Rails单元测试的主要内容,如果未能解决你的问题,请参考以下文章