Test helpers with RSpec in Padrino (Sinatra) - ruby

I'm trying to test a helper in a Padrino (Sinatra) app. My helper method is itself calling Padrino core helper methods but they are undefined. The error appears only in RSpec, while the app works fine. So the way I'm including my helper in RSpec makes it loose "Padrino scope" but I don't know how to bring Padrino helper's scope properly in my RSpec environment.
My helper:
module AdminHelper
Sort = Struct.new(:column, :order)
def sort_link(model, column)
order = sorted_by_this?(column) ? 'desc' : 'asc'
link_to mat(model, column), url(:pages, :index, sort: column, order: order)
end
def sorted_by_this?(column)
column.to_s == #sort.column && #sort.order == 'asc'
end
end
Lenstroy::Admin.helpers AdminHelper
My spec:
describe AdminHelper do
before(:all) do
class AdminHelperClass
include AdminHelper
end
end
subject(:helper) { AdminHelperClass.new }
describe '#sort_link' do
context "with :pages and :title parameters" do
before do
sort = AdminHelperClass::Sort.new('title', 'asc')
helper.instance_variable_set('#sort', sort)
end
subject { helper.sort_link(:pages, :title) }
it { should match(/<a href=([^ ]+)pages/) }
end
end
end
Results in error:
1) AdminHelper#sort_link with :pages and :title parameters
Failure/Error: subject { helper.sort_link(:pages, :title) }
NoMethodError:
undefined method `mat' for #<AdminHelperClass:0x007f1d951dc4a0>
Including a helper where mat is defined doesn't work, as one method is dependent on another helper and it goes on and on...
Update
In my spec helper I have:
def app(app = nil, &blk)
#app ||= block_given? ? app.instance_eval(&blk) : app
#app ||= Lenstroy::Admin
#app.register Padrino::Helpers
#app.register Padrino::Rendering
#app
end
in my spec I have:
it "returns link to resource with sort parameters" do
app do
get '/' do
sort_link(:pages, :title)
end
end
get "/"
last_response.body.should =~ /<a href=([^ >]+)pages/
end
And now tests fail, last_response.body is ''.

Method #mat is defined in Padrino::Admin::Helpers::ViewHelpers. You can do
class AdminHelperClass
include Padrino::Admin::Helpers::ViewHelpers
include AdminHelper
end
Update:
If your methods are really dependent on all these routes and helpers you should consider doing full mockup of your app like this:
def mock_app(base=Padrino::Application, &block)
#app = Sinatra.new(base, &block)
#app.register Padrino::Helpers
#app.register Padrino::Rendering
# register other things
end
def app
Rack::Lint.new(#app)
end
mock_app do
get '/' do
sort_link(my_model, my_column)
end
end
get "/"
assert_equal "some test text", body
Here's how it's done in padrino-admin: https://github.com/padrino/padrino-framework/blob/master/padrino-admin/test/test_admin_application.rb

I was having the same problem (and getting very frustrated tracking down the modules and including them). So far, I've got my specs working by:
1) Explicitly defining my module (as explained in how to use padrino helper methods in rspec)
module MyHelper
...
end
MyApp::App.helpers MyHelper
2) Automatically including helpers at the top of my spec. (Right now I only have one helper spec, but in the future I might try to move this into spec_helper.rb.)
describe MyHelper do
let(:helpers) { Class.new }
before { MyApp::App.included_modules.each { |m| helpers.extend m } }
subject { helpers }
it 'blah' do
expect(subject.helper_method).to eq 'foo'
end
end

Related

Capybara.page not in scope after extending capybara-screenshot's after_failed_example method

I'm trying to override the after_failed_example method so I can inflict some custom file naming on our screenshots. I'm loading the module as an initializer.
So far, so good, but the Capybara.page.current_url is blank, making me think I need to require something additional?
require "capybara-screenshot/rspec"
module Capybara
module Screenshot
module RSpec
class << self
attr_accessor :use_description_as_filename
attr_accessor :save_html_file
end
self.use_description_as_filename = true
self.save_html_file = true
def self.after_failed_example(example)
if example.example_group.include?(Capybara::DSL) # Capybara DSL method has been included for a feature we can snapshot
Capybara.using_session(Capybara::Screenshot.final_session_name) do
puts ">>>> Capybara.page.current_url: " + Capybara.page.current_url.to_s
if Capybara::Screenshot.autosave_on_failure && failed?(example) && Capybara.page.current_url != ''
saver = Capybara::Screenshot.new_saver(Capybara, Capybara.page, Capybara::Screenshot.save_html_file?, set_saver_filename_prefix(example))
saver.save
example.metadata[:screenshot] = {}
example.metadata[:screenshot][:html] = saver.html_path if saver.html_saved?
example.metadata[:screenshot][:image] = saver.screenshot_path if saver.screenshot_saved?
end
end
end
private
def self.set_saver_filename_prefix(example)
return example.description.to_s.gsub(" ", "-") if Capybara::Screenshot.use_description_as_filename?
return Capybara::Screenshot.filename_prefix_for(:rspec, example)
end
end
end
end
end
This is successfully overriding the capybara-screenshot/rspec method, and any of the Capybara::Screenshot static information is accessible, but not Capybara session related information (afa I can tell).
For example, Capybara.page.current_url.to_s is null when overridden, but present when not.
I was missing a require (kind of silly mistake):
require 'capybara/rspec'

Hanami parameters whitelisting

Following the hanami docs, in order to block a admin parameter inside an action, I can use the following configuration:
params do
required(:email).filled
required(:address).schema do
required(:country).filled
end
end
def call(params)
puts params[:email] # => "alice#example.org"
puts params[:address][:country] # => "Italy"
puts params[:admin] # => nil
end
But this does not work for nested parameters, i.e.:
params do
required(:email).filled
required(:address).schema do
required(:country).filled
end
end
def call(params)
puts params[:email] # => "alice#example.org"
puts params[:address] # => { country: "Italy", admin: true }
puts params[:address][:admin] # => true
end
I was able to solve this by using select to filter out the undesirable parameters with a private method, but this does not seems like the Hanami way. What would be the proper way to do this whitelisting of nested parameters?
I have never had this issue when using Hanami Validations. Within the app directory there should be a validations folder which should have the same directory structure as your controllers, views, templates etc. Your validation file should look something like this:
# apps/web/validations/users/create.rb
module Web
module Validations
module Users
class Create < Web::Action::Params
predicates Web::Validations::CommonPredicates
validations do
required(:email).filled
required(:address).schema do
required(:country).filled
end
end
end
end
end
end
And then your controller should set the params to be filtered through the validation:
module Web
module Controllers
module Users
class Create
include Web::Action
params Web::Validations::Users::Create
def call(params); end
end
end
end
end

How to test if some specific rack middleware is being used?

To be more particular, I'm talking about sentry-raven and sinatra here. I saw examples testing sinatra applications, or middlewares. But I didn't see ones testing if some particular middleware is present. Or should I be testing behavior, not configuration (or how should I call it)?
The important thing (I'd say) is the behaviour, but if you wish to check for middleware there are 2 ways I'd suggest after taking a delve into the Sinatra source (there are possibly much easier/better ways):
The env
In the Sinatra source there's a method that uses the env to check if a middleware is already present:
# Behaves exactly like Rack::CommonLogger with the notable exception that it does nothing,
# if another CommonLogger is already in the middleware chain.
class CommonLogger < Rack::CommonLogger
def call(env)
env['sinatra.commonlogger'] ? #app.call(env) : super
end
You could do the same thing in a route, e.g.
get "/env-keys" do
env.keys.inspect
end
It'll only show you the middleware if it's inserted something in env hash, e.g.
class MyBad
def initialize app, options={}
#app = app
#options = options
end
def call env
#app.call env.merge("mybad" => "I'm sorry!")
end
end
output:
["SERVER_SOFTWARE", "SERVER_NAME", "rack.input", "rack.version", "rack.errors", "rack.multithread", "rack.multiprocess", "rack.run_once", "REQUEST_METHOD", "REQUEST_PATH", "PATH_INFO", "REQUEST_URI", "HTTP_VERSION", "HTTP_HOST", "HTTP_CONNECTION", "HTTP_CACHE_CONTROL", "HTTP_ACCEPT", "HTTP_USER_AGENT", "HTTP_DNT", "HTTP_ACCEPT_ENCODING", "HTTP_ACCEPT_LANGUAGE", "GATEWAY_INTERFACE", "SERVER_PORT", "QUERY_STRING", "SERVER_PROTOCOL", "rack.url_scheme", "SCRIPT_NAME", "REMOTE_ADDR", "async.callback", "async.close", "rack.logger", "mybad", "rack.request.query_string", "rack.request.query_hash", "sinatra.route"]
It's near the end of that list.
The middleware method
There's also a method called middleware in Sinatra::Base:
# Middleware used in this class and all superclasses.
def middleware
if superclass.respond_to?(:middleware)
superclass.middleware + #middleware
else
#middleware
end
end
Call it in the class definition of a modular app and you can get the middlewares in an array:
require 'sinatra/base'
class AnExample < Sinatra::Base
use MyBad
warn "self.middleware = #{self.middleware}"
output:
self.middleware = [[MyBad, [], nil]]
There may be a way to get it from Sinatra::Application, but I haven't looked.
With help from ruby-raven guys, we've got this:
ENV['RACK_ENV'] = 'test'
# the app: start
require 'sinatra'
require 'sentry-raven'
Raven.configure(true) do |config|
config.dsn = '...'
end
use Raven::Rack
get '/' do
'Hello, world!'
end
# the app: end
require 'rspec'
require 'rack/test'
Raven.configure do |config|
logger = ::Logger.new(STDOUT)
logger.level = ::Logger::WARN
config.logger = logger
end
describe 'app' do
include Rack::Test::Methods
def app
#app || Sinatra::Application
end
class TestRavenError < StandardError; end
it 'sends errors to sentry' do
#app = Class.new Sinatra::Application do
get '/' do
raise TestRavenError
end
end
allow(Raven.client).to receive(:send).and_return(true)
begin
get '/'
rescue TestRavenError
end
expect(Raven.client).to have_received(:send)
end
end
Or if raven sending requests is in the way (when tests fail because of raven sending requests, and not because of the underlying error), one can disable them:
Raven.configure(true) do |config|
config.should_send = Proc.new { false }
end
And mock Raven.send_or_skip instead:
...
allow(Raven).to receive(:send_or_skip).and_return(true)
begin
get '/'
rescue TestRavenError
end
expect(Raven).to have_received(:send_or_skip)
...

Passing an object as subject to rspec

I am running rspec tests on a catalog object from within a Ruby app, using Rspec::Core::Runner::run:
File.open('/tmp/catalog', 'w') do |out|
YAML.dump(catalog, out)
end
...
unless RSpec::Core::Runner::run(spec_dirs, $stderr, out) == 0
raise Puppet::Error, "Unit tests failed:\n#{out.string}"
end
(The full code can be found at https://github.com/camptocamp/puppet-spec/blob/master/lib/puppet/indirector/catalog/rest_spec.rb)
In order to pass the object I want to test, I dump it as YAML to a file (currently /tmp/catalog) and load it as subject in my tests:
describe 'notrun' do
subject { YAML.load_file('/tmp/catalog') }
it { should contain_package('ppet') }
end
Is there a way I could pass the catalog object as subject to my tests without dumping it to a file?
I am not very clear as to what exactly you are trying to achieve but from my understanding I feel that using a before(:each) hook might be of use to you. You can define variables in this block that are available to all the stories in that scope.
Here is an example:
require "rspec/expectations"
class Thing
def widgets
#widgets ||= []
end
end
describe Thing do
before(:each) do
#thing = Thing.new
end
describe "initialized in before(:each)" do
it "has 0 widgets" do
# #thing is available here
#thing.should have(0).widgets
end
it "can get accept new widgets" do
#thing.widgets << Object.new
end
it "does not share state across examples" do
#thing.should have(0).widgets
end
end
end
You can find more details at:
https://www.relishapp.com/rspec/rspec-core/v/2-2/docs/hooks/before-and-after-hooks#define-before(:each)-block

Unable to use ActiveModel::MassAssignmentSecurity

I am trying to use some functionality in ActiveModel but I'm having trouble making everything work. I've included my class file and the test I'm running.
The test is failing with:
': undefined method `attr_accessible
I really don't know why, since MassAssignmentSecurity will bring that in and it is in fact running. I've also tried to include all of ActiveModel as well but that's doesn't work either. It doesn't seem to matter if I use include or extend to bring in the MassAssignmentSecurity.
If I pass in some attributes in my test to exercise "assign_attributes" in the initialize, that fails as well. I'm fairly new to rails, so I'm hoping I'm just missing something really simple.
TIA.
Using rails 3.2.12
my_class.rb
class MyClass
include ActiveModel::MassAssignmentSecurity
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Naming
extend ActiveSupport::Callbacks
attr_accessible :persisted, :creds
def initialize(attributes = nil, options = {})
#persisted = false
assign_attributes(attributes, options) if attributes
yield self if block_given?
end
end
my_class_spec.rb
require 'spec_helper'
describe MyClass do
before do
#testcase = MyClass.new
end
subject { #testcase }
it_should_behave_like "ActiveModel"
it { MyClass.should include(ActiveModel::MassAssignmentSecurity) }
it { should respond_to(:persisted) }
end
support/active_model.rb
shared_examples_for "ActiveModel" do
include ActiveModel::Lint::Tests
# to_s is to support ruby-1.9
ActiveModel::Lint::Tests.public_instance_methods.map{|m| m.to_s}.grep(/^test/).each do |m|
example m.gsub('_',' ') do
send m
end
end
def model
subject
end
end
Yikes! What a mess I was yesterday. Might as well answer my own question since I figured out my issues.
attr_accessible in MassAssignmentSecurity does not work like it does with ActiveRecord. It does not create getters and setters. You still have to use attr_accessor if you those created.
assign_attributes is a connivence function that someone wrote to wrap around mass_assignment_sanitizer and isn't something baked into in MassAssignment Security. An example implementation is below:
def assign_attributes(values, options = {})
sanitize_for_mass_assignment(values, options[:as]).each do |k, v|
send("#{k}=", v)
end
end

Resources