3
\$\begingroup\$

I've been trying to create a shared example to quickly and efficiently test controllers with CRUD operations that might also be nested.

I came up with this solution that works, but I feel that can be improved.

 module ApiHelper
 RSpec.shared_examples "a CRUD controller" do |model:|
 def self.controller_has_action?(action)
 described_class.action_methods.include?(action.to_s)
 end
 def self.model_belongs_to_associations(model)
 model.reflect_on_all_associations(:belongs_to).map(&:name).nil?
 end
 resource_singular = model.name.underscore.to_sym
 parent_resource = model.reflect_on_all_associations(:belongs_to).map(&:name).first
 #let(:parent_resource) { model.reflect_on_all_associations(:belongs_to).map(&:name) }
 let(:records) { FactoryBot.create_list(resource_singular, 10) }
 let(:parent_record) { FactoryBot.create(parent_resource.to_sym) }
 before(:each) { request.env["HTTP_ACCEPT_LANGUAGE"] = "en" }
 describe "#show", if: controller_has_action?(:show) do
 context "when requested record exists" do
 let(:record) { records[rand 10] }
 before(:each) do
 get :show, params: {id: record.id}
 end
 it "succeeds" do
 expect(response).to have_http_status(:success)
 end
 it "returns the requested record" do
 expect(json["id"]).to eq(record.id)
 end
 end
 end
 describe "#index", if: controller_has_action?(:index) do
 let(:params) {
 params = {}
 unless parent_resource.nil?
 params.merge!((parent_resource.to_s << "_id").to_sym => parent_record.id)
 end
 }
 before(:each) do
 get :index, params: params
 end
 it "succeeds" do
 expect(response).to have_http_status(:success)
 end
 it "returns the factory names" do
 record_names = json.map { |c| c["name"] }
 expect(record_names).to all(be_a(String).and(include(resource_singular.capitalize.to_s)))
 end
 end
 # noinspection RubyDuplicatedKeysInHashInspection
 describe "#create", if: controller_has_action?(:create) do
 let(:request_params) {
 params = {}
 unless parent_resource.nil?
 params.merge!((parent_resource.to_s << "_id").to_sym => parent_record.id)
 end
 params.merge!(resource_singular => record_attrs)
 }
 before(:each) do
 post :create, params: request_params
 end
 context "when valid" do
 let(:record_attrs) { attributes_for(resource_singular) }
 it "succeeds" do
 expect(response).to have_http_status(:created)
 expect(response.headers["Content-Type"]).to eql("application/json; charset=utf-8")
 end
 it "saves the new record" do
 expect { post :create, params: request_params }.to change(model, :count).by(1)
 end
 end
 context "when invalid" do
 let(:record_attrs) { attributes_for(resource_singular, :invalid) }
 it "fails" do
 expect(response).to have_http_status(:unprocessable_entity)
 end
 end
 end
 describe "#update" do
 let(:record) { records[rand 10] }
 before(:each) do
 patch :update, params: {resource_singular => new_attrs, :id => record.id}
 end
 context "when valid" do
 let(:new_attrs) { attributes_for(resource_singular) }
 it "succeeds" do
 expect(response).to have_http_status(:success)
 end
 it "saves update" do
 record.reload
 expect(record).to have_attributes(new_attrs)
 end
 end
 context "when invalid" do
 let(:new_attrs) { attributes_for(resource_singular, :invalid) }
 it "fails" do
 expect(response).to have_http_status(:unprocessable_entity)
 end
 end
 end
 describe "#destory", if: controller_has_action?(:destroy) do
 context "when record exists" do
 let(:record) { records[rand 10] }
 before(:each) do
 delete :destroy, params: {id: record.id}
 end
 it "success" do
 expect(response).to have_http_status(:no_content)
 end
 it "remove record" do
 expect(model.all).not_to include(record)
 end
 end
 context "when requested record doesn't exist" do
 it "throws exception" do
 bypass_rescue
 expect { delete :destroy, params: {id: -1} }.to raise_exception(ActiveRecord::RecordNotFound)
 end
 end
 end
 describe "#translations" do
 context "#show DE" do
 before(:each) { request.env["HTTP_ACCEPT_LANGUAGE"] = "de" }
 let(:record) { records[rand(10)] }
 it "returns the title in DE" do
 get :show, params: {id: record.id}
 puts(record.attributes)
 expect(json["name"]).to eq(record.name)
 end
 end
 end
 end
end

This can be called like this:

it_behaves_like "a CRUD controller", model: Island
asked Dec 4, 2019 at 18:18
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

A couple notes.

1) You don't have to pass :each to before calls, it's the default:

# Before
before(:each) do
 get :index, params: params
end
# After
before { get :index, params: params }

2) The code may benefit from the use of be matchers:

# Before
it "success" do
 expect(response).to have_http_status(:no_content)
end
it "succeeds" do
 expect(response).to have_http_status(:success)
end
# After
it { is_expected.to be_no_content }
it { is_expected.to be_successful }

3) In some cases the content of params object is ambiguous, because unless returns nil when the condition is falsy. Here is an example of different params initialization:

# Before
let(:params) {
 params = {}
 unless parent_resource.nil?
 params.merge!((parent_resource.to_s << "_id").to_sym => parent_record.id)
 end
}
# After
let(:params) { parent_resource.nil? ? {} : { "#{parent_resource}_id".to_sym => parent_record.id } }

5) You're likely setting the language header in multiple places throughout the code, so a helper would come handy:

def set_language_header(lang)
 request.env["HTTP_ACCEPT_LANGUAGE"] = lang
end
before { set_language_header(:en) }
before { set_language_header(:de) }

Hope it helps.

answered Dec 15, 2019 at 12:18
\$\endgroup\$
1
  • \$\begingroup\$ Thank you, what do you think of this part parent_resource = model.reflect_on_all_associations(:belongs_to).map(&:name).first? With multiple belongs_to associations obviously it will be problematic. \$\endgroup\$ Commented Dec 15, 2019 at 12:36

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.