Search code examples
ruby-on-railsrspecrspec-rails

Ruby on Rails - Is this a good way to eliminate duplicate code in RSpec?


I wrote this code for testing controller update function. Wrote a method for eliminating duplicate code. Is this an explicit way to do it?

users_controller_spec.rb

context 'Update failed' do
  def render_edit
    user.reload
    expect(response.status).to eq(200)
  end
  it 'Name is nil' do
    put :update, params: { id: user.id, user: { name: '' } }
    render_edit
  end
  it 'Email is exist' do
    create(:user, email: 'user@gmail.com')
    put :update, params: { id: user.id, user: { email: 'user@gmail.com' } }
    render_edit
  end
  it 'Email is nil' do
    put :update, params: { id: user.id, user: { email: '' } }
    render_edit
  end
  it 'Password must be at least 8 characters' do
    put :update, params: { id: user.id, user: { password: '1234567', password_confirmation: '1234567' } }
    render_edit
  end
  it 'Passwords do not match' do
    put :update, params: { id: user.id, user: { password: '1234567890', password_confirmation: '123456789' } }
    render_edit
  end
end

I was thinking to use after(:each). But it looks a little wired in logic. Or use loop to replace params.

Any suggestion?


Solution

  • You can use shared examples as suggested in the comments, but there's an easier way.

    context 'Update failed' do
      before do
        put :update, params: params
        user.reload # I'm not sure why you need this
      end
    
      subject { response } 
    
      context 'Name is nil' do
        let(:params} { {id: user.id, user: { name: '' }} }
        it { is_expected.to be_success }
      end
    
      context 'Email exists' do
        let(:params) { { id: user.id, user: { email: 'user@gmail.com' } }
        let(:user) { create(:user, email: 'user@gmail.com') }
        it { is_expected.to be_success }
      end
      # and so on
    end
    

    The main rune I use is - make it obvious what change in each context. So instead of redefining put ..., extract it as a let and define it per context.

    be_success is part of rspec magic, wherever you use be_something matcher it'll try to use something? method and check if it's true, i.e.

    expect(foo).to be_empty? == expect(foo.empty?).to eq(true)

    If you don't want it make it like this

    subject { response.status }
    # and later
    is_expected.to eq 200 
    

    is_expected.to is just a shorthand for expect(subject).to