我打算将这作为编写针对控制器动作的有效测试用例集的一般问题。

我包括以下内容:


Ruby on Rails

RSpec:一个测试框架。我曾考虑过要提出一个普通的Rails问题,但是我个人使用RSpec,而且我觉得很多其他人也都这样做。不过,本次讨论得出的原理应该可以移植到其他测试框架中。这是仅使用@programs = Program.all之类的模型方法的极端示例。我选择走这条路,以纳入讨论的其他因素,并证明无论使用外部应用程序代码(例如,模型代码)还是外部插件,都适用相同的原理。

鉴于我谦逊的Google Fu,在此级别上缺少RSpec测试的样式指南,所以我希望,除了改进我的代码之外,它可以成为对我的旅行者有用的指南。

出于示例目的,假设我当前在控制器中具有以下内容:

class ProgramssController < ApplicationController
  def index
    @programs = Program.paginate :page => params[:page], :per_page => params[:per_page] || 30
  end
end



侧边栏:对于那些不熟悉will_paginate的用户,可以添加到ActiveRecord关系(allfirstcountto_a是此类方法的其他示例),并提供类WillPaginate::Collection的分页结果集,该结果集的行为基本上类似于具有一些有用成员方法的数组。


在这种情况下,我应该进行哪些有效的测试?使用RSpec,这是我目前的构想:

describe ProgramsController do
  def mock_program(stubs={})
    @mock_program ||= mock_unique_program(stubs)
  end

  def mock_unique_program(stubs={})
    mock_model(Program).as_null_object.tap do |program|
      program.stub(stubs) unless stubs.empty?
    end
  end

  describe "GET index" do
    it "assigns @programs" do
      Program.stub(:paginate) { [mock_program] }
      get :index
      response.should be_success
      assigns(:programs).should == [mock_program]
    end

    it "defaults to showing 30 results per page" do
      Program.should_receive(:paginate).with(:page => nil, :per_page => 30) do
        [mock_program]
      end
      get :index
      response.should be_success
      assigns(:programs).should == [mock_program]
    end

    it "passes on the page number to will_paginate" do
      Program.should_receive(:paginate).with(:page => '3', :per_page => 30) do
        [mock_program]
      end
      get :index, 'page' => '3'
      response.should be_success
      assigns(:programs).should == [mock_program]
    end

    it "passes on the per_page to will_paginate" do
      Program.should_receive(:paginate).with(:page => nil, :per_page => '15') do
        [mock_program]
      end
      get :index, 'per_page' => '15'
      response.should be_success
      assigns(:programs).should == [mock_program]
    end
  end
end


我在编写此代码时牢记以下原则:



不要测试非控制器代码:我根本不研究will_paginate的实际工作,也不抽象其结果。

测试所有控制器代码:控制器执行以下四件事:将@programs分配给模型,将page参数传递给模型,将per_page参数传递给模型,并将per_page参数默认为30,然后执行其他操作。所有这些东西都经过测试。

没有误报:如果您删除index的方法主体,则所有测试都会失败。

尽可能进行模拟:没有数据库访问权限(尽管有其他ApplicationController逻辑)

我的担忧,因此我将其发布在这里的原因:



我太学究了吗?我知道TDD植根于严格的测试中。的确,如果此控制器充当更广泛的应用程序的一部分,并且说它停止传递page,则最终的行为将是不希望的。但是,以这种严格程度测试这种基本代码是否合适?

我是否正在测试不应该进行的测试?我不是在测试应该做的事情吗?如果在测试方面有太多不足之处,它认为我已经做得很好。

will_paginate模拟是否合适?例如,您看到了最后的三个测试,我返回了一个仅包含一个程序的数组,而will_paginate很容易返回更多的数组。虽然实际上通过添加(例如)31条记录来研究此行为,但可能会违反模块化代码和测试,但我还是有点不安返回不切实际或限制性的模拟结果。

可以简化测试代码吗?测试一行控制器代码似乎很长。尽管我完全感谢TDD的出色功能,但这似乎会让我感到沮丧。虽然编写这些测试用例在大脑上并​​不太费力,基本上相当于一个有趣的vim演练,但我认为,从长远来看,编写这组测试可能花费比节省的时间更多的时间。

这是一组很好的测试吗?一个普遍的问题。


#1 楼

您忘了测试应该渲染什么视图。如果使用此选项,您的规格将更加简洁。后四个应该是标准匹配器。以此为例。

describe ProgramsController do
  let(:programs) { [mock_model(Program)] }    

  describe "GET index" do
    it { should paginate(Program).with_default_per_page(30) }

    describe "after pagination" do
      before(:each) do
        Program.stub(:paginate).and_return(programs)    
        get :index
      end

      it { should assign_to(:programs).with(programs) }
      it { should respond_with(:success) }
      it { should render_template(:index) }
      it { should_not set_the_flash } 
    end
  end
end


评论


