rails测试中遇到的一些问题 博客分类: Rails2 测试Rails
程序员文章站
2024-02-12 14:39:16
...
1,页面的测试。
假设某view中有一个表单,表单里有3个字段,《The Rspec Book》中的做法是为这3个字段各写一个example:
而我认为这样写就足够了:
我不明白前一种写法的好处。书上为什么要这么写?
2,多层链式调用的setup问题:
虽然我极力避免2层及2层以上的链式调用,但有时候似乎不得不用,例如:
这种情况下controller测试进行之前的setup很麻烦:
对应要测的controller:
这种问题有什么办法解决?
======================================================================
关于问题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得了。
修改后的代码如下:
这里@current_store的setup过程又可以提出来,放在一个叫login_as_seller的macros里面,以后只要在需要的地方调用一下login_as_seller即可:
当然,为了以上代码能够正常工作,我们需要为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 你这个地方
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
=====================================================================
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:
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:
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:
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,不是吗?
假设某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
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,不是吗?