I am trying to figure out class/method/instantiation process I should use in Rspec to get a functioning view template that includes both the standard ActionView herlper methods and HAML helper methods.
I have implemented presenters in my rails app as is done in RailsCast #287
I have a presenter where I want to do some output that uses one of the HAML helpers.
Here is the base presenter:
class BasePresenter
attr_reader :object, :template
def initialize(object, template)
#object = object
#template = template
end
end
Here is an example of a theoretical presentable object and it's presenter class:
class FakeObject
def a_number
rand(10).to_s
end
end
class FakePresenter < BasePresenter
def output_stuff
# #content_tag is standard ActionView
template.content_tag(:span) do
# #succeed is from the haml gem
template.succeed('.') do
object.a_number
end
end
end
end
If I were to write a test for this in Rspec and only the #content_tag method were being used, this would work, as it's part of ActionView. However, #succeed is from the haml gem and isn't available with my current test.
Test:
require 'rails_helper'
describe FakeObjectPresenter do
let(:template){ ActionView::Base.new }
let(:obj){ FakeObject.new }
let(:presenter){ FakeObjectPresenterPresenter.new obj, template }
describe '#output_stuff' do
it{ expect(presenter.output_stuff).to match(/<span>\d+<\/span>/) }
end
end
Failures:
1) FakeObjectPresenter #output_stuff
Failure/Error:
template.succeed('.') do
object.a_number
NoMethodError:
undefined method `succeed' for #<ActionView::Base:0x00560e818125a0>
What class should I be instantiating as my template instead of ActionView::Base.new to get the haml methods available in the test?
If you'd like to easily play around with this, you can dump the below code into a spec.rb file in a rails app and run it as a demo:
require 'rails_helper'
# Define classes to be tested
class FakeObject
def a_number
rand(10).to_s
end
end
class BasePresenter
attr_reader :object, :template
def initialize(object, template)
#object = object
#template = template
end
end
class FakeObjectPresenter < BasePresenter
def output_stuff
# #content_tag is standard ActionView
template.content_tag(:span) do
# #succeed is from the haml gem
template.succeed('.') do
object.a_number
end
end
end
end
# Actual spec
describe FakeObjectPresenter do
let(:template){ ActionView::Base.new }
let(:obj){ FakeObject.new }
let(:presenter){ FakeObjectPresenter.new obj, template }
describe '#output_stuff' do
it{ expect(presenter.output_stuff).to match(/<span>\d+<\/span>/) }
end
end
Related
In my Order model I include a PORO class "ShipmentHandler". This PORO is located like this: app/models/order/shipment_handler.rb
I invoke this in my Order model like so:
def assign_shipments
ShipmentHandler.new(self).assign_shipments
end
My PORO looks like:
class Order
class ShipmentHandler
def initialize(order)
#set_some_variables
end
def some_methods
end
end
end
I'm trying to create spec to test the methods in my ShipmentHandler class. I'm not sure how to do this as I keep getting errors like uninitialized constant ShipmentHandler
I've tried to add it to my order_spec.rb like so:
describe Order do
describe Order::ShipmentHandler do
end
end
and:
describe Order do
describe ShipmentHandler do
end
end
Neither work. I've also tried creating a spec in spec/models/order/shipment_handler_spec.rb
This also failed.
The following way of writing specs worked for me when I made some assumptions on what your Order class looks like with the nested ShipmentHandler class:
class Order
def assign_shipments
ShipmentHandler.new(self).assign_shipments
end
class ShipmentHandler
def initialize(order)
#order = order
end
def some_methods
end
end
end
RSpec.describe Order do
it { is_expected.to be_a Order }
end
# Method 1
RSpec.describe Order::ShipmentHandler do
subject(:shipment_handler) { described_class.new(Order.new) }
it { is_expected.to be_a Order::ShipmentHandler }
end
# Method 2
class Order
RSpec.describe ShipmentHandler do
subject(:shipment_handler) { described_class.new(Order.new) }
it { is_expected.to be_a Order::ShipmentHandler }
end
end
Im testing a Module that can be included in a Controller.
Right now the code looks like that:
class GithubAuthorizationController < ApplicationController
include Frontend::Concerns::GithubAuthorization
def index
render inline: "lorem_ipsum"
end
end
describe GithubAuthorizationController do
before(:all) do
#page_content = "lorem_ipsum"
end
...........
As you can see I basically create a Test-Controller before the tests are run. Now I would like to add the module and index method in the before(:all)-block. I tried:
class GithubAuthorizationController < ApplicationController
end
describe GithubAuthorizationController do
before(:all) do
#page_content = "lorem_ipsum"
class < #controller
include Frontend::Concerns::GithubAuthorization
def index
render inline: "lorem_ipsum"
end
end
end
...........
As I can see in debugger in the before(:all) block the #controller is defined as <GithubAuthorizationController .... So It is a instance. There Is also no error when running the code, but the tests fails, because of The action 'index' could not be found ...
What do I wrong? How can I move the code to the before(:all) block? Thanks
The way to do this in rspec is with a controller block:
describe GithubAuthorizationController, type: :controller do
context "with module" do
controller do
include Frontend::Concerns::GithubAuthorization
def index
render inline: "lorem_ipsum"
end
end
# within this block the controller will be the anonymous controller class created above
end
end
If you have set infer_base_class_for_anonymous_controllers to false (this is not the default) then you need to do controller(GithubAuthorizationController) or you'll inherit directly from ApplicationController
Your issue could be down to a missing route - the controller helper creates some routes for you (for the default index, show etc.) actions. You can add extra ones in an example with
it "does something with a custom route" do
routes.draw { get "custom" => "github_authorization#custom" }
get :custom
...
end
I'm using ActiveJob to send mails:
Using deliver_now method:
invoices_controller.rb
def send_invoice
#other stuff
Members::InvoicesMailer.send_invoice(#invoice.id, view_context).deliver_now
end
invoices_mailer.rb
require 'open-uri'
class Members::InvoicesMailer < ApplicationMailer
def send_invoice(invoice_id, view_context)
#invoice = Invoice.find(invoice_id)
attachments["#{#invoice.identifier}.pdf"] = InvoicePdf.new(#invoice, view_context).render
mail :to => #invoice.client.email, :subject => "Invoice"
end
end
Notice here that I'm sending the view_context from the controller to the mailer, that will again pass it to the InvoicePdf class to generate the invoice.
Result: Email sent correctly
Using deliver_later method:
invoices_controller.rb
def send_invoice
#other stuff
Members::InvoicesMailer.send_invoice(#invoice.id, view_context).deliver_later
end
Result: ActiveJob::SerializationError in Members::InvoicesController#send_invoice Unsupported argument type: view_context.
How to inject the view_context inside the InvoicePdf, either loading it from inside InvoicePdf, or InvoiceMailer?
Edit: This is what the InvoicePdf looks like
invoice_pdf.rb
class InvoicePdf < Prawn::Document
def initialize(invoice, view_context)
#invoice, #view_context = invoice, view_context
generate_pdf
end
def generate_pdf
# calling some active_support helpers:
# #view_context.number_to_currency(//)
# calling some helpers I created
end
end
The problem with passing an object like the view context and then using deliver_later is that the parameters you give it are serialized to some backend (redis, MySQL), and another ruby background process picks it up later.
Objects like a view context are not really things you can serialize. It's not really data.
You can just use ActionView::Base.new, for example from rails console:
# New ActionView::Base instance
vagrant :002 > view = ActionView::Base.new
# Include some helper classes
vagrant :003 > view.class_eval { include ApplicationHelper }
vagrant :004 > view.class_eval { include Rails.application.routes.url_helpers }
# Now you can run helpers from `ApplicationHelper`
vagrant :005 > view.page_title 'Test'
"Test"
# And from url_helpers
vagrant :006 > view.link_to 'Title', [:admin, :organisations]
=> "Title"
Here's what I do in my PdfMaker class, which is probably similar to your InvoicePdf class.
def action_view
#action_view ||= begin
view = ActionView::Base.new ActionController::Base.view_paths
view.class_eval do
include Rails.application.routes.url_helpers
include ApplicationHelper
include FontAwesome::Rails::IconHelper
include Pundit
def self.helper_method *name; end
def view_context; self; end
def self.before_action f; end
def protect_against_forgery?; end
end
view
end
end
I am using Ruby on Rails 3.1.0 and the rspec-rails 2gem. I would like to refactor the following code (I have intentionally omitted some code and I have given meaningful names in order to highlight the structure):
describe "D1" do
# Helper method
def D1_Method_1
...
end
context "C1" do
# Helper methods
def D1_C1_Method_1
session.should be_nil # Note: I am using the RoR 'session' hash
D1_Method_1 # Note: I am calling the 'D1_Method_1' helper method
...
end
def D1_C1_Method_2
...
end
it "I1" do
D1_Method_1
...
end
it "I2" do
...
D1_C1_Method_1
D1_C1_Method_2
end
end
context "C2" do
# Helper methods
def D1_C2_Method_1
...
end
def D1_C2_Method_2
...
end
it "I1" do
D1_Method_1
...
end
it "I2" do
...
D1_C2_Method_1
D1_C2_Method_2
end
end
end
What can\should I make in order to refactor the above code?
P.S.: I have tried to extract helper methods in an external module (named Sample) but, for example relating to the D1_C1_Method_1 method (that contains the RoR session), I get the following error when I run the spec file:
Failure/Error: session.should be_nil
NameError:
undefined local variable or method `session' for Sample:Module
Have you tried to include the helpers as an external module?
require 'path/to/my_spec_helper'
describe "D1" do
include MySpecHelper
...
end
And now the helper:
# my_spec_helper.rb
module MySpecHelper
def D1_C1_Method_1
session.should be_nil
...
end
end
They don't seem to be accessible from ActionView::TestCase
That's right, helper methods are not exposed in the view tests - but they can be tested in your functional tests. And since they are defined in the controller, this is the right place to test them. Your helper method is probably defined as private, so you'll have to use Ruby metaprogramming to call the method.
app/controllers/posts_controller.rb:
class PostsController < ApplicationController
private
def format_something
"abc"
end
helper_method :format_something
end
test/functional/posts_controller_test.rb:
require 'test_helper'
class PostsControllerTest < ActionController::TestCase
test "the format_something helper returns 'abc'" do
assert_equal 'abc', #controller.send(:format_something)
end
end
This feels awkward, because you're getting around encapsulation by using send on a private method.
A better approach is to put the helper method in a module in the /controller/concerns directory, and create tests specifically just for this module.
e.g. in app controller/posts_controller.rb
class PostsController < ApplicationController
include Formattable
end
in app/controller/concerns/formattable.rb
module Concerns
module Formattable
extend ActiveSupport::Concern # adds the new hot concerns stuff, optional
def format_something
"abc"
end
end
end
And in the test/functional/concerns/formattable_test.rb
require 'test_helper'
# setup a fake controller to test against
class FormattableTestController
include Concerns::Formattable
end
class FormattableTest < ActiveSupport::TestCase
test "the format_something helper returns 'abc'" do
controller = FormattableTestController.new
assert_equal 'abc', controller.format_something
end
end
You could test #controller.view_context from your functional/controller tests. This method is available in Rails 3, 4, and 5, as far as I can tell.
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
helper_method :current_user
# ...
end
test/controllers/application_controller_test.rb
require 'test_helper'
class ApplicationControllerTest < ActionController::TestCase
test 'current_user helper exists in view context' do
assert_respond_to #controller.view_context, :current_user
end
end
If you didn't want to test one of your controller subclasses, you could also create a test controller to verify that the method in the view_context is the same one from the controller and not from one of your view helpers.
class ApplicationControllerHelperTest < ActionController::TestCase
class TestController < ApplicationController
private
def current_user
User.new
end
end
tests TestController
test 'current_user helper exists in view context' do
assert_respond_to #controller.view_context, :current_user
end
test 'current_user returns value from controller' do
assert_instance_of User, #controller.view_context.current_user
end
end
Or, more likely, you'd want to be able to test the helper in the presence of a request.
class ApplicationControllerHelperTest < ActionController::TestCase
class TestController < ApplicationController
def index
render plain: 'Hello, World!'
end
end
tests TestController
def with_routing
# http://api.rubyonrails.org/classes/ActionDispatch/Assertions/RoutingAssertions.html#method-i-with_routing
# http://guides.rubyonrails.org/routing.html#connecting-urls-to-code
super do |set|
set.draw do
get 'application_controller_test/test', to: 'application_controller_test/test#index'
end
yield
end
end
test 'current_user helper exists in view context' do
assert_respond_to #controller.view_context, :current_user
end
test 'current_user returns value from controller' do
with_routing do
# set up your session, perhaps
user = User.create! username: 'testuser'
session[:user_id] = user.id
get :index
assert_equal user.id, #controller.view_context.current_user.id
end
end
end
I've struggled with this for a bit, because the accepted answer didn't actually test whether the method was exposed as a helper method.
That said, we can use the #helpers method to get a proxy for testing.
For example:
class FooController < ApplicationController
private
def bar
'bar'
end
helper_method :bar
end
Can be tested with:
require 'test_helper'
class FooControllerTest < ActionController::TestCase
test 'bar is a helper method' do
assert_equal 'bar', #controller.helpers.bar
end
end
Indeed they're not. The view tests are specifically for the views. They don't load the controllers.
You should mock this method and make it return whatever is appropriate depending of your context.