Testing a Rails FormBuilder extension - ruby-on-rails

I have created a FormBuilder extension:
form.div_field_with_label(:email)
Which outputs:
<div class="field email">
<label for="user_email">Email</label>
<input class="input" id="user_email" name="user[email]" type="email" value="">
</div>
How do i create or mock the template object in my rspec test?
app/helper/form_helper.rb
module FormHelper
class GroundedFormBuilder < ActionView::Helpers::FormBuilder
def div_field_with_label(key, &block)
if block_given?
content = self.label(key)
content << block.call(key)
end
classes = ['field']
classes << key.to_s
if #object.errors[key].size != 0
classes << 'warning'
msg = #object.errors.full_message(key, #object.errors[key].join(', '))
content << #template.content_tag(:p, msg, :class => "error-message warning alert" )
end
#template.content_tag(:div, content, :class => classes.join(' ').html_safe)
end
end
end
spec/helper/form_helper_spec.rb
require 'spec_helper'
describe FormHelper do
describe FormHelper::GroundedFormBuilder do
describe 'div_field_with_label' do
# how do i create or mock template?
# template = ?
let(:resource) { FactoryGirl.create :user }
let(:helper) { FormHelper::GroundedFormBuilder.new(:user, resource, template, {})}
let(:output) { helper.div_field_with_label :email }
it 'wraps input and label' do
expect(output).to match /<div class="field">/
end
it 'creates a label' do
expect(output).to match /<label for="user[email]">Email/
end
end
end
end

Update for rails 3 or higher.
This how it should looks like now:
require 'spec_helper'
class TestHelper < ActionView::Base; end
describe LabeledFormBuilder do
describe '#upload_tag' do
let(:helper) { TestHelper.new }
let(:resource) { FactoryGirl.build :user }
let(:builder) { LabeledFormBuilder.new :user, resource, helper, {}, nil }
let(:output) do
builder.upload_tag :avatar, title: "Test upload"
end
before { expect(helper).to receive(:render) { "render partial "} }
it { expect(output).to include "Upload file via new solution" }
end
end

I haven't tried this, but looking at how your GroundedFormBuilder uses #template, maybe all you need is an object that extends ActionView::Helpers::TagHelper:
# ...
template = Object.new.extend ActionView::Helpers::TagHelper
# ...
Any of the TagHelper methods such as content_tag should generate the same content as they would when included in a real template.

actually it was as easy as using self since FormBuilder has TagHelper somewhere in the inheritance chain.
# spec/helper/form_helper_spec.rb
require 'spec_helper'
describe FormHelper do
describe FormHelper::GroundedFormBuilder do
describe 'div_field_with_label' do
# how do i create or mock template?
# template = ?
let(:resource) { FactoryGirl.create :user }
let(:helper) { FormHelper::GroundedFormBuilder.new(:user, resource, self, {})}
let(:output) {
helper.div_field_with_label :email do
helper.email_field(:email, :class => 'input')
end
}
it 'wraps input and label' do
expect(output).to include '<div class="field email">'
end
it 'creates a label' do
expect(output).to include '<label'
end
end
end
end

Related

Rails RSpec (beginner): Why is this test sometimes passing and sometimes not?

