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
Related
I am using Minitest and have a controller like this:
def update
authorize #flag
respond_to do |format|
if #flag.update(flag_params)
format.html { redirect_to flags_path , success: "#{#flag.name} was successfully updated." }
format.js
else
format.html { render :edit }
# format.js
end
end
end
I have a JS remote select that triggers a form submit and then returns a DataTable update. Works fine in development etc. I commented out the format for the else to force a no format error if the update fails.
Here is my test:
test "should assign flag" do
assert_changes 'flags(:one).assigned_id' do
patch flag_url flags(:one), format: :js, params: { flag: { assigned_id: users(:operator).id } }
end
end
The asssigned_id is nil for flags(:one) and all the patch does it update that column to the id from a user fixture. Yet my test fails:
FAIL FlagsControllerTest#test_should_assign_flag (44.56s)
"flags(:one).assigned_id" didn't change
test/controllers/flags_controller_test.rb:38:in `block in <class:FlagsControllerTest>'
Yet this passes:
patch flag_url #flag, format: :js, params: { flag: { assigned_id: users(:operator).id } }
assert_response :success
Also this fails:
assert flags(:one).assigned_id_changed?
I have double checked my tests / code and don't think I am missing anything here.
Have I missed something obvious or perhaps I am not applying the assertions correctly?
UPDATED INFO FOR ANSWER
Here is my updated model test:
require 'test_helper'
class FlagTest < ActiveSupport::TestCase
fixtures :flags
let(:flag_one) { flags(:one) }
end
and controller test:
require 'test_helper'
class FlagsControllerTest < ActionDispatch::IntegrationTest
test "should assign flag" do
assert_changes 'flag_one.assigned_id' do
patch flag_url flag_one, format: :js, params: { flag: { assigned_id: users(:operator).id } }
end
end
Think the issue is that when you call the fixture directly in code like so...
assert_changes 'flags(:one).assigned_id' do
patch flag_url flags(:one), format: :js, params: { flag: { assigned_id: users(:operator).id } }
end
..the test reloads the instance from the fixture and of course it is unchanged. I've never written a test with a fixture reference inline. What I usually do is set up a let using the value from the fixture...
class FlagTest < ActiveSupport::TestCase
fixtures :flags
let(:a_flag){flags(:one)}
and then use that in my tests.
test "should assign flag" do
assert_changes 'a_flag.assigned_id' do
patch flag_url a_flag, format: :js, params: { flag: { assigned_id: users(:operator).id } }
end
end
Saves some typing if you have a number of tests, and should fix your issue.
I have a class, that in one situation should call :my_method, but in another situation must not call method :my_method. I would like to test both cases. Also, I would like the test to document the cases when :my_method should not be called.
Using any_instance is generally discouraged, so I would be happy to learn a nice way to replace it.
This code snippet is a reduced example on what I kind of test I would like to write.
class TestSubject
def call
call_me
end
def call_me; end
def never_mind; end
end
require 'rspec'
spec = RSpec.describe 'TestSubject' do
describe '#call' do
it 'calls #call_me' do
expect_any_instance_of(TestSubject).to receive(:call_me)
TestSubject.new.call
end
it 'does not call #never_mind' do
expect_any_instance_of(TestSubject).not_to receive(:never_mind)
TestSubject.new.call
end
end
end
spec.run # => true
It works, but uses expect_any_instance_of method, which is not recommended.
How to replace it?
I'll do somehting like that
describe TestSubject do
describe '#call' do
it 'does not call #something' do
subject = TestSubject.new
allow(subject).to receive(:something)
subject.call
expect(subject).not_to have_received(:something)
end
end
end
Hope this helped !
This is how I normally unit-test. I updated the code to support other possible questions you (or other readers) may have in the future.
class TestSubject
def call
some_call_me_value = call_me
call_you(some_call_me_value)
end
def call_me; end
def call_you(x); end
def never_mind; end
class << self
def some_class_method_a; end
def some_class_method_b(x, y); end
end
end
require 'rspec'
spec = RSpec.describe TestSubject do
context 'instance methods' do
let(:test_subject) { TestSubject.new }
describe '#call' do
let(:args) { nil }
let(:mocked_call_me_return_value) { 'somecallmevalue' }
subject { test_subject.call(*args) }
before do
allow(test_subject).to receive(:call_me) do
mocked_call_me_return_value
end
end
it 'calls #call_me' do
expect(test_subject).to receive(:call_me).once
subject
end
it 'calls #call_you with call_me value as the argument' do
expect(test_subject).to receive(:call_you).once.with(mocked_call_me_return_value)
subject
end
it 'does not call #never_mind' do
expect(test_subject).to_not receive(:never_mind)
subject
end
it 'calls in order' do
expect(test_subject).to receive(:call_me).once.ordered
expect(test_subject).to receive(:call_you).once.ordered
subject
end
end
describe '#call_me' do
let(:args) { nil }
subject { test_subject.call_me(*args) }
# it ...
end
describe '#call_you' do
let(:args) { nil }
subject { test_subject.call_you(*args) }
shared_examples_for 'shared #call_you behaviours' do
it 'calls your phone number'
it 'creates a Conversation record'
end
# just an example of argument-dependent behaviour spec
context 'when argument is true' do
let(:args) { [true] }
it 'does something magical'
it_behaves_like 'shared #call_you behaviours'
end
# just an example of argument-dependent behaviour spec
context 'when argument is false' do
let(:args) { [false] }
it 'does something explosive'
it_behaves_like 'shared #call_you behaviours'
end
end
end
context 'class methods' do
let(:args) { nil }
describe '#some_class_method_a' do
let(:args) { nil }
subject { TestSubject.some_class_method_a(*args) }
# it ...
end
describe '#some_class_method_b' do
let(:args) { [1, 2] }
subject { TestSubject.some_class_method_b(*args) }
# it ...
end
end
end
spec.run # => true
Do not test if some method was called or wasn't.
This will tight your tests to the implementation details and will force you to change tests every time you refactor(change implementation details without changing the behaviour) your class under test.
Instead test against return value or changed application state.
It is difficult come up with the example, you didn't provide enough context about the class under the test.
class CreateEntity
def initialize(name)
#name = name
end
def call
if company_name?(#name)
create_company
else
create_person
end
end
def create_person
Person.create!(:name => #name)
end
def create_company
Company.create!(:name => #name)
end
end
# tests
RSpec.describe CreateEntity do
let(:create) { CreateEntity.new(name).call }
describe '#call' do
context 'when person name is given' do
let(:name) { 'Firstname Lastname' }
it 'creates a person' do
expect { create }.to change { Person.count }.by(1)
end
it 'do not create a company' do
expect { create }.not_to change { Company.count }
end
end
context 'when company name is given' do
let(:name) { 'Name & Sons Ltd' }
it 'creates a company' do
expect { create }.to change { Company.count }.by(1)
end
it 'do not create a person' do
expect { create }.not_to change { Person.count }
end
end
end
end
With tests above I would be able to change how CreateEntity.call method implemented without changing tests as far as behaviour remain same.
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
I'm trying to test the following code:
module ApplicationHelper
def current_book
Book.find(params[:id])
end
end
using the following test with RSpec:
RSpec.describe ApplicationHelper, :type => :helper do
describe "#current_book" do
book_1 = create(:book)
params = {}
params[:id] = book_1.id
expect(helper.current_book).to eq(book_1)
end
end
But for some reason the params[:id] parameter isn't being passed in properly. Any suggestions with this?
You need to stub the params:
RSpec.describe ApplicationHelper, type: :helper do
describe "#current_book" do
let(:first_book) { create(:book) }
before(:all) { helper.stub!(:params).and_return(id: 1) }
it "returns a book with a matching id" do
expect(helper.current_book).to eq(first_book)
end
end
end
Here another way of stubbing params. I think this requires rspec 3 can't remember for sure.
context 'path is a route method' do
before { allow(helper).to receive(:params).and_return(order_by: { updated_at: :desc }) }
subject { helper.sortable_link_to('Created At', order_by: :created_at) }
it { is_expected.to match /comments/ }
it { is_expected.to match /\?order_by/}
it { is_expected.to match /\?order_by%5Bupdated_at%5D=asc/}
end
I have a controller as following:
class ReportsController < ApplicationController
respond_to :html
def show
client = ReportServices::Client.new(ServiceConfig['reports_service_uri'])
#report = client.population_management(params[:id])
if #report
#kpis = #report[:key_performance_indicators]
#populations = #report[:population_scores]
#population_summaries = #report[:population_summaries]
#providers = #report[:provider_networks]
end
respond_with (#report)
end
end
Which I want to write a RSpec test for it but have no clue where to start from, I guess because it has that URL in it, it makes it harder for me, I am pretty new to Rails and RSpec and have some basic knowledge of writing RSpec for my Models but this one has puzzled me for the whole weekend.
So the first thing to tackle is mocking the external API request. The general idea here is that your going to return a mock object from new that will respond to population_management and return what you expect for #report.
describe ReportsController do
before do
#report_data = {
:key_performance_indicators => 'value',
:population_scores => 'value',
:population_summaries => 'value',
:provider_networks => 'value'
}
# This will fake the call to new, return a mock object that when
# population_management is called will return the hash above.
#fake_client = double(:population_management => #report_data)
ReportServices::Client.stub(:new => #fake_client)
end
describe '#show' do
it 'assigns #report' do
get :show, id: 1
assigns(:report).should == #report
end
it 'assigns some shortcut vars' do
[:kpis, :populations, :population_summaries, :providers].each do |var|
assigns(var).should_not be_nil
end
end
# and whatever else you'd like
end
end
You can stub the client interface to write an isolated test of your controller.
describe RerpotsController do
it "assigns a new report as #report" do
expected_id = '1234'
expected_kpi = 'kpi'
report = { key_performance_indicators: expected_kpi, ... }
client = double(ReportServices::Client)
client.should_receive(:population_management).with(expected_id) { report }
ReportServices::Client.should_receive(:new) { client }
get :show, id: expected_id
assigns(:kpis).should eq(expected_kpi)
# ...
end
end
You probably don't need to unpack the report in the controller.