How to mock database calls with RSpec - ruby-on-rails

I am trying to test a JSON API endpoint and I am getting an ActiveRecord::RecordNotFound exception because the call to user.find is failing.
How can I mock the active record calls for my test so that it return something and doesn't throw an exception?
(I am using factory_girl also, in case I can use that)
def do_something
success = false
message = ""
user = User.find(params[:user_id])
if user.present?
# ...
if user.save!
success = true
end
end
render json: {
"success": success,
"message": message
}
end
My RSpec looks like:
#RSpec.describe Api::UsersController, type: :controller do
it "should return some JSON" do
payload = {
user_id: "1",
# ...
}.to_json
post :do_something, payload, format: :json
expected = {
success: false,
message: ""
}.to_json
expect(response.body).to eq(expected)
end

I think you need to create a user first by using FactoryGirl, then you can pass id of that user in payload, like this:
let(:user) { create :user } # create user by FactoryGirl
it "should return some JSON" do
payload = {
user_id: user.id,
# ...
}.to_json
...
end

Well in case you want to mock database with ActiveRecord, you can try something like this.
let(:user) { build_mock_class.new() }
before(:all) { create_table }
after(:all) { drop_table }
def build_mock_class
Class.new(ActiveRecord::Base) do
self.table_name = 'mock_table'
end
end
def create_table
ActiveRecord::Base.connection.create_table :mock_table do |t|
t.integer :user_id
t.timestamps
end
end
def drop_table
ActiveRecord::Base.connection.drop_table :mock_table
end
But you have to establish connection before you proceed and you can put your connection adapter in spec_helper
ActiveRecord::Base.establish_connection(
adapter: 'sqlite3',
database: ':memory:'
)
You can change your column names with whatever you want and also this will create table when spec is run and will destroy once spec is passed.
Note: Don't forget to require active_record and sqlite3 in your spec_helper or wherever you want to use.
Hope it helps.

Related

Mock a singleton class In Ruby on Rails using Rspec [SOLVED]