I have a book database where books can have different book formats (hardcover, softcover etc).
I have factories with factory_bot.
The following spec just run through with an error - and then when I run it the second time, it worked. I have no idea where I need to start searching....
The error was:
1) BookFormat displays the proper book format for a book with that format
Failure/Error: expect(#book.book_format.name).to eq('Hardcover')
expected: "Hardcover"
got: "Not defined"
Here is the full spec:
require 'rails_helper'
RSpec.describe BookFormat, type: :model do
before(:all) do
#book = create(:hobbit)
#book_format_default = create(:not_defined)
end
it 'displays the proper book format for a book with that format' do
expect(#book.book_format.name).to eq('Hardcover')
end
it 'should reassign to the fallback book_format if their book_format is deleted' do
format = #book.book_format
format.destroy
expect(#book.reload.book_format.id).to eq(#book_format_default.id)
end
it 'should not let the fallback format be deleted' do
format = #book_format_default
format.destroy
expect(format).to be_truthy
end
end
Here is the corresponding factor for the book :hobbit:
factory :hobbit, class: Book do
title { 'The Hobbit' }
year { 1937 }
rating { 5 }
condition { 4 }
synopsis { "<p>#{Faker::Lorem.paragraphs(number: 30).join(' ')}</p>" }
association :book_format, factory: :hardcover
association :user, factory: :me
genres { [ create(:fiction) ] }
after(:build) do |hobbit|
hobbit.cover.attach(
# rubocop:disable Rails/FilePath
io: File.open(Rails.root.join('db', 'sample', 'images', 'cover-1.jpg')),
# rubocop:enable Rails/FilePath
filename: 'cover.jpg',
content_type: 'image/jpeg'
)
end
end
And here are the factories for book_formats:
FactoryBot.define do
factory :not_defined, class: BookFormat do
name { 'Not defined'}
fallback { true }
end
factory :hardcover, class: BookFormat do
name { 'Hardcover' }
end
factory :softcover, class: BookFormat do
name { 'Softcover' }
end
end

Pass the devise user path in a partial

I'm trying to create a sidebar menu with the below information:
# app/controllers/users_controller.rb
def show
#user = User.friendly.find(params[:id])
end
# config/routes.rb
get "/user/:id", to: "users#show"
I have the link from the dropdown menu to the current_user path:
<!-- app/views/layouts/_dropdown_menu.html.erb -->
<%= link_to "My Account", current_user %>
This is how I create the sidebar menu:
<!-- app/views/layouts/dashboard.html.erb -->
<%= render "shared/sidebar_panel" %>
<!-- app/views/shared/_sidebar_panel.html.erb -->
<%= render "shared/nav" %>
<!-- app/views/shared/_nav.html.erb -->
<% SidebarMenu.all.each do |entry| %>
<% if entry[:group_title].present? %>
<li class="nav-title"><%= entry[:group_title]%></li>
<% end %>
<%= render partial: 'shared/nav_submenu',
collection: entry[:children],
as: :sub_menu,
locals: {parents: []} %>
<% end %>
<!-- app/views/shared/_nav_submenu.html.erb -->
<li>
<a href="<%= sub_menu[:href] %>">
<span><%= sub_menu[:title] %></span>
<% if sub_menu[:subtitle].present? %>
<span class="<%= sub_menu[:subtitle_class] %>">
<%= sub_menu[:subtitle] %>
</span>
<% end %>
</a>
<% if sub_menu[:children].present? %>
<ul>
<%= render partial: 'shared/nav_submenu',
collection: sub_menu[:children],
as: :sub_menu,
locals: {parents: parents + [sub_menu]} %>
</ul>
<% end %>
</li>
I have a SidebarMenu model and I've added current_user path:
# app/models/sidebar_menu.rb
class SidebarMenu
class << self
include Rails.application.routes.url_helpers
end
def self.all
[
{
group_title: "Account & Contact",
children: [
{
href: "#",
title: "Account",
icon: "...",
children: [
{
href: current_user,
title: "My Account"
}
]
},
# ...
]
}
]
end
end
But it raises an error message:
undefined local variable or method `current_user' for SidebarMenu:Class
Can anyone advise me how I can fix this?
This is how current_user method is set up by Devise: current_user is a controller method included by devise and loaded in ActionController::Base class. It is also available in the views because it is a helper method which gets included in ActionView::Base. current_user is not a url helper and it returns a User model instance which link_to can turn into a url automatically.
current_user is outside of the scope of SidebarMenu class. To fix it, just pass it as an argument:
class SidebarMenu
def self.all user
[{ href: user, title: "My Account" }]
end
end
# in the view or controller
SidebarMenu.all(current_user) # => [{ href: #<User:0x0000563cc7c69198>, title: "My Account" }]
I think, a better approach is to use menu class as an object. It is more flexible and easier to use:
class Menu
include Rails.application.routes.url_helpers
def initialize(user:)
#user = user
end
def user_menu
{
group_title: "Account",
# NOTE: use url helper to return a path instead of a `User` model
children: [{ href: user_path(user), title: "My Account" }]
}
end
def sidebar_menu
{
group_title: "Main",
children: [{ href: root_path, title: "Home" }]
}
end
def all
[sidebar_menu, user_menu]
end
private
attr_reader :user
end
Use it in the view:
Menu.new(user: current_user).user_menu[:children]
# => [{ href: "/users/1", title: "My Account" }]
You can also set up a helper method:
# in the ApplicationController
private
def menu
#menu ||= Menu.new(user: current_user)
end
helper_method :menu
# in the view
menu.user_menu[:children] # => [{ href: "/users/1", title: "My Account" }]
# TODO:
# menu.sidebar_menu
# menu.all.each do |group|
# group[:group_title]
# ...
# end
https://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to
https://api.rubyonrails.org/classes/AbstractController/Helpers/ClassMethods.html#method-i-helper_method
List url helpers: Rails.application.routes.named_routes.helper_names
List controller helpers: ApplicationController._helper_methods

Rspec for testing the rendered template outside of controller/view

I have a situation to render a HTML template outside of controller(A class under service/lib directory) and I am rendering the template using the below code.
class SomeClass
def some_method
#template = ApplicationController.render(
template: 'template',
layout: mailer_template,
)
end
end
Is there any ways to test if the rendered template is the expected one and whether render happened during that method call?
EDIT
class BatchSendingService < AbstractController::Base
require 'abstract_controller'
include AbstractController::Rendering
include AbstractController::AssetPaths
include AbstractController::Helpers
include Rails.application.routes.url_helpers
include ActionView::Rendering
include ActionView::ViewPaths
include ActionView::Layouts
self.view_paths = "app/views"
def send_batch_email(mail, domain)
#project = mail.project
#client = Mailgun::Client.new ENV['MAILGUN_API_KEY']
batch_message = Mailgun::BatchMessage.new(#client, domain)
batch_message.from(from_data)
mailer_layout = get_mailer_layout(mail.layout)
mail_html = render(
template: 'send_batch_email',
layout: mailer_layout
)
batch_message.body_html(mail_html.to_s)
batch_message.add_recipient(:to, recipient_email, {})
response = batch_message.finalize
end
EDIT
obj= BatchSendingService.new
allow(obj).to receive(:render)
BatchSendingService.send_batch_email(mail, domain)
expect(obj) .to have_received(:render)
.with({ template: "template", layout: "layout" })
By using the class where the instance method is called, the error is gone.
ActionController.render is a well tested method. The Rails Core Team saw to that. There's no need to test that it does what it says it does.
Rather, what you want to do is to make sure you called ActionController.render with the right parameters, using mock objects, like this:
describe SomeClass do
subject(:some_class) { described_class.new }
describe '#some_method' do
let(:template) { 'template' }
let(:layout) { 'mailer_template' }
before do
allow(ActionController).to receive(:render)
some_class.some_method
end
it 'renders the correct template' do
expect(ActionController)
.to have_received(:render)
.with({ template: template, layout: layout })
end
end
end
EDIT
Given the edited post, here's how I would approach the test. Note that not all of the code in your send_batch_email method is visible in your edit. So, YMMV:
describe BatchSendingService do
subject(:batch_sending_service) { described_class.new }
describe '#send_batch_email' do
subject(:send_batch_email) do
batch_sending_service.send_batch_email(email, domain)
end
let(:email) { 'email' }
let(:domain) { 'domain' }
let(:batch_message) do
instance_double(
Mailgun::BatchMessage,
from: true,
body_html: true,
add_recipient, true,
finalize: true
)
end
let(:template) { 'send_batch_template' }
let(:layout) { 'layout' }
before do
allow(Mailgun::Client).to receive(:new)
allow(Mailgun::BatchMessage)
.to receive(:new)
.and_return(batch_message)
allow(batch_sending_service)
.to receive(:render)
send_batch_email
end
it 'renders the correct template' do
expect(batch_sending_service)
.to have_received(:render)
.with(template, layout)
end
end
end

Rails and Rspec Unit Testing Static Methods

I have a very simple static method in one of my models:
def self.default
self.find(1)
end
I'm trying to write a simple Rspec unit test for it that doesn't make any calls to the DB. How do I write a test that generates a few sample instances for the test to return? Feel free to complete this:
describe ".default" do
context "when testing the default static method" do
it "should return the instance where id = 1" do
end
end
end
The model file is as follows:
class Station < ApplicationRecord
acts_as_paranoid
acts_as_list
nilify_blanks
belongs_to :color
has_many :jobs
has_many :station_stops
has_many :logs, -> { where(applicable_class: :Station) }, foreign_key: :applicable_id
has_many :chattels, -> { where(applicable_class: :Station) }, foreign_key: :applicable_id
delegate :name, :hex, to: :color, prefix: true
def name
"#{full_display} Station"
end
def small_display
display_short || code.try(:titleize)
end
def full_display
display_long || small_display
end
def average_time
Time.at(station_stops.closed.average(:time_lapsed)).utc.strftime("%-M:%S")
end
def self.default
# referencing migrate/create_stations.rb default for jobs
self.find(1)
end
def self.first
self.where(code: Constant.get('station_code_to_enter_lab')).first
end
end
The spec file is as follows:
require "rails_helper"
describe Station do
subject { described_class.new }
describe "#name" do
context "when testing the name method" do
it "should return the capitalized code with spaces followed by 'Station'" do
newStation = Station.new(code: 'back_to_school')
result = newStation.name
expect(result).to eq 'Back To School Station'
end
end
end
describe "#small_display" do
context "when testing the small_display method" do
it "should return the capitalized code with spaces" do
newStation = Station.new(code: 'back_to_school')
result = newStation.small_display
expect(result).to eq 'Back To School'
end
end
end
describe "#full_display" do
context "when testing the full_display method" do
it "should return the capitalized code with spaces" do
newStation = Station.new(code: 'back_to_school')
result = newStation.full_display
expect(result).to eq 'Back To School'
end
end
end
describe ".default" do
context "" do
it "" do
end
end
end
end
You can use stubbing to get you there
describe ".default" do
context "when testing the default static method" do
let(:dummy_station) { Station.new(id: 1) }
before { allow(Station).to receive(:default).and_return(dummy_station)
it "should return the instance where id = 1" do
expect(Station.default.id).to eq 1
end
end
end

rspec 3.4 test controller concern with response.body.read

I have the following controller concern that is used for authentication:
module ValidateEventRequest
extend ActiveSupport::Concern
def event_request_verified?(request)
sha256 = OpenSSL::Digest::SHA256.new
secret = app_client_id
body = request.body.read
signature = OpenSSL::HMAC.hexdigest(sha256, secret, body)
([signature] & [request.headers['X-Webhook-Signature'], request.headers['X-Api-Signature']]).present?
end
private
def app_client_id
ENV['APP_CLIENT_ID']
end
end
So far I have the following Rspec Test setup to hit this:
RSpec.describe ValidateEventRequest, type: :concern do
let!(:current_secret) { SecureRandom.hex }
describe '#event_request_verified?' do
it 'validates X-Webhook-Signature' do
# TBD
end
it 'validates X-Api-Signature' do
# TBD
end
end
end
I started out with stubbing the request, then mocking and stubbing, and now I am down to scrapping what I have and seeking assistance. 100% coverage is important to me and I am looking for some pointers on how to structure tests that cover this 100%.
object_double is handy for testing concerns:
require 'rails_helper'
describe MyClass do
subject { object_double(Class.new).tap {|c| c.extend MyClass} }
it "extends the subject" do
expect(subject.respond_to?(:some_method_in_my_class)).to be true
# ...
Then you can test subject like any other class. Of course you need to pass in the appropriate arguments when testing methods, which may mean creating additional mocks -- in your case a request object.
Here is how I solved this issue, and I am open to ideas:
RSpec.describe ValidateApiRequest, type: :concern do
let!(:auth_secret) { ENV['APP_CLIENT_ID'] }
let!(:auth_sha256) { OpenSSL::Digest::SHA256.new }
let!(:auth_body) { 'TESTME' }
let(:object) { FakeController.new }
before(:each) do
allow(described_class).to receive(:secret).and_return(auth_secret)
class FakeController < ApplicationController
include ValidateApiRequest
end
end
after(:each) do
Object.send :remove_const, :FakeController
end
describe '#event_request_verified?' do
context 'X-Api-Signature' do
it 'pass' do
request = OpenStruct.new(headers: { 'X-Api-Signature' => OpenSSL::HMAC.hexdigest(auth_sha256, auth_secret, auth_body) }, raw_post: auth_body)
expect(object.event_request_verified?(request)).to be_truthy
end
it 'fail' do
request = OpenStruct.new(headers: { 'X-Api-Signature' => OpenSSL::HMAC.hexdigest(auth_sha256, 'not-the-same', auth_body) }, raw_post: auth_body)
expect(object.event_request_verified?(request)).to be_falsey
end
end
context 'X-Webhook-Signature' do
it 'pass' do
request = OpenStruct.new(headers: { 'X-Webhook-Signature' => OpenSSL::HMAC.hexdigest(auth_sha256, auth_secret, auth_body) }, raw_post: auth_body)
expect(object.event_request_verified?(request)).to be_truthy
end
it 'fail' do
request = OpenStruct.new(headers: { 'X-Webhook-Signature' => OpenSSL::HMAC.hexdigest(auth_sha256, 'not-the-same', auth_body) }, raw_post: auth_body)
expect(object.event_request_verified?(request)).to be_falsey
end
end
end
end

Resources