`

rails测试中遇到的一些问题

阅读更多
1,页面的测试。
假设某view中有一个表单,表单里有3个字段,《The Rspec Book》中的做法是为这3个字段各写一个example:
it 'renders a form to create product'

it 'renders a text field for product name'

it 'renders a text field for product price'

it 'renders a text field for product sku'

而我认为这样写就足够了:
it 'renders a form to create product' do
  render
  response.should have_selector('form', :action => 'xxaction', :method => 'post') do |form|
    form.should have_selector('input[type=text]', :name => 'product[name]')
    form.should have_selector('input[type=text]', :name => 'product[price]')
    form.should have_selector('input[type=text]', :name => 'product[sku]')
  end
end

我不明白前一种写法的好处。书上为什么要这么写?

2,多层链式调用的setup问题:
虽然我极力避免2层及2层以上的链式调用,但有时候似乎不得不用,例如:
current_store.products.build params[:product] #store has many products

这种情况下controller测试进行之前的setup很麻烦:

describe Mystore::ProductsController, 'POST create' do
  before do
    @product = mock_model(Product).as_null_object
    @products = [stub!(:products)]

    @current_store = stub!('current_store')
    @current_store.stub!(:products => @products)
    controller.stub!(:current_store => @current_store)
    @products.stub!('build' => @product)
  end

  it 'creates product with params' do
    @products.should_receive(:build).with('name' => 'the rspec book')
    post 'create', :product => {:name => 'the rspec book'}
  end

#....

对应要测的controller:
  def create
    @product = current_store.products.build params[:product]
#...

这种问题有什么办法解决?

======================================================================

关于问题2我认真的想了一会,其实rails提供的那种方法本身就破坏了封装,直接把products这么一个集合暴露出来,我们关心的只是往store里添加一个product,并不关心store内部是个什么结构。

我觉得可以给store加上build_product和create_product方法,虽然这两个方法是在has_one和belongs_to的关系中自动生成的,但这里store和product是has_many关系,并没有这两个方法。一开始我想给它们起名叫做new_product和add_product,但是这样似乎理解起来更费劲,不如就按照rails的规则叫build_product和create_product得了。

修改后的代码如下:

describe Mystore::ProductsController, 'POST create' do
  before do
    @current_store = mock_model(Store).as_null_object
    controller.stub!(:current_store => @current_store)

    @product = mock_model(Product).as_null_object
    @current_store.stub!(:build_product => @product)
  end

  it 'creates product with params' do
    @current_store.should_receive(:build_product).with('name' => 'the rspec book')
    post 'create', :product => {:name => 'the rspec book'}
  end
#...


  def create
    @product = current_store.build_product params[:product]
#...


这里@current_store的setup过程又可以提出来,放在一个叫login_as_seller的macros里面,以后只要在需要的地方调用一下login_as_seller即可:
class Store;end
class User;end
module ControllerMacros
  def login_as_seller
    @current_user = mock_model(User).as_null_object
    controller.stub!(:current_user => @current_user)

    @current_store = mock_model(Store).as_null_object
    controller.stub!(:current_store => @current_store)
  end
end

describe 'as a seller' do
  before do
    login_as_seller
  end
  describe Mystore::ProductsController, 'POST create' do
    before do
      @product = mock_model(Product).as_null_object
      @current_store.stub!(:build_product => @product)
    end

    it 'creates product with params' do
      @current_store.should_receive(:build_product).with('name' => 'the rspec book')
      post 'create', :product => {:name => 'the rspec book'}
    end
  #...


当然,为了以上代码能够正常工作,我们需要为Store模型添加build_product的测试,并实现之。

=====================================================================

2010-9-3 19:47:56 我: hi

2010-9-3 19:49:37 Kadvin hi

2010-9-3 19:49:53 我: 呵呵,有空不?想请教点问题

2010-9-3 19:50:01 Kadvin 有

2010-9-3 19:51:03 我: controller中出现这样的代码好还是不
好?current_store.products.build params[:product]

2010-9-3 19:51:41 Kadvin 可以接受

2010-9-3 19:51:48 我: 也就是说有点问题?

2010-9-3 19:52:10 Kadvin 把逻辑往模型里面放,也不要走得太过,什么都往模型里面放,也会导致模型臃肿。
2010-9-3 19:53:03 Kadvin 是有一点点的问题,就是一般写得好的编码,调用层次不应该超过3层 A类里面 b.c.d.e 这样调用,就基本上说明对b的封装不足。

2010-9-3 19:53:12 我: 嗯,可是这里出现了2层的链式调用,暴露了products,写测试的时候setup好像有些麻烦。
2010-9-3 19:53:16 我: 是呀,我也是这么觉得。

2010-9-3 19:53:51 Kadvin 但你完全追求两层调用,这会导致你系统过度设计。
2010-9-3 19:54:18 Kadvin 过度设计(Over Design),这是很多初搞软件的人犯的错误。
2010-9-3 19:54:32 Kadvin 所以,你这里这个代码,是可以接受的。
2010-9-3 19:55:00 Kadvin 因为Store -has_many-> products,你是通过ActiveRecord的DSL建立的一个很简便的关系。

2010-9-3 19:54:38 我: 可是测试变得难写了咋办?

2010-9-3 19:55:34 Kadvin 你测试用例的Fixture里面,应该做好准备啊。
2010-9-3 19:55:40 Kadvin 我没觉得数据困难了多少

2010-9-3 19:56:39 我: 咦,你写controller测试需要用到fixture吗?
2010-9-3 19:57:27 我: 我用的是stub和mock
2010-9-3 19:58:00 我: fixture我们现在用factory_girl来替代,也只是在测model的时候用到。

2010-9-3 19:58:38 Kadvin 嗯,一般是Stub就够了
2010-9-3 19:58:51 Kadvin 我用DataSet
2010-9-3 19:58:55 Kadvin 做模型的Fixture

2010-9-3 20:02:27 我: 哦。这是我今天记的一点东西,其中第2个问题就是现在咱们说的这个问题:http://yuan.iteye.com/blog/754367 在后面我自己给自己回复,试着给出一个解决办法,用这个办法看起来before那块代码简单得多了,但是model里要多一个方法。那如果给每个 has_many关系添加这么一个方法,好像就会有许多重复代码。

2010-9-3 20:00:40 Kadvin 你这个地方
stub(:current_store) do
  Store.first 
end
给一个实际的store对象
2010-9-3 20:00:54 Kadvin 那build自然就没问题了嘛~

2010-9-3 20:03:07 我: 这样啊,我一直以为controller的测试代码里出现真实的model不好。

2010-9-3 20:04:46 Kadvin 如果你的模型是经过测试的,那没啥不好的。尤其是,如果你用的模型的方法是Rails自身的(说明经过测试)
2010-9-3 20:04:49 Kadvin 不能太僵化

2010-9-3 20:05:39 我: 我想想

2010-9-3 20:06:17 Kadvin 我看了下你的文章,觉得你完全弃用AR做控制器测试的思路太呆板了。
2010-9-3 20:07:02 Kadvin 一般而言,控制器的测试是基于模型的测试,适当引入一些预制的模型,会大大降低控制器的setup工作。

2010-9-3 20:13:34 我: 那问题1呢?

2010-9-3 20:14:19 Kadvin 你的观点正确,我也是这么搞的,一下子验证多个。

2010-9-3 20:14:26 我:

2010-9-3 20:16:00 Kadvin 关键是,找你信得过的东西来做stub

2010-9-3 20:16:27 我: 信得过的是指经过测试的或者Rails本身提供的api是吧?

2010-9-3 20:16:33 Kadvin yes

=====================================================================

RSpec官方文档 写道
We strongly recommend that you use RSpec’s mocking/stubbing frameworkto intercept class level calls like :find, :create andeven :new to introduce mock instances instead of real active_record instances.

This allows you to focus your specs on the things that the controller does and notworry about complex validations and relationships that should be described indetail in the Model Examples

http://rspec.info/rails/writing/controllers.html

=====================================================================

Stub Chain
Let’s say we’re building an educational website with Ruby on Rails, and we need a database query that finds all of the published articles written in the last week by a particular author. Using a custom DSL built on ActiveRecord named scopes, we can express that query like so:
 Article.recent.published.authored_by(params[:author_id])

Now let’s say that we want to stub the return value of authored_by( ) for an example. Using standard stubbing, we might come up with something like this:
recent= double()
published= double()
authored_by = double()
article= double()
Article.stub(:recent).and_return(recent)
recent.stub(:published).and_return(published)
published.stub(:authored_by).and_return(article)

That’s a lot of stubs! Instead of revealing intent it does a great job of hiding it. It’s complex, confusing, and if we should ever decide to change any part of the chain we’re in for some pain changing this. For these reasons, many people simply avoid writing stubs when they’d otherwise want to. Those people don’t know about RSpec’s stub_chain( ) method, which allows us to write this:
article = double()
Article.stub_chain(:recent, :published, :authored_by).and_return(article)

Much nicer! Now this is still quite coupled to the implementation, but it’s also quite a bit easier to see what’s going on and map this to any changes we might make in the implementation.

最后的结果是:我承认在current_store.products.build这个地方追求1层的方法调用有点过了。但我仍然坚持在controller里使用mock/stub,不碰fixture/factory等model相关的东西。stub_chain完美解决以上问题。并且,对属性的链式访问,我仍然会继续追求1层的方法调用——如果你采用自顶向下的开发方式,会发现这是自然而然的事情。至于rails的current_store.products.build/create这些方法,这属于rails实现的问题,其实rails也可以实现成current_store.build_product,不是吗?
分享到:
评论
3 楼 yuan 2011-05-10  
关于第1个问题,我猜测作者是为了遵循one assertion per test。
但这里有个问题是,以下代码也可以通过作者所写的测试:

<form action='xxaction' method='post'></form>
<form action='yyaction' method='get'>
<input type='text' name='product[name]'/>
<input type='text' name='product[price]'/>
<input type='text' name='product[sku]'/>
</form>

但显然以上代码是不符合需求的。如果为每个字段写一个example,并在每个example中指定action、method,重复代码就太多了。所以,在这个地方,目前我仍然坚持用后面那种写法。

关于one assertion per test: http://www.artima.com/weblogs/viewpost.jsp?thread=35578
2 楼 夜鸣猪 2010-09-25  
这个好

值得研究
1 楼 yuan 2010-09-19  
这可能是rspec-rails的一只bug:
controller如下:
  def index
#.....
      render 'index', :layout => !request.xhr?
#......
  end

相应的测试代码如下:
controller.should_receive(:render).with 'index', :layout => false


无论如何测试都通不过:
Mock "render_proxy" expected :render with ("index", {:layout=>false}) once, but received it 0 times

但这样的代码却没问题:
  def index
#.....
      render :partial => 'categories', :layout => !request.xhr?
#......
  end

controller.should_receive(:render).with(:partial => 'categories', :layout => false)


或者这样也没问题:
  def index
#.....
      render 'index'
#......
  end

controller.should_receive(:render).with('index')

难道rspec-rails只认第一个参数?

相关推荐

    Rails中遇到错误跳转到统一提示错误页的方法

    一个迭代开发中的网站难免存在bug,出bug的时候客户体验就很不好了,为解决此问题,可以在class error产生的时候,触发跳转到统一提示页面,并给开发人员发邮件报错误信息,提高测试能力和用户体验。以下是核心方法...

    ember-cli-rails:统一您的EmberCLI和Rails工作流程

    灰烬CLI滑轨 ... 如果遇到问题,请签出! EmberCLI-Rails支持EmberCLI 1.13.13和更高版本。 安装 将以下内容添加到您的Gemfile : gem "ember-cli-rails" 然后运行bundle install : $ bundle instal

    ajax-datatables-rails:DataTable的ajax方法的包装,该方法允许与Rails应用程序中的服务器端分页同步

    但是,在处理大型表(超过两百行)时,会遇到性能问题。 可以使用服务器端分页来解决这些问题,但这会破坏某些DataTables功能。 ajax-datatables-rails是DataTables ajax方法的包装,该方法允许与Rails应用程序中...

    sample_app_rails_4:Ruby on Rails教程(Rails 4)的示例应用程序的参考实现

    如果您最终在教程中遇到代码问题,则可以使用此参考实现来帮助跟踪错误。 特别是,作为第一次调试检查,我建议让测试套件通过您的本地计算机: cd /tmp git clone ...

    ruby-on-rails-app

    如果您最终在本教程中遇到代码问题,您可以使用此参考实现来帮助跟踪错误。 特别是,作为第一次调试检查,我建议让测试套件在您的本地机器上传递: cd /tmp git clone ...

    rails-server-template:全面的Chef存储库,可为Rails应用程序部署准备Ubuntu 20.04服务器

    这是示例代码,可从leanpub此处的“可靠地部署Rails应用程序”一书中找到,此处 。 如果您使用此处提供的模板遇到任何问题,请打开Github问题,我会积极监控这些问题,并会尽快做出回应。 要求 该模板旨在在Ubuntu ...

    tips-on-rails:Rails 开发人员的专业网络

    Rails 小贴士 曾经遇到过您不确定从哪里开始的情况吗? 如何实现 Rails 应用程序的特定部分? 完全不知道如何测试特定行为? Tips on Rails 是为像你这样的人制作的。 它是一个工具,可以让 Rails 用户学习、发布和...

    rails-ng-foundation-seed:Rails、AngularJS、Foundation 和 Heroku 入门

    如果您在启动和运行时遇到任何问题,请告诉我。安装: git clone git@github.com:omancipator/rails-ng-foundation-seed.git cd rails-ng-foundation-seed npm install -g 业力捆绑安装耙凉亭:安装导轨在此处查看...

    在 Ubuntu 12.04 Server 上安装部署 Ruby on Rails 应用

    本教程只适合 Ubuntu Server 用于部署项目到线上,建议使用同样的 Ubuntu 版本,以免遇到一些版本不同带来的问题。 本教程适合新手初次部署 Rails 应用; 本文测试通过环境 Ubuntu 12.04 Server, 服务器安装测试于 ...

    mongoid_shortener:基于 Rails 3.1 引擎的 url 缩短器,必须与 mongoid 一起使用

    您可能在使用低于 3.1 的 Rails 版本时遇到问题。 要使用 MongoidShortener,首先你必须安装 gem。 # Gemfile gem "mongoid_shortener" gem "yab62" , require : "yab62" # Terminal bundle install 之后,确保...

    emacs_for_rails_devs:我的 emacs 设置供您使用

    您的面向 Ruby 和 Rails 的 Emacs 点文件。 最新版本 24.4.1 强调 使用内置于 Ruby 模式。...随着每个包的下载,您将在迷你缓冲区中看到一些活动。 如果您遇到任何“错误”,尤其是超时错误,只需重新启动 emacs。

    强大功能:为Rails应用动态呈现错误页面或JSON响应的简单而安全的方法

    它继承自ActionController::Base ,因此即使您的ApplicationController遇到问题,它也可以正常工作。 灵活 您可以完全控制要显示特定异常的错误页面。 它还可以json呈现(对于API应用程序而言是完美的)。 它甚至...

    validates_zipcode:Rails的邮政编码邮政编码验证,支持233个国家_地区代码

    如果在更高版本上遇到问题,请尝试v0.2系列。 Truffleruby也经过了测试,但是目前尚无关于在生产应用程序中工作的报告。 安装 将此行添加到您的应用程序的Gemfile中: gem 'validates_zipcode' 然后执行: $ ...

    members_only_app

    会员专用应用程序。 这是Odin项目Ruby on Rails课程中Forms项目的一部分。 在找到它 入门 要开始使用该应用程序,请克隆存...如果您遇到任何问题,请告诉我们,我们将竭诚为您服务。 此项目没有实时版本。 合作者

    i18n-bamboo:通过猴子修补 Rails I18n 模块来帮助开发,以始终返回所有可用语言环境中最长的翻译或本地化值

    I18n:Bamboo monkey 修补 Rails I18n 模块,并将强制所有对 I18n.translate (I18n.t) 和 I18n.localize (I18n.l) 的调用返回所有可用语言环境中最长的翻译或本地化值。 出于显而易见的原因(猴子补丁 :anxious_face...

    pa2_rails166:166的PA2

    在使某些脚手架屈光方面存在问题,并且在搜索pa方面也遇到了困难 我的架构中有一个问题,其结果是我无法(在时间限制内)设置注册数据库,从而使我的主页无法获取已注册的类。 如果我有足够的时间解决问题,可以...

    RubyTutorial

    如果您最终在教程中遇到代码问题,则可以使用此参考实现来帮助跟踪错误。 特别是,作为第一次调试检查,我建议让测试套件通过您的本地计算机: cd /tmp git clone ...

    prelunch:午餐前

    如果您最终在教程中遇到代码问题,则可以使用此参考实现来帮助跟踪错误。 特别是,作为第一次调试检查,我建议让测试套件通过本地计算机: cd /tmp git clone ...

    desolate-fjord-9325:示例应用程序

    如果您最终在本教程中遇到代码问题,您可以使用此参考实现来帮助跟踪错误。 特别是,作为第一次调试检查,我建议让测试套件在您的本地机器上传递: cd /tmp git clone ...

    爱迪菲

    如果遇到种子问题,请使用bin/rails db:seed再次运行它们。 要运行测试,请使用RAILS_ENV=test rails db:drop db:create db:migrate来准备数据库(因为每次运行时都要清理DB,所以只需一次),然后执行这两个命令...

Global site tag (gtag.js) - Google Analytics