1
votes

Background: I've got an after_action callback in my controller, which takes the string address, processes it and stores longitude and latitude in corresponding fields. I want to test this.

This SO question, as well as this article only consider update methods, but at least, they are quite clear, because I've already got an object to work with.

So my question is - how to find this newly created record? This SO question led me to this code:

require 'rails_helper'

RSpec.describe Admin::Settings::GeneralSettingsController, type: :controller do

  context "POST methods" do
    describe "#edit and #create" do
      it "encodes and stores lang/lot correctly" do
          post :create, general_setting: FactoryGirl.attributes_for(:general_setting)
          expect(assigns(:general_setting).long).to eq(37.568021)
          # expect(general_setting.long).to eq(37.568021)
          # expect(general_setting.lat).to eq(55.805553)
      end
    end
  end
end

But using the code in the answer, I get this error:

Failure/Error: expect(assigns(:general_setting).long).to eq(37.568021)

     NoMethodError:
       undefined method `long' for nil:NilClass

Update #1: This is my new controller spec code:

RSpec.describe Admin::Settings::GeneralSettingsController, type: :controller do

  context 'POST methods' do
    before(:each) do
      allow(subject).to receive(:set_long_lat)
    end

    describe 'post create' do
      before(:each) do
        post :create, params: { general_setting: FactoryGirl.attributes_for(:general_setting) }
      end

      it "saves the record with valid attributes" do
        expect{subject}.to change{GeneralSetting.count}.by(1)
      end

      it 'calls :set_long_lat' do
        expect(subject).to have_received(:set_long_lat)
      end
    end
  end

  describe '#set_long_lat' do
    # spec for method
  end
end

Update #2:

Here is my controller code:

class Admin::Settings::GeneralSettingsController < AdminController
  include CrudConcern

  before_action :find_general_setting, only: [:edit, :destroy, :update, :set_long_lat]
  after_action :set_long_lat

  def index
    @general_settings = GeneralSetting.all
  end

  def new
    @general_setting = GeneralSetting.new
    # Билдим для того, что бы было видно сразу одно поле и пользователь не должен
    # кликать на "добавить телефон"
    @general_setting.phones.build
    @general_setting.opening_hours.build
  end

  def edit
    # Тоже самое, что и с нью - если телефонов нет вообще, то показываем одно пустое поле
    if @general_setting.phones.blank?
      @general_setting.phones.build
    end

    if @general_setting.opening_hours.blank?
      @general_setting.opening_hours.build
    end
  end

  def create
    @general_setting = GeneralSetting.new(general_setting_params)
    create_helper(@general_setting, "edit_admin_settings_general_setting_path")
  end

  def destroy
    destroy_helper(@general_setting, "admin_settings_general_settings_path")
  end

  def update
    # debug
    # @general_setting.update(language: create_hash(params[:general_setting][:language]))
    @general_setting.language = create_hash(params[:general_setting][:language])
    update_helper(@general_setting, "edit_admin_settings_general_setting_path", general_setting_params)
  end


  private

  def set_long_lat
    geocoder = Geocoder.new
    data = geocoder.encode!(@general_setting.address)
    @general_setting.update!(long: data[0], lat: data[1])
  end

  def find_general_setting
    @general_setting = GeneralSetting.find(params[:id])
  end

  def general_setting_params
    params.require(:general_setting).permit(GeneralSetting.attribute_names.map(&:to_sym).push(
      phones_attributes: [:id, :value, :_destroy, :general_setting_id ]).push(
      opening_hours_attributes: [:id, :title, :value, :_destroy, :deneral_setting_id]) )
  end

  def create_hash(params)
    language_hash = Hash.new

    params.each do |param|
      language_hash[param.to_sym] = param.to_sym
    end
    return language_hash
  end
end

(If it helps - I've got a lot of similar crud-actions, that is why I've put them all in a concern controller)

module CrudConcern 
  extend ActiveSupport::Concern
  include Language

  included do
    helper_method :create_helper, :update_helper, :destroy_helper, :get_locales
  end

  def get_locales
    @remaining_locales = Language.get_remaining_locales
  end

  def create_helper(object, path)
    if object.save!
      respond_to do |format|
        format.html {
          redirect_to send(path, object)
          flash[:primary] = "Well done!"
        }
      end
    else
      render :new
      flash[:danger] = "Something not quite right"
    end
    @remaining_locales = Language.get_remaining_locales
  end

  def update_helper(object, path, params)
    if object.update!(params)
      respond_to do |format|
        format.html {
          redirect_to send(path, object)
          flash[:primary] = "Well done!"
        }
      end
    else
      render :edit
      flash[:danger] = "Something's not quite right"
    end
  end

  def destroy_helper(object, path)
    if object.destroy
      respond_to do |format|
        format.html {
          redirect_to send(path)
          flash[:primary] = "Well done"    
        }
      end
    else
      render :index
      flash[:danger] = "Something's not quite right"
    end
  end

end

Update #3 It's not the ideal solution, but, somehow, controller tests just won't work. I've moved my callback into the model and updated my general_setting_spec test.

class GeneralSetting < ApplicationRecord
  after_save :set_long_lat    
  validates :url, presence: true

  private

  def set_long_lat
    geocoder = Geocoder.new
    data = geocoder.encode(self.address)
    self.update_column(:long, data[0])
    self.update_column(:lat, data[1])
  end
end

My tests now:

RSpec.describe GeneralSetting, type: :model do

  let (:regular) { FactoryGirl.build(:general_setting) } 

  describe "checking other validations" do
    it "is invalid with no url" do
      expect{
        invalid.save
      }.not_to change(GeneralSetting, :count)
    end

    it 'autofills the longitude' do
      expect{ regular.save }.to change{ regular.long }.from(nil).to(37.568021)
    end

    it 'autofills the latitude' do
      expect{ regular.save }.to change{ regular.lat }.from(nil).to(55.805078)
    end
  end
end
1
Current RSpec version discourages testing assings in controller. Why don't you want to use GeneralSetting.last? - Maxim Khan-Magomedov

1 Answers

0
votes

I would test expectation that controller calls method specified in after_action and make a separate test for that method.

Something like:

context 'POST methods' do
  before(:each) do
    allow(subject).to receive(:method_from_callback)
  end

  describe 'post create' do
    before(:each) do
      post :create, params: { general_setting: attributes_for(:general_setting) }
    end

    it 'calls :method_from_callback' do
      expect(subject).to have_received(:method_from_callback)
    end
  end
end

describe '#method_from_callback' do
  # spec for method
end

Be sure to use your method name instead of :method_from_callback pay attention that I've used rspec 3.5 syntax (wrapped request request parameters into params).