Hi I Try to create a mock for follow class:
module EstablishmentsQueryService
class << self
def find_by_id(id)
Establishment.find_by!(id:)
rescue ActiveRecord::RecordNotFound
raise EstablishmentNotFoundError.new id
end
end
end
to try test my controller
# frozen_string_literal: true
module Api
module V1
# Controllewr to manager Establishments
class EstablishmentsController < Api::V1::ApiController
before_action :validate_id, only: %i[destroy update show]
before_action :load_establishment, only: %i[destroy update show]
def show; end
def create
#establishment = Establishment.new(establishment_params)
#establishment = EstablishmentService.save(#establishment)
render status: :created
end
def destroy
EstablishmentService.delete(#establishment)
end
def update
#establishment.attributes = establishment_params
#establishment = EstablishmentService.save(#establishment)
end
private
def validate_id
message = I18n.t('establishment_controller.id.invalid', id: params[:id])
UuidValidateService.call(params[:id], message)
end
def load_establishment
#establishment = EstablishmentsQueryService.find_by_id(params[:id])
end
def establishment_params
params.require(:establishment).permit(:name, :cnpj, :description)
end
end
end
end
follow my test:
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'Api::V1::Establishments', type: :request do
describe 'GET /api/v1/establishments/:id' do
context 'when has establishment' do
let(:establishment) { build(:establishment, id: p, created_at: DateTime.now, updated_at: DateTime.now) }
before do
allow_any_instance_of(EstablishmentsQueryService).to receive(:find_by_id).and_return(establishment)
get "/api/v1/establishments/#{establishment.id}"
end
it 'then http status is ok' do
expect_status_is_ok
end
it 'has body equal to expected' do
except_field_by_field(establishment, body_to_open_struct, %i[id name cnpj description])
end
end
context 'when has no establishment' do
before do
get "/api/v1/establishments/#{UUID.new.generate}"
end
it 'then http status is not_found' do
expect_status_is_not_found
end
end
context 'when use invalid id' do
before { get "/api/v1/establishments/#{FFaker::Lorem.word}" }
it 'then http status is bad_request' do
expect_status_is_bad_request
end
end
end
describe 'PUT /api/v1/establishments/:id' do
let(:establishments_query_service) { allow(EstablishmentsQueryService) }
let(:establishments_service) { allow(EstablishmentsService) }
context 'when updated with success' do
let(:establishment) { build(:establishment) }
let(:id) { UUID.new.generate }
before do
establishments_query_service.to receive(:find_by_id) { |p| build(:establishment, id: p, created_at: DateTime.now, updated_at: DateTime.now) }
establishments_service.to receive(:save) do |p|
to_return = p
to_return.created_at = DateTime.now
to_return.updated_at = DateTime.now
end
put "/api/v1/establishments/#{id}"
end
it 'then http status is ok' do
expect_status_is_ok
end
it 'has body equal to expected' do
actual = body_to_open_struct
except_field_by_field(establishment, actual, %i[name cnpj description])
expected(actual.id).to eq(id)
end
end
context 'when has no establishment' do
end
context 'when has constraint violation' do
end
end
describe 'DELETE /api/v1/establishments/:id' do
end
describe 'POST /api/v1/establishments' do
end
end
If I work using allow_any_instance_of a test ignore configuration, use a real configuration and fails because has no data stores. If I use double I received a follow error:
Api::V1::Establishments GET /api/v1/establishments/:id when has establishment then http status is ok
Failure/Error: allow_any_instance_of(EstablishmentsQueryService).to receive(:find_by_id).and_return(establishment)
EstablishmentsQueryService does not implement #find_by_id
I think the right away is user allow_any_instance_of because this config is for static methods, but didn't work
how can I mock my class to test my controller? I using Ruby 3.1.2, rails 7.0.3 and rspec-rails 5.1.2
thank you
I found my problem, I forgot to definie expected params in my confi using with()
allow(EstablishmentsQueryService).to receive(:find_by_id).with(establishment.id).and_return(establishment)

Rspec: How to test a method that raises an error

I have a SubscriptionHandler class with a call method that creates a pending subscription, attempts to bill the user and then error out if the billing fails. The pending subscription is created regardless of whether or not the billing fails
class SubscriptionHandler
def initialize(customer, stripe_token)
#customer = customer
#stripe_token = stripe_token
end
def call
create_pending_subscription
attempt_charge!
upgrade_subscription
end
private
attr_reader :stripe_token, :customer
def create_pending_subscription
#subscription = Subscription.create(pending: true, customer_id: customer.id)
end
def attempt_charge!
StripeCharger.new(stripe_token).charge! #raises FailedPaymentError
end
def upgrade_subscription
#subscription.update(pending: true)
end
end
Here is what my specs look like:
describe SubscriptionHandler do
describe "#call" do
it "creates a pending subscription" do
customer = create(:customer)
token = "token-xxx"
charger = StripeCharger.new(token)
allow(StripeCharger).to receive(:new).and_return(charger)
allow(charger).to receive(:charge!).and_raise(FailedPaymentError)
handler = SubscriptionHandler.new(customer, token)
expect { handler.call }.to change { Subscription.count }.by(1) # Fails with FailedPaymentError
end
end
end
But this does not change the subscription count, it fails with the FailedPaymentError. Is there a way to check that the subscription count increases without the spec blowing up with FailedPaymentError.
You should be able to use Rspec compound expectations for this
https://relishapp.com/rspec/rspec-expectations/docs/compound-expectations
So I'll re-write your expectation to something like this:
expect { handler.call }.
to raise_error(FailedPaymentError).
and change { Subscription.count }.by(1)
It can be done like this
expect{ handler.call }.to raise_error FailedPaymentError
Should work.
If you don't want to raise error at all then you can remove this line, and return a valid response instead
allow(charger).to receive(:charge!).and_raise(FailedPaymentError)
More info - How to test exception raising in Rails/RSpec?
Official RSpec docs
https://relishapp.com/rspec/rspec-expectations/v/2-0/docs/matchers/expect-error

How to test "Model.valid?" on a FactoryBot generated model using Rspec?

I have created a Chatroom Model that first validates for the presence of some fields so in the controller I create the chatroom then check if it is valid by using the .valid? method to determine the response. Now when I created a test model using FactoryBot the test doesn't go past the if statement and it returns a response as if the test has finished.
Code for my action
def create
new_chatroom = Chatroom.create(chatroom_params)
if new_chatroom.valid?
new_chatroom.members.create({ user_id: #current_user[:username] })
render_response(new_chatroom, :created)
else
render_error_response(new_chatroom.errors, :bad_request)
end
end
Code for the factory
FactoryBot.define do
factory :chatroom do
topic { Faker::Lorem.unique.question }
slug { Faker::IndustrySegments.unique.sub_sector }
description { Faker::Lorem.paragraph }
owner { Faker::Name.first_name }
public { true }
end
end
Here is my test
it "creates a new chatroom" do
post :create, params: {
:topic => "test chatroom",
:slug => "code-testing",
:description => "Testing with Rspec",
}
expect(response).to have_http_status(:created)
end
Here is the render_response method:
def render_response(resource, status)
if block_given?
yield(resource, status)
else
render json: resource, :status => status
end
end
Test failure:
Failure/Error: expect(response).to have_http_status(:created)
expected the response to have status code :created (201) but it was :ok (200)
I get this failure and when I try to make it pass(false positive), the coverage shows the response I'm testing against is not what's actually in my action because the rest of the lines starting from the if statement are not covered.
but I thought FactoryBot takes over the whole model creation in the
tests.
No - FactoryBot just provides factories that create model instances. This is widely used as a replacement for fixtures to populate the database before tests. Unlike with fixtures this is not automatic.
Just adding FactoryBot changes absolutely nothing in your application besides the fact that the generators will create the factory file. It does not effect the behaviour of your models in any way.
When testing the creation of resources you need to test that:
Given valid params, then a model should be persisted to the database
Given valid params, then the response should be successful and point to the newly created resource.
Given invalid params, then a model should not be persisted to the database
Given invalid params, then the response should be 422 and an error page should be rendered.
You want to test this with a request spec and not a controller spec.
Request specs provide a high-level alternative to controller specs. In
fact, as of RSpec 3.5, both the Rails and RSpec teams discourage
directly testing controllers in favor of functional tests like request
specs.
require "rails_helper"
RSpec.describe "Chatroom creation", type: :request do
let(:valid_params) do
{
chatroom: {
topic: "test chatroom",
slug: "code-testing",
description: "Testing with Rspec"
}
}
end
let(:invalid_params) do
{
chatroom: {
topic: ''
}
}
end
context "when the parameters are valid" do
it "creates a new chatroom" do
expect do
post '/chatrooms', params: valid_params
end.to change(Chatroom, :count).by(1)
end
it "returns success" do
post '/chatrooms', params: valid_params
expect(response).to have_http_status(:created)
end
end
context "when the parameters are invalid" do
it "does not create a new chatroom" do
expect do
post '/chatrooms', params: invalid_params
end.to_not change(Chatroom, :count)
end
it "returns bad entity" do
post '/chatrooms', params: invalid_params
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
Then we can address the problem with your controller which should read:
class ChatroomsController < ApplicationController
# ...
def create
new_chatroom = Chatroom.new(chatroom_params)
if new_chatroom.save
new_chatroom.members.create(user_id: #current_user[:username])
render_response(new_chatroom, :created)
else
render_error_response(new_chatroom.errors, :bad_request)
end
end
end
You should never use .valid? to check if a record was saved to the database. It only ensures that the model level validations have passed. Not that the INSERT statement from .create actually created a row in the database. See The Perils of Uniqueness Validations for an example of what can happen.
While you can use new_chatroom.persisted? this is the common rails idiom since it gives you a variable that you can manipulate before the record is persisted.
class ChatroomsController < ApplicationController
# ...
def create
new_chatroom = Chatroom.new(chatroom_params)
new_chatroom.members.new(user: current_user)
if new_chatroom.save
render_response(new_chatroom, :created)
else
render_error_response(new_chatroom.errors, :bad_request)
end
end
end
I would not try to test model validity in a controller spec, but rather write request specs (as suggested by the other respondent). You can test the validity of an object on the model itself like this:
context 'valid' do
let(:chatroom) { create :chatroom }
it { expect(chatroom).to be_valid }
end
context 'fails validation' do
let(:chatroom) { create :chatroom, topic: nil }
it { expect(chatroom).not_to be_valid }
end
Resource: https://www.rubydoc.info/gems/rspec-rails/RSpec%2FRails%2FMatchers:be_valid
But if you want to check that actual fields are validated, I recommend using shoulda matchers on the model like this:
it { should validate_presence_of :topic }
it { should validate_presence_of :slug }
Resource: https://github.com/thoughtbot/shoulda-matchers

How can I test ActionCable using RSpec?

This is my NotificationChannel
class NotificationChannel < ApplicationCable::Channel
def subscribed
stream_from "notification_user_#{user.id}"
end
def unsubscribed
stop_all_streams
end
end
How can I write test for this ActionCable channels
This is my Rspec
require 'rails_helper'
require_relative 'stubs/test_connection'
RSpec.describe NotificationChannel, type: :channel do
before do
#user = create(:user)
#connection = TestConnection.new(#user)
#channel = NotificationChannel.new #connection, {}
#action_cable = ActionCable.server
end
let(:data) do
{
"category" => "regular",
"region" => "us"
}
end
it 'notify user' do
#error is in below line
expect(#action_cable).to receive(:broadcast).with("notification_user_#{#user.id}")
#channel.perform_action(data)
end
end
when I run this spec it gives error
Wrong number of arguments. Expected 2, got 1
I used this link to write code for stub and this file.
Rails version - 5.0.0.1
Ruby version - 2.3.1
expect(#action_cable).to receive(:broadcast).with("notification_user_#{#user.id}")
Looking closely broadcast needs two parameters so
expect(#action_cable).to receive(:broadcast).with("notification_user_#{#user.id}", data)
I cant guess what is going on however one issue is
let(:data) do
{
"action" => 'action_name',
"category" => "regular",
"region" => "us"
}
end
You need an action for perform_action.
However you dont have any action defined in NotificationsChannel.
Otherwise you can try
NotificationChannel.broadcast_to("notification_user_#{#user.id}", data )

Achieving 100% test coverage in Rails Oauth using Rspec stubs and mocks

I am trying to figure out a way to stub/mock the access token calls to provide coverage to methods called when a user's token has expired. The more guides I read on this issue the more I get confused. I do not want to call the external provider, and I want to confirm the methods report 100% coverage in case a developer modifies them and they work incorrectly. What should I add to the spec below to make it reach our testing goal of 100%?
The load_json_fixture('omitted_oauth') brings in a JSON fixture based on what the initial Oauth call returns.
Model Concern
module OmittedOmniAuthentication
extend ActiveSupport::Concern
module ClassMethods
def from_omniauth(auth)
Rails.logger.debug auth.inspect
where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
setup_user(user, auth)
end
end
def setup_user(user, auth)
user.provider = auth.provider
user.uid = auth.uid
user.email = auth.info.email
user.customer_ids = auth.extra.raw_info.customer_ids
user.store_token(auth.credentials)
end
end
def refresh_token!
access_token ? refresh_access_token! : false
end
def refresh_access_token!
result = access_token.refresh!
store_token(result)
save
rescue OAuth2::Error
false
end
def settings
#settings ||= Devise.omniauth_configs[:omitted].strategy
end
def strategy
#strategy ||= OmniAuth::Strategies::Omitted.new(nil, settings.client_id, settings.client_secret, client_options: settings.client_options)
end
def client
#client ||= strategy.client
end
def access_token
OAuth2::AccessToken.new(client, token, refresh_token: refresh_token)
end
def store_token(auth_token)
self.token = auth_token.token
self.refresh_token = auth_token.refresh_token
self.token_expires_at = Time.at(auth_token.expires_at).to_datetime
end
def token_expired?
Time.now > token_expires_at
end
end
Rspec Spec
RSpec.describe 'OmittedOmniAuthentication', type: :concern do
let(:klass) { User }
let(:user) { create(:user) }
let(:user_oauth_json_response) do
unfiltered_oauth_packet = load_json_fixture('omitted_oauth')
unfiltered_oauth_packet['provider'] = unfiltered_oauth_packet['provider'].to_sym
unfiltered_oauth_packet['uid'] = unfiltered_oauth_packet['uid'].to_i
unfiltered_oauth_packet
end
before do
OmniAuth.config.test_mode = true
OmniAuth.config.mock_auth[:omitted] = OmniAuth::AuthHash.new(
user_oauth_json_response,
credentials: { token: ENV['OMITTED_CLIENT_ID'], secret: ENV['OMITTED_CLIENT_SECRET'] }
)
end
describe "#from_omniauth" do
let(:omitted_oauth){ OmniAuth.config.mock_auth[:omitted] }
it 'returns varying oauth related data for Bigcartel OAuth response' do
data = klass.from_omniauth(omitted_oauth)
expect(data[:provider]).to eq(user_oauth_json_response['provider'].to_s)
expect(data[:uid]).to eq(user_oauth_json_response['uid'].to_s)
expect(data[:email]).to eq(user_oauth_json_response['info']['email'])
expect(data[:customer_ids]).to eq(user_oauth_json_response['extra']['raw_info']['customer_ids'])
end
end
describe '#token expired?' do
it 'true if valid' do
expect(user.token_expired?).to be_falsey
end
it 'false if expired' do
user.token_expires_at = 10.days.ago
expect(user.token_expired?).to be_truthy
end
end
end
UPDATE
describe '#refresh_access_token!' do
it 'false if OAuth2 Fails' do
allow(user).to receive(:result).and_raise(OAuth2::Error)
expect(user.refresh_access_token!).to be_falsey
end
it 'false if refresh fails' do
allow(user).to receive(:access_token) { true }
allow(user).to receive(:refresh_access_token!) { false }
expect(user.refresh_token!).to be_falsey
end
it 'true if new token' do
allow(user).to receive(:access_token) { true }
allow(user).to receive(:refresh_access_token!) { true }
expect(user.refresh_token!).to be_truthy
end
it 'true when refreshed' do
allow(user).to receive(:access_token) { true }
allow(user).to receive(:refresh_access_token!) { true }
allow(user).to receive(:store_token) { true }
allow(user).to receive(:save) { true }
expect(user.refresh_access_token!).to be_truthy
end
end
=> I was able to get to 94.12% with these updates
I'm not sure where you might be calling the external provider, so I'm not sure what you want to stub/mock.
To get you a little closer to your coverage goal, try adding another spec for your simplest module methods:
describe '#refresh_token!' do
it 'is true if there is an access_token' do
if !user.access_token?
expect(user.refresh_token!).to be_truthy
end
end
# Do you have factories or fixtures set up that can force
# #access_token? to be falsey?
it 'is false if there is no access_token' do
if !user.access_token?
expect(user.refresh_token!).to be_falsey
end
end
# Maybe you want to set the falsey value for the access_token
# as you have have for the value of token_expires_at in
# your #token_expired? test.
it 'is false if there is no access_token' do
# You should be able to force the method to return a false
# value (stub the method) with this line
allow(user).to receive(:access_token) { false }
expect(user.refresh_token!).to be_falsey
end
end
This example feels a little unnecessary since your access_token method appears that it will never return false. I would expect that your access_token method will always return an object, or an error, so your refresh_token! method would never encounter a falsey condition in the ternary. Maybe you should instead rescue and return false.
Regardless, I think the point is that you should stub the method with the allow method, and that will get you on your way to figuring out your method stubs. Hope it helps somewhat.
For refresh_access_token! you can unit test the method by stubbing the user.result method with an error, and not stubbing for the "successful" result of the refresh_access_token! method.
describe '#refresh_access_token!' do
it 'it returns true when refreshed' do
# The successful control flow path for this method
# is to save the user and return true.
# I suppose this would happen smoothly in your tests and app.
expect(user.refresh_access_token!).to be_truthy
end
it 'returns false when an OAuth2 Error is rescued' do
# To force the case that you receive an OAuth2 Error,
# stub the user's access_token return value with the Error
# The refresh_access_token! method should then rescue the error
# and cover the false return value of the method
allow(user).to receive(:access_token) { OAuth2::Error }
expect(user.refresh_access_token!).to be_falsey
end
end
(Posted solution on behalf of the question author).
This is now working. With the following spec adjustment stubbing the method chain I was able to get a successful call of true for the method:
def refresh_access_token!
result = access_token.refresh!
store_token(result)
save
rescue OAuth2::Error
false
end
The completed spec that pushed me to 100%
it 'true when refreshed' do
auth_token = OpenStruct.new(token: FFaker::Lorem.characters(50),
refresh_token: FFaker::Lorem.characters(50),
expires_at: 5.days.from_now)
allow(user).to receive_message_chain('access_token.refresh!') { auth_token }
expect(user.refresh_access_token!).to be_truthy
end
Stubs and Mocks can be fun. I learned a ton from this thread. Here are the Rspec 3.4 docs on this.

Resources