If I was using RSpec I could test if a method is being called like so:
expect(obj).to receive(:method)
What is the equivalent in MiniTest? I have a model, Post, which has a before_validation callback which runs a method create_slug. In my test test/models/post_test.rb I want to ensure that the create_slug method is being called when calling post.save.
The Minitest::Spec documentation says that I can use a method must_send to check if a method is called. However, when I try #post.must_send :create_slug I receive the following error:
NoMethodError: undefined method `must_send' for #<Post:0x007fe73c39c648>
I am including Minitest::Spec in my test_helper.rb file:
ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
require 'minitest/spec'
class ActiveSupport::TestCase
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
#
# Note: You'll currently still have to declare fixtures explicitly in integration tests
# -- they do not yet inherit this setting
fixtures :all
# Add more helper methods to be used by all tests here...
end
An excerpt of my test:
describe Post do
before do
#post = FactoryGirl.build(:post)
end
describe "when saving" do
it "calls the create_slug method before validation" do
#post.must_send :create_slug
#post.save
end
end
end
What you are asking for here is a partial mock. This means you have a real object and you want to mock out one method and verify it was called. Minitest::Mock does not support partial mocks out of the box, but I'll try to show you how you can accomplish this anyway. Do a search or two on "partial mock" and see what folks have to say about it.
The easiest way to support partial mocks is to use a different mocking library like Mocha. Just add gem "mocha" to your Gemfile and you should be good to go.
describe Post do
before do
#post = FactoryGirl.build(:post)
end
describe "when saving" do
it "calls the create_slug method before validation" do
#post.expects(:create_slug).returns(true)
#post.save
end
end
end
But if you really want to use Minitest::Mock there is a way to make it work. It requires a new mock object and using ruby to redefine the create_slug method. Oh, and global variables.
describe Post do
before do
#post = FactoryGirl.build(:post)
end
describe "when saving" do
it "calls the create_slug method before validation" do
$create_slug_mock = Minitest::Mock.new
$create_slug_mock.expect :create_slug, true
def #post.create_slug
$create_slug_mock.create_slug
end
#post.save
$create_slug_mock.verify
end
end
end
Wow. That's ugly. Looks like an oversight, right? Why would Minitest make partial mocks so difficult and ugly? This is actually a symptom of a different problem. The question isn't "How do I use partial mocks?", the question is "How do I test the expected behavior of my code?" These tests are checking the implementation. What if you renamed the create_slug method? Or what if you changed the mechanism that created a slug from a callback to something else? That would require that this test change as well.
Instead, what if instead your tests only checked the expected behavior? Then you could refactor your code and change your implementation all without breaking the test. What would that look like?
describe Post do
before do
#post = FactoryGirl.build(:post)
end
describe "when saving" do
it "creates a slug" do
#post.slug.must_be :nil?
#post.save
#post.slug.wont_be :nil?
end
end
end
Now we are free to change the implementation without changing the tests. The tests cover the expected behavior, so we can refactor and clean the code without breaking said behavior. Folks ask why Minitest doesn't support partial mocks. This is why. You very rarely need them.
For this purpose Minitest has a .expect :call which allows you to check if method is getting called:
describe Post do
before do
#post = FactoryGirl.build(:post)
end
describe "when saving" do
it "calls the create_slug method before validation" do
mock_method = MiniTest::Mock.new
mock_method.expect :call, "return_value", []
#post.stub :create_slug, mock_method do
#post.save
end
mock_method.verify
end
end
end
If #post.create_slug was called, test will pass. Otherwise test will raise a MockExpectationError.
Unfortunately this feature is not documented very well. I found this answer from here: https://github.com/seattlerb/minitest/issues/216
Related
Rails 7 application using devise for authentication.
fixture (model tests pass for all classes):
one:
email: 'me#mail.co'
encrypted_password: <%= User.new.send(:password_digest, '12345678')
test_helper which attempts to re-factor the login and action process into short one-liners (test_access) in the actual controller tests
require 'simplecov'
SimpleCov.start 'rails'
ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rails/test_help"
require 'webmock/minitest'
require 'barby/outputter/png_outputter'
require 'barby/barcode/ean_13'
class ActiveSupport::TestCase
fixtures :all
include Devise::Test::IntegrationHelpers
include Warden::Test::Helpers
parallelize(workers: :number_of_processors)
def log_in(user)
if integration_test?
login_as(user, :scope => :user)
else
sign_in(user)
end
end
class Minitest::Test
def setup
#one = users(:one)
end
end
private
def test_access(user, action)
get '/users/sign_in'
puts user.inspect
sign_in user
# puts user_signed_in?
# puts current_user.manager?
puts user.manager?
post user_session_url
follow_redirect!
puts action
get action
assert_response :success
end
and the controller_test (presently on an intermediate refactoring level, as the method should extend to an array of users)
require "test_helper"
class LokationsControllerTest < ActionDispatch::IntegrationTest
include Devise::Test::IntegrationHelpers
setup do
#users_allowed = [ #one ]
#actions = [dummy_lokations_url]
end
test "should get dummy" do
#actions.each do |action|
test_access(#one, action)
assert_response :success
end
end
the problem is there seems to be no way of verifying whether the user is signed in or not, notwithstanding the inclusion of devise integration helpers. The output in the console points to proper values, but the result is the opposite of the method that should be applied
#<User id: 760812109, email: [...]>
true
http://www.example.com/lokations/dummy
[...]
Expected response to be a <2XX: success>, but was a <302: Found> redirect to <http://www.example.com/>
Response body: <html><body>You are being redirected.</body></html>
because the before_action method applied on the action specifies
def index_manager
if user_signed_in? && current_user.manager?
else
redirect_to root_path and return
end
end
Where is the above mistaken ? (I have a nagging doubt that something may be missing as this pattern has been successfully followed in the past)
Sorry but this is just not a good/acceptable refactor.
Do not set the password digest - ever. That should only be known by the underlying system that encrypts the password. Your code and tests should not even know that it exists. Your code should only ever set the password attribute with a cleartext.
These are integration and not controller tests. Controller tests are subclasses of ActionController::TestCase used in legacy applications. They should not be used. While this might seem like nickpicking you're only confusing yourself and others by conflating them.
Avoid monkeypatching a bunch of junk into ActiveSupport::TestCase or even worse Minitest::Test. If you MUST monkeypatch then write your methods in a module and include it so that the stack trace points there. But do it at the correct level in the class heirarchy. For example separate between the helpers for integration and system tests and include them into ActionDispatch::IntegrationTest and ActionDispatch::SystemTest instead of polluting all your tests.
If you need to add a lot of additional behavior don't reopen the class. Instead create your own test class which inherits from ActionDispatch::IntegrationTest and have your tests subclass it.
If you have code that you want to reuse in multiple tests like the test setup use modules that can be included where they are actually needed.
test_access doesn't belong in the shared subclass way up the tree. This belongs in an actual integration test which covers your authentication system flows or that a specific resource is authorized correctly.
Devise is an authentication system (who is the user?) - but what you're doing here is authorization (who can do what?). Devise doesn't provide any kind of authorization besides preventing access for users that are not authenticated. Neither should it (see SRP).
The index_manager method isn't a good way to implement authorization. If you want to reinvent the wheel make sure you raise an exception and use rescue_from so that the callback chain is halted and so that you're not repeating yourself. You may want to look into existing systems like CanCanCan and Pundit.
What you actually want here is something more like:
class AuthorizationError < StandardError
end
class ApplicationController
before_action :authenticate_user! # use an opt-out secure by default setup.
rescue_from AuthorizationError, with: :deny_access
private
def authorize_manager!
unless current_user.manager?
raise AuthorizationError.new("You need to be a manager to access this resource")
end
end
def deny_access(exception)
redirect_to root_path, error: exception.message
end
end
module AuthorizationIntegrationHelpers
def assert_denies_access
follow_redirect!
assert_current_path root_path
# ...
end
end
class FoosFlowTest < ActionDispatch::IntegrationTest
include AuthorizationIntegrationHelpers
include Warden::Test::Helpers
test "should not be accessable to non-managers" do
login_as(users(:one))
get '/foos'
assert_denies_access
end
end
I have a method in my ApplicationHelper that checks to see if there are any items in my basket
module ApplicationHelper
def has_basket_items?
basket = Basket.find(session[:basket_id])
basket ? !basket.basket_items.empty? : false
end
end
Here is my helper spec that I have to test this:
require 'spec_helper'
describe ApplicationHelper do
describe 'has_basket_items?' do
describe 'with no basket' do
it "should return false" do
helper.has_basket_items?.should be_false
end
end
end
end
however when I run the test i get
SystemStackError: stack level too deep
/home/user/.rvm/gems/ruby-1.9.3-p194/gems/actionpack-3.2.8/lib/action_dispatch/testing/test_process.rb:13:
From debugging this i see that session is being accessed in ActionDispatch::TestProcess from #request.session, and #request is nil. When i access the session from my request specs #request is an instance of ActionController::TestRequest.
My question is can I access the session object from a helper spec? If I can, how? And if I cant what is the best practice to test this method?
****UPDATE****
This was down to having include ActionDispatch::TestProcess in my factories. Removing this include sorts the problem.
can I access the session object from a helper spec?
Yes.
module ApplicationHelper
def has_basket_items?
raise session.inspect
basket = Basket.find(session[:basket_id])
basket ? !basket.basket_items.empty? : false
end
end
$ rspec spec/helpers/application_helper.rb
Failure/Error: helper.has_basket_items?.should be_false
RuntimeError:
{}
The session object is there and returns an empty hash.
Try reviewing the backtrace in more detail to find the error. stack level too deep usually indicates recursion gone awry.
You are testing has_basket_items? action in ApplicationHelper, which check a specfic basket with a basket_id in the baskets table, so you should have some basket objects in your test which you can create using Factory_Girl gem.
Hers's an example :-
basket1 = Factory(:basket, :name => 'basket_1')
basket2 = Factory(:basket, :name => 'basket_2')
You can get more details on How to use factory_girl from this screen cast http://railscasts.com/episodes/158-factories-not-fixtures
It will create a Factory object in your test database. So, basically you can create some factory objects and then set a basket_id in session to check for its existence like this :
session[:basket_id] = basket1.id
So, your test should be like this :-
require 'spec_helper'
describe ApplicationHelper do
describe 'has_basket_items?' do
describe 'with no basket' do
it "should return false" do
basket1 = Factory(:basket, :name => 'basket_1')
basket2 = Factory(:basket, :name => 'basket_2')
session[:basket_id] = 1234 # a random basket_id
helper.has_basket_items?.should be_false
end
end
end
end
Alternatively, you can check for a basket_id which is being created by factory_girl to be_true by using :
session[:basket_id] = basket1.id
helper.has_basket_items?.should be_true
I'm an RSpec newb, but am really loving how easy it is to write the tests and I'm continually refactoring them to be cleaner as I learn new features of RSpec. So, originally, I had the following:
describe Account do
context "when new" do
let(:account) { Account.new }
subject { account }
it "should have account attributes" do
subject.account_attributes.should_not be_nil
end
end
end
I then learned about the its method, so I tried to rewrite it as such:
describe Account do
context "when new" do
let(:account) { Account.new }
subject { account }
its(:account_attributes, "should not be nil") do
should_not be_nil
end
end
end
This fails due to its not accepting 2 arguments, but removing the message works just fine. The issue is that if the test fails, the message under the Failed examples section just says
rspec ./spec/models/account_spec.rb:23 # Account when new account_attributes
which isn't overly helpful.
So, is there a way to pass a message to its, or better yet, have it output a sane message automatically?
You could define an RSpec custom matcher:
RSpec::Matchers.define :have_account_attributes do
match do |actual|
actual.account_attributes.should_not be_nil
end
failure_message_for_should do
"expected account_attributes to be present, got nil"
end
end
describe Account do
it { should have_account_attributes }
end
You can also write: its(:account_attributes) { should_not be_nil }
See https://www.relishapp.com/rspec/rspec-core/v/2-14/docs/subject/attribute-of-subject
Take note that "its" will be extracted from rspec-core to a gem with the release of rspec 3, though.
Looks like a relatively simple monkey-patch will enable what you seek.
Look at the source of the rspec-core gem version you're using. I'm on 2.10.1. In the file lib/rspec/core/subject.rb I see the its method defined.
Here's my patched version - I changed the def line and the line after that.
Caution - this is very likely to be version specific! Copy the method from your version and modify it just like I did. Note that if the rspec-core developers do a major restructuring of the code, the patch may need to be very different.
module RSpec
module Core
module Subject
module ExampleGroupMethods
# accept an optional description to append
def its(attribute, desc=nil, &block)
describe(desc ? attribute.inspect + " #{desc}" : attribute) do
example do
self.class.class_eval do
define_method(:subject) do
if defined?(#_subject)
#_subject
else
#_subject = Array === attribute ? super()[*attribute] : _nested_attribute(super(), attribute)
end
end
end
instance_eval(&block)
end
end
end
end
end
end
end
That patch can probably be put in your spec_helper.rb.
Now the usage:
its("foo", "is not nil") do
should_not be_nil
end
Output on failure:
rspec ./attrib_example_spec.rb:10 # attr example "foo" is not nil
If you omit the second arg, the behavior will be just like the unpatched method.
I am trying to stub a method on a helper that is defined in my controller. For example:
class ApplicationController < ActionController::Base
def current_user
#current_user ||= authenticated_user_method
end
helper_method :current_user
end
module SomeHelper
def do_something
current_user.call_a_method
end
end
In my Rspec:
describe SomeHelper
it "why cant i stub a helper method?!" do
helper.stub!(:current_user).and_return(#user)
helper.respond_to?(:current_user).should be_true # Fails
helper.do_something # Fails 'no method current_user'
end
end
In spec/support/authentication.rb
module RspecAuthentication
def sign_in(user)
controller.stub!(:current_user).and_return(user)
controller.stub!(:authenticate!).and_return(true)
helper.stub(:current_user).and_return(user) if respond_to?(:helper)
end
end
RSpec.configure do |config|
config.include RspecAuthentication, :type => :controller
config.include RspecAuthentication, :type => :view
config.include RspecAuthentication, :type => :helper
end
I asked a similar question here, but settled on a work around. This strange behavior has creeped up again and I would like to understand why this doesnt work.
UPDATE: I have found that calling controller.stub!(:current_user).and_return(#user) before helper.stub!(...) is what is causing this behavior. This is easy enough to fix in spec/support/authentication.rb, but is this a bug in Rspec? I dont see why it would be expected to not be able to stub a method on a helper if it was already stubbed on a controller.
Update to Matthew Ratzloff's answer: You don't need the instance object and stub! has been deprecated
it "why can't I stub a helper method?!" do
helper.stub(:current_user) { user }
expect(helper.do_something).to eq 'something'
end
Edit. The RSpec 3 way to stub! would be:
allow(helper).to receive(:current_user) { user }
See: https://relishapp.com/rspec/rspec-mocks/v/3-2/docs/
In RSpec 3.5 RSpec, it seems like helper is no longer accessible from an it block. (It will give you the following message:
helper is not available from within an example (e.g. an it block) or from constructs that run in the scope of an example (e.g. before, let, etc). It is only available on an example group (e.g. a describe or context block).
(I can't seem to find any documentation on this change, this is all knowledge gained experimentally).
The key to solving this is knowing that helper methods are instance methods, and that for your own helper methods it's easy to do this:
allow_any_instance_of( SomeHelper ).to receive(:current_user).and_return(user)
This is what finally worked for me
Footnotes/Credit Where Credit Due:
Super Props to a blog entry by Johnny Ji about their struggles stubbing helper/instance methods
Try this, it worked for me:
describe SomeHelper
before :each do
#helper = Object.new.extend SomeHelper
end
it "why cant i stub a helper method?!" do
#helper.stub!(:current_user).and_return(#user)
# ...
end
end
The first part is based on this reply by the author of RSpec, and the second part is based on this Stack Overflow answer.
Rspec 3
user = double(image: urlurl)
allow(helper).to receive(:current_user).and_return(user)
expect(helper.get_user_header).to eq("/uploads/user/1/logo.png")
This worked for me in the case of RSpec 3:
let(:user) { create :user }
helper do
def current_user; end
end
before do
allow(helper).to receive(:current_user).and_return user
end
As of RSpec 3.10, this technique will work:
before do
without_partial_double_verification {
allow(view).to receive(:current_user).and_return(user)
}
end
The without_partial_double_verification wrapper is needed to avoid a MockExpectationError unless you have that turned off globally.
RSpec has:
describe "the user" do
before(:each) do
#user = Factory :user
end
it "should have access" do
#user.should ...
end
end
How would you group tests like that with Test::Unit? For example, in my controller test, I want to test the controller when a user is signed in and when nobody is signed in.
You can achieve something similar through classes. Probably someone will say this is horrible but it does allow you to separate tests within one file:
class MySuperTest < ActiveSupport::TestCase
test "something general" do
assert true
end
class MyMethodTests < ActiveSupport::TestCase
setup do
#variable = something
end
test "my method" do
assert object.my_method
end
end
end
Test::Unit, to my knowledge, does not support test contexts. However, the gem contest adds support for context blocks.
Shoulda https://github.com/thoughtbot/shoulda although it looks like they've now made the context-related code into a separate gem: https://github.com/thoughtbot/shoulda-context
Using shoulda-context:
In your Gemfile:
gem "shoulda-context"
And in your test files you can do things like (notice the should instead of test:
class UsersControllerTest < ActionDispatch::IntegrationTest
context 'Logged out user' do
should "get current user" do
get api_current_user_url
assert_response :success
assert_equal response.body, "{}"
end
end
end