\ $ \ begingroup \ $
在2.0中,不应该使用某些Matcha匹配器并将其删除。删除的匹配器为:assign_to,response_with_content_type,query_the_database,validate_format_of,have_sent_email,permit,delegate_method。参见robots.thoughtbot.com/shoulda-matchers-2-0
\ $ \ endgroup \ $
–ccliard
2014年1月7日15:40

#2 楼

您已经按照自己的原则进行了合理的编码。

我唯一真正的建议是每个测试仅包含一个断言,包括任何#should_receive#should=语句。大多数测试的目的似乎是测试一个功能单元,因此不需要重复声明,尤其是在它们是从属的时候。这将有助于更清晰地将重点放在各个测试的目标上,从而减少其体积并提高可读性。

就RSpec样式而言,我有一些惯用的惯用法: >使用#and_return
mock_model块中进行设置

总体而言,我可能会这样写:

describe ProgramsController do
  before(:each) do
    @mock_programs = [mock_model(Program)]
    Program.stub(:paginate).and_return(@mock_programs)
  end

  describe "GET index" do
    it "succeeds" do
      get :index

      response.should be_success
    end

    it "renders the index template" do
      get :index

      response.should render_template("programs/index")
    end

    it "assigns @programs" do
      get :index

      assigns(:programs).should == [mock_program]
    end

    it "defaults to showing 30 results per page" do
      Program.should_receive(:paginate).with(:page => nil, :per_page => 30).and_return(@mock_programs)

      get :index
    end

    it "passes on the page number to will_paginate" do
      Program.should_receive(:paginate).with(:page => '3', :per_page => 30).and_return(@mock_programs)

      get :index, 'page' => '3'
    end

    it "passes on the per_page to will_paginate" do
      Program.should_receive(:paginate).with(:page => nil, :per_page => '15').and_return(@mock_programs)

      get :index, 'per_page' => '15'
    end
  end
end


我认为,对于您提供的有限范围,这是一组很好的测试。您可能可以跳过测试以获得基本成功,因为所有其他测试都取决于成功的操作。我个人对测试will_paginate不能说得太深,因为从控制器测试它的使用时,我做的事情很多。如果已在使用它的模型中对其进行了很好的测试,则您已涵盖了基本行为。

评论


\ $ \ begingroup \ $
有什么令人信服的理由为什么您不检查最近三个测试中的assigns(:programs)?我认为这是因为否则,对带有参数的will_paginate的调用只会失败。正确?
\ $ \ endgroup \ $
–史蒂文
2011年2月2日在18:13

\ $ \ begingroup \ $
正确。我仅对program#paginate进行存根处理,以避免will_paginate失败。根据我对某事物进行打桩的频率,我还可以将其移至特定上下文的before块。我认为我已经测试了一次分配,并且没有其他行为可能会改变哈希的内容,因此我无需在其他示例中验证其是否有效。
\ $ \ endgroup \ $
–丹尼·亚伯拉罕
2011-2-5 21:05



\ $ \ begingroup \ $
@PavelDruzyak在上面很有意义。我们已经测试了控制器与模型(而非视图层)的交互。无论您选择使用RSpec还是Shoulda匹配器(几周前我上次检查时,每个版本的最新版本都不兼容,尽管可能是固定的),您最终应该超越验证响应是否成功并确保适当的响应的范围。返回
\ $ \ endgroup \ $
–丹尼·亚伯拉罕
2011-02-10 19:35



#3 楼

只是我考虑过的一些事情,如何将分页的东西隐藏在辅助方法的后面,然后以这种方式对辅助方法进行存根,这样您就可以随时随地交换分页实现
,然后测试辅助方法是否委托了参数转到will_paginate

class ProgramssController < ApplicationController
  def index
    @programs = paginate_progams(params)
  end
end


现在,您对paginate_programs方法进行存根处理
,然后可以指定辅助方法paginate

#4 楼

我已经离开RoR几年了,但是在一家致力于测试RSpec的公司工作。

在这种情况下,我们会测试一些其他要点,主要是关于内容回应。几行代码(语法可能是完全错误的,正如我所说的那样):

测试记录量
@ programs.should.have(30).records

然后有一个命令来测试结果中是否有某些记录
@ programs.should.contain(...)
这当然需要固定装置。

其目的是确保我们可以将某个插件替换为一个新插件,并确保它不会破坏任何东西或不包含任何错误。 (例如,acts_as_taggable插件存在一个导致无法删除标签的错误,事情陷入了无限循环)
当然,要避免程序员在浏览器或类似产品中进行测试时进行更改。使用will_paginate时,一个典型的错误是将分页设置为10以在浏览器中查看多个页面(因为固定装置中只有20条记录...)而忘记将此值设置回30。 ,那么也许是从事HTML / CSS工作的人。我们确保测试了所有这些细节

更多测试将确保使用正确的文件进行渲染。
诸如response.should.render_template(name)之类的东西

这似乎是很多工作。但从积极的方面来看:当我为客户建立的第一个网站上线后,经过六周的编程并且我对Ruby on Rails的了解只有3个月,它完全没有错误,每分钟只有1,000次命中。要更改的代码。