I am a Sr. Software Developer at Oracle Cloud. The opinions expressed here are my own and not necessarily those of my employer.
Rails Rspec mock tests
In object oriented programming classes call methods on other classes. While it’s important to test the integration between classes it is very useful to test them in isolation, simulating valid and invalid responses from the dependent objects.
POROs
As applications grow we usually create smaller classes that call each other. Let’s imagine class Foo that calls class Bar.
# app/services/foo.rb
class Foo
def perform
Bar.new.perform
end
end
# app/services/bar.rb
class Bar
def perform
'hello world'
end
endWe want to thoroughly test Bar class:
# spec/services/bar_spec.rb
require 'rails_helper'
RSpec.describe Bar do
it 'valid test' do
expect(Bar.new.perform).to eq 'hello world'
end
it 'invalid test' do
expect(Bar.new.perform).not_to eq 'good bye world'
end
endSeparately we want to test Foo class and make sure it can handle different responses from Bar:
# spec/services/foo_spec.rb
require 'rails_helper'
RSpec.describe Foo do
it 'integration test' do
expect(Foo.new.perform).to eq 'hello world'
end
it 'mocked test' do
bar = double('bar', perform: 'good bye world')
expect(Bar).to receive(:new).and_return(bar)
expect(Foo.new.perform).to eq 'good bye world'
end
endWhat expect(Bar).to receive(:new).and_return(bar) does it is allows Foo.new.perform to execute but instead of calling real Bar class it uses double.
Controllers and form objects
For more realistic use case let’s imagine a system where user can subscribe/unsubscribe from various newsletters. Separately user can choose global unsubscribe. Once the user unsubscribes from specific newsletter we want to keep that record so we do not accidentally re-subscribe user.
# app/models/user.rb
class User
...
field :unsubscribed, type: Boolean
has_many :user_newsletters
end
# app/models/newsletter.rb
class Newsletter
field :name
has_many :user_newsletters
end
# app/models/user_newsletter.rb
class UserNewsletter
belongs_to :user
belongs_to :newsletter
field :unsubscribed, type: Boolean
endForm objects can be a useful design pattern for handling complex user input. When user submits form via http://localhost:3000/unsubscribe/user_id we need to create/update records in UserNewsletter and update User.subscribed.
Here is the form object:
# app/forms/unsubscribe.rb
class Unsubscribe
include ActiveModel::Model
attr_accessor :user
def initialize(user:, global_unsubscribed: false)
@user = user
@global_unsubscribed = global_unsubscribed
end
def save
@user.update!(unsubscribed: @global_unsubscribed)
# update/create user_newsletter records
end
endController:
# config/routes.rb
resources :unsubscribe, only: [:show, :update]
# app/controllers/unsubscribe_controller.rb
class UnsubscribeController < ApplicationController
def show
user = User.find(params[:id])
@unsubscribe = Unsubscribe.new(user: user)
end
def update
@user = User.find(params[:id])
if Unsubscribe.new(user: @user,
global_unsubscribed: params[:global_unsubscribed]).save
render js: "$('.api_response').html('account updated')", status: 200
else
render js: "$('.api_response').html('account not updated')", status: 422
end
end
endAnd basic UI:
# app/views/unsubscribe/show.html.erb
<h2>Manage Email Subscriptions</h2>
<%= form_for(@unsubscribe, url: unsubscribe_path(@unsubscribe.user.id.to_s),
method: :put, remote: true) do |unsub| %>
# list newsletters here and global_unsubscribed here
<%= submit_tag 'Save', class: "btn btn-primary" %>
<div class="api_response"></div>
<% end %>We want to thoroughly test the form object by creating appropriate records and check that data is persisted in the DB.
# spec/forms/unsubscribe_spec.rb
require 'rails_helper'
RSpec.describe Unsubscribe do
it 'global_unsubscribed' do
user = create(:user)
unsub = Unsubscribe.new(user: user, global_unsubscribed: true)
unsub.save
expect(user.reload.unsubscribed).to eq true
end
it 'newsletters' do
...
end
endIn controller test we can mock Unsubscribe form responses and only check the HTTP status code.
# spec/controllers/unsubscribe_controller_spec.rb
require 'rails_helper'
RSpec.describe UnsubscribeController, type: :controller do
context 'update' do
context 'global_unsubscribed' do
before(:each) do
@user = User.new(email: 'foo@bar.com')
@user.save(validate: false)
end
it 'valid' do
unsub = double('unsubscribe', save: true)
expect(Unsubscribe).to receive(:new).and_return(unsub)
put :update, params: {id: @user.id.to_s, global_unsubscribed: true }
expect(response.status).to eq 200
end
it 'invalid' do
unsub = double('unsubscribe', save: false)
expect(Unsubscribe).to receive(:new).and_return(unsub)
put :update, params: {id: @user.id.to_s, global_unsubscribed: true }
expect(response.status).to eq 422
end
end
end
end