优化Rails RSpec测试

时间:2013-07-27 01:34:49

标签: ruby-on-rails ruby rspec factory-bot ruby-on-rails-4

我正在为我的Rails 4应用程序进行测试,而且我很擅长使用RSpec。我有一个名为AppsController的控制器,它有标准索引,new,show,create ...方法,它们都按照Rails建议的方式工作。“new”创建对象的新实例,创建实际保存它,显示,显示它和索引显示所有对象。以下是我目前的测试,任何人都可以看到任何潜在的问题或我可以改进的事情吗?

FactoryGirl.define do
  factory :developer do
    email 'example@me.com'
    password 'new_york'
    password_confirmation 'new_york'
    tos '1'
  end

  factory :app do
    name 'New App'
    tos '1'
  end

  factory :invalid_app, parent: :app do
    name 'nil'
    tos '0'
  end
end

require 'spec_helper'

def create_valid!
  post :create, app: app_attributes
end

def create_invalid!
  post :create, app: app_invalid_attributes
end

def show!
  get :show, id: app
end

def update_valid!
  put :update, id: app, app: app_attributes
end

def update_invalid!
  put :update, id: app, app: app_invalid_attributes
end

def delete!
  delete :destroy, id: app
end

def http_success
  expect(response).to be_success
end

def expect_template(view)
  expect(response).to render_template(view)
end

describe AppsController do
  render_views

  before(:each) do
    @developer = FactoryGirl.create(:developer)
    @developer.confirm!
    sign_in @developer
  end

  let(:app) { FactoryGirl.create(:app, developer: @developer) }
  let(:app_attributes) { FactoryGirl.attributes_for(:app) }
  let(:app_invalid_attributes) { FactoryGirl.attributes_for(:invalid_app) }

  describe 'GET #index' do
    it 'responds with an HTTP 200 status' do
      get :index
      http_success
    end

    it 'renders the :index view' do
      get :index
      expect_template(:index)
    end

    it 'populates @apps with the current_developers apps' do
      app = FactoryGirl.create(:app, :developer => @developer)
      get :index
      expect(assigns(:app)).to eq([app])
    end
  end

  describe 'POST #create' do
    context 'with valid parameters' do
      it 'creates a new app' do
        expect { create_valid!
        }.to change(App, :count).by(1)
      end

      it 'redirects to the new app keys' do
        create_valid!
        expect(response).to redirect_to keys_app_path(App.last)
      end
    end

    context 'with invalid parameters' do
      it 'does not create the new app' do
        expect { create_invalid!
        }.to_not change(App, :count)
      end

      it 'renders the :new view' do
        create_invalid!
        expect_template(:new)
      end
    end
  end

  describe 'GET #show' do
    it 'responds with an HTTP 200 status' do
      show!
      http_success
    end

    it 'renders the :show view' do
      show!
      expect_template(:show)
    end

    it 'populates @app with the requested app' do
      show!
      expect(assigns(:app)).to eq(app)
    end
  end

  describe 'PUT #update' do
    context 'with valid parameters' do
      it 'locates the requested app' do
        update_valid!
        expect(assigns(:app)).to eq(app)
      end

      it 'changes app attributes' do
        update_valid!
        expect(app.name).to eq('Updated App')
      end

      it 'redirects to the updated app' do
        update_valid!
        expect(response).to redirect_to app
      end
    end

    context 'with invalid parameters' do
      it 'locates the requested app' do
        update_invalid!
        expect(assigns(:app)).to eq(app)
      end

      it 'does not change app attributes' do
        update_invalid!
        expect(app.name).to_not eq('Updated App')
      end

      it 'renders the :edit view' do
        update_invalid!
        expect_template(:edit)
      end
    end
  end

  describe 'DELETE #destroy' do
    it 'deletes the app' do
      expect { delete!
      }.to change(App, :count).by(-1)
    end

    it 'redirects to apps#index' do
      delete!
      expect(response).to redirect_to apps_url
    end
  end
end

count should have been changed by -1, but was changed by 0 - on DELETE #destroy

expecting <"new"> but rendering with <[]> - on POST #create

expected: "Updated App"
     got: "New App"     - on PUT #update

expecting <"edit"> but rendering with <[]> - on PUT #update

expected: [#<App id: nil, unique_id: "rOIc5p", developer_id: 18, name: "New App">]
     got: nil           - on GET #index

3 个答案:

答案 0 :(得分:0)

小事 - 你的#http_success方法测试完全相同的东西两次。

你也可以通过在#before块之后放一个#let语句来分解对app的引用:

let(:app) { FactoryGirl.create(:app, developer: @developer) }

然后在你的规格中,只是

it 'renders the :show view' do
  get :show, id: app
  expect_template(:show)
end

编辑: 然后操作的顺序是1)在#before块中创建@developer,2)输入规范,3)在规范中第一次引用app时,#let块将创建一个应用程序的实例。

这意味着您无法在#index规范中分解应用创建,因为在这种情况下,规范会在创建应用之前调用该操作。

答案 1 :(得分:0)

我想到的一些事情,阅读你的代码:

您不需要在不带参数的方法调用中包含parens。只需http_success即可。

您应该尝试一致地使用现代RSpec期望语法。而不是assigns(:app).should eq(app),请使用expect(assigns(:app)).to eq(app)。 (这有一个例外,即对模拟的期望(即。should_receive(:message)),它只会采用RSpec 3的现代期望语法。

对于控制器规范,我喜欢为实际调用动作的每个动作创建一些小方法。您会注意到get :show, id: app规范中多次致电GET #show。为了更多地删除您的规范,您可以在describe块中编写以下方法:

def show!
  get :show, id: app
end

尝试一致地使用一种哈希语法。更重要的是,Rails 4无法使用Ruby 1.8运行,因此(几乎)没有理由使用哈希火箭哈希语法。

如果我变得非常,非常挑剔,我通常认为规范中的实例变量是一种气味。几乎在所有情况下,实例变量都应重构为已记忆的let / given块。

如果我真的,真的,真的挑剔,我更倾向于将控制器规格视为控制器的严格单元测试,而不是集成测试(这就是Capybara的用途),因此你根本不应该运用你的模型层。您应该仅测试控制器是否正在向模型层发送正确的消息。换句话说,所有模型层的东西都应该被删除。例如:

describe 'GET #show' do
  let(:app) { stub(:app) }

  before do
    App.stub(:find).and_return(app)
  end

  it 'populates @app' do
    get :show, id: app
    assigns(:app).should eq(app)
  end
end

我知道这最后是个人偏好,不是形而上学的事实,甚至不一定是广泛的标准惯例,所以你可以选择接受或离开它。我更喜欢它,因为它使我的规格非常快速,并且在我的控制器动作做得太多时给了我一个非常明确的启发式,我可能需要考虑重构。这可能是一个很好的习惯。

答案 2 :(得分:0)

首先,我不确定,但我怀疑你的invalid app工厂可能是错的。你的意思是

  factory :invalid_app, parent: :app do
    name nil
    tos '0'
  end
  • nil作为红宝石NilClass而不是"nil"作为字符串?

关于清理和其他内容的其他评论,以下是我的一些想法。

通过对每个before使用describe块,您可以避免使用某些辅助方法和重复。只需要进行索引测试就可以得到更像

的东西
describe 'GET #index' do
  before do
    get :index
  end
  it 'responds with an HTTP 200 status' do
    http_success
  end

  it 'renders the :index view' do
    expect_template(:index)
  end

  it 'populates @apps with the current_developers apps' do
    expect(assigns(:app)).to eq([app])
  end
end

另请注意,您不需要重新创建app,因为let正在根据需要进行重新设置。

关于失败,我怀疑delete计数更改可能失败,因为在期望内,测试框架正在创建一个新的应用程序(来自let)然后删除它导致计数更改0.对于该测试,您需要确保您的app是在您的期望之外创建的。因为您使用的是let,所以您可以这样做:

describe 'DELETE #destroy' do
  it 'deletes the app' do
    # ensure that app is already created
    app
    expect { 
      delete!
    }.to change(App, :count).by(-1)
  end
end

或者,将let更改为let!,这会在规范实际运行之前强制创建。

至于其他失败,想想@DanielWright提出了辅助方法,我发现那些使调试变得复杂。我无法看到您将应用名称设置为&#34;更新应用程序&#34;的位置。也许更清晰的测试(对于那个特定的测试)不会使用辅助方法,但可能更明确。像

这样的东西
describe 'PUT #update' do
  let(:app_attributes) { FactoryGirl.attributes_for(:app, name: 'The New App Name') }
  before do
    put :update, id: app, app: app_attributes
  end
  context 'with valid parameters' do
    it 'locates the requested app' do
      expect(assigns(:app)).to eq(app)
    end

    it 'changes app attributes' do
      # notice the reload which will make sure you refetch this from the db
      expect(app.reload.name).to eq('The New App Name')
    end

    it 'redirects to the updated app' do
      expect(response).to redirect_to app
    end
  end
end

对于其他错误,您可能希望开始调试代码。你确定它应该有用吗?你看过输出日志了吗?也许测试正在执行工作并在控制器代码中发现错误。你做过任何逐步调试吗?