Testing an API resource with RSpec - ruby-on-rails

I've done a bit of googling on the topic, and I'm still confused.
I'm building a custom help page with the Zendesk API Ruby Client, and I'm at a stage when I need to test the creation of the ZendeskAPI::Ticket resource. The following code is in the spec/features directory. It fills out a form with valid values and submits the form to the #create action. Fairly standard, simple stuff.
require 'spec_helper'
feature 'ticket creation' do
scenario 'user creates new ticket' do
visit root_path
fill_in 'Name', with: 'Billybob Joe'
fill_in 'Email', with: 'joe#test.com'
fill_in 'Subject', with: 'Aw, goshdarnit!'
fill_in 'Issue', with: 'My computer spontaneously blew up!'
click_button 'Create Ticket'
expect(page).to have_content('Ticket details')
end
end
And here is the relevant part of the Tickets controller. The ticket_valid? method supplies minimal validations for the options hash and client is an instance of the ZendeskAPI::Client.
def create
options = { subject: params[:subject], comment: { value: params[:issue] },
requester: params[:email], priority: 'normal' }
if ticket_valid? options
flash[:success] = 'Ticket created.'
#ticket = ZendeskAPI::Ticket.create(client, options)
redirect_to ticket_path(#ticket.id)
else
flash[:error] = 'Something went wrong. Try again.'
redirect_to root_url
end
end
Problem is, whenever I run the tests, an actual ticket is created in the Zendesk backend that I'll have to delete manually later when I just want to test for successful form submission without actually creating a ticket.
So my question is, how can I test the ticket creation form without creating an actual ticket in the Zendesk backend whenever I run the tests?
The articles and blogs I've been reading as a result of my googling vaguely refers to using RackTest, while others suggest not using Capybara at all for this sort of thing, which leaves me even more confused. I'm still relatively new to RSpec and even newer to dealing with building Rails apps with an API, so a clear explanation would be great.
Thanks in advance!! You're awesome.

One way to do this would be abstract away your interface to ZenDesk into your own class and then mock it in your tests.
For example, you could create an interface class:
class ZendeskGateway
def create_ticket(client, options)
ZendeskAPI::Ticket.create(client, options)
end
end
Then, in your code, you replace the usage of the Zendesk API in your controller with your interface class:
class TicketsController < ApplicationController
attr_accessor :zendesk_gateway
after_initialize :init
def init
#zendesk_gateway = ZendeskGateway.new
end
def create
options = { subject: params[:subject], comment: { value: params[:issue] },
requester: params[:email], priority: 'normal' }
if ticket_valid? options
flash[:success] = 'Ticket created.'
#ticket = #zendesk_gateway.create_ticket(client, options)
redirect_to ticket_path(#ticket.id)
else
flash[:error] = 'Something went wrong. Try again.'
redirect_to root_url
end
end
end
Now that it is abstracted, you can using a mocking framework (like mocha) to stub out the method during your test so that it doesn't actually call out to Zendesk:
zendesk_ticket = ZendeskAPI::Ticket.new(client, :id => 1, :priority => "urgent")
#controller.zendesk_gateway.expects(:create_ticket).returns(zendesk_ticket)
This was a very quick/dirty example. But hopefully you can see the general idea.

If you don't want to call the Zendesk, you'll have to create a "test double" instead of the actual call. The test double capability that comes with RSpec is described minimally at https://github.com/rspec/rspec-mocks, but is covered more comprehensively in blogs and books.
The answer posted simultaneously to this discusses creating a separate class, but still seems to involve creating a Zendesk ticket. You don't actually need a separate class and you don't need to create any ZendeskAPI objects at all. Instead, you would stub ZendeskAPI::Ticket#create to return a test double which in turn would need to serve up whatever Zendesk ticket methods the rest of your test needs, which at least includes id.
The use of Capybara is really a secondary issue and refers to how you drive the test. But note that your test currently requires rendering the ticket page and checking the content of that page. If you want to test "just" your controller, then you can/should just test that it makes the proper calls (e.g. to ZendeskAPI::Ticket) and redirects to the appropriate page. Further, if you do just that, you have much less to simulate in your test double.

Related

Using Rails 5 and minitest, how can I access a record that was created in the test?

I have this test that was passing before I switched the Note table to use UUID:
test "SHOULD create note AND redirect to Notes#index(SINCE null folder_id) WHEN only body is provided IF logged in as account:owner" do
sign_in #user
assert_difference('#user.account.notes.count') do
post notes_url(#user.account.hash_id), params: { note: { body: #note_by_user_no_folder.body } }
end
assert_redirected_to notes_url(#user.account.hash_id, anchor: Note.last.id)
end
But after switching to UUID, I'm getting the following failure error:
Failure:
NotesControllerTest#test_SHOULD_create_note_AND_redirect_to_Notes#index(SINCE_null_folder_id)_WHEN_only_body_is_provided_IF_logged_in_as_account:owner [/Users/chris/Dropbox/Repositories/keepshelf/test/controllers/notes_controller_test.rb:117]:
Expected response to be a redirect to <http://www.example.com/11111111/notes#9dc409ff-14cc-5f64-8f5f-08e487f583ee> but was a redirect to <http://www.example.com/11111111/notes#34dac6b7-46af-4c5c-bff7-760ffa77edf6>.
Expected "http://www.example.com/11111111/notes#9dc409ff-14cc-5f64-8f5f-08e487f583ee" to be === "http://www.example.com/11111111/notes#34dac6b7-46af-4c5c-bff7-760ffa77edf6".
I take this as that since the UUID's do not follow an order, that the new Note I am creating in the "post notes_url(..." does not end up being the "last" one that Note.last.id finds.
How can I set the anchor: to record_that_was_just_created.id ?
I figured this out. apparently there is a way to access the controller's instance variables in the test so I got it to work by changing the assert_redirected_to to
assert_redirected_to notes_url(#user.account.hash_id, anchor: controller.instance_variable_get(:#note).id)
Late answer here, I'm glad you got the result you need!
... in case anyone else reads this looking for the test practices ...
The short answer is - you should always know what data is being used in the test ... because you make it. The most common 3 ways ...
In the setup method where you
Load a fixture #account = Accounts(:first) (needs matching entry in yaml file)
Call factorybot gem (see thoughtbot's page for how to)
Explicitly write #account = #user.account.build(field: value, field2: value)
So, you have the literal answer to your question - it's bad practice to test against uncertain data - Shaunak was saying that too.
The above is probably enough - but your detail oriented testing should be in unit tests - where you are not calling against db & making your tests the slowest possible version. Your integration level stuff should just test that creation was successful - you already know the data going in there works.
For the best experience you probably want unit tests something from the 3 methods above in setup & use #account in the test like so to test against the validations you are about to create ...
So the unit test would be ...
setup
#account = #user.account.build # avoid hitting db unnecessarily versus create
#account.field = value
#account.field2 = value2
end
def test_details_pass_activerecord_validations
signin #user
#account.test_specific_fields = value # stuff not all the tests use
assert #account.valid? # trigger all the activerecord validations, but not db
end
From there your integration ...
test "SHOULD create note AND redirect to Notes#index(SINCE null folder_id) WHEN only body is provided IF logged in as account:owner" do
# Your setup method above should have most of the intialization
# ... basically #account = #user.account .note would be setup, but not inserted
signin #user
assert_difference('#user.account.notes.count') do
post notes_url(#account), params: { note: { body: #account.note.body } }
end
assert_redirected_to notes_url(#account, anchor: #account.whatever_field)
end

Webhooks in Rails Controller for Shopify App

I have a webhooks controller which listens for when a new customer is created in Shopify.
def customer_created_callback
#first_name = params[:first_name]
#last_name = params[:last_name]
#email = params[:email]
#customer = Customer.new(first_name: #first_name, last_name: #last_name, email: #email)
if #customer.save
redirect_to customers_url
else
puts "error"
end
end
I am wondering if I should be creating the customer in this controller action. It seems like I should be handling the customer creation in Customer#create. But I'm not sure if it's a good idea to be passing all these params to a new action.
What's a good example of where I should be redirecting_to? I thought the idea of a webhook is that it's happening in the background so no page is actually going to be rendered..
Hope those make sense.
It seems like I should be handling the customer creation in Customer#create
Where the code lives is up to you but you must keep it DRY. Personally I like to use Service Objects since they make testing easier.
What's a good example of where I should be redirecting_to?
You need to return a 200 response with no content:
render :nothing => true, :status => 200
Typically you'll use a background job which will use the service object when it runs. This post on Webhook Best Practices is an excellent resource to get acquainted with the in's and out's of web hooks.

How to write a Klass.any_instance_with_id(id).expects(:method) in RSpec

When writing tests using RSpec, I regularly have the need to express something like
Klass.any_instance_with_id(id).expects(:method)
Main reason is that in my test, I often have the object that should receive that method call available, but due to the fact that ActiveRecord, when loading the object with that id from the database, will create a different instance, I can't put the "expects" on my own instance
Sometimes I can stub the find method to force ActiveRecord to load my instance, sometimes I can stub other methods, but having that "any_instance_with_id" would make life so much easier...
Can't image I'm the first having this problem... So if any of you found a "workaround", I'd be glad to find out!
Example illustrating the need:
controller spec:
describe 'an authorized email' do
let(:lead) { create(:lead, status: 'approved') }
it "should invoice its organisation in case the organisation exceeds its credit limit" do
lead.organisation.expects :invoice_leads
get :email
end
end
controller:
def email
leads = Lead.approved
leads.each do |lead|
lead.organisation.invoice_leads if lead.organisation.credit_limit_exceeded?
end
redirect_to root_path
end
It seems weird to me you need that for specs.
You should take the problem one level higher: when your app tries to retrieve the record.
Example:
#code
#user = User.find(session[:user_id])
# spec
let(:fake_user) { mock_model 'User', method: false }
it 'description' do
User.should_receive(:find).and_return fake_user
fake_user.expects(:method)
#...
end
Order/invoice example:
let(:order) { mock_model 'order', invoice: invoice }
let(:invoice) { mock_model 'Invoice', 'archive!' => false }

Doubts on Rspec feature when user_id is needed

Say I want to test that a User is able to fill a form to create a new project. The route that I need to visit is: new_user_project_path(:user_id).
So, my feature rspec looks like this right now:
feature "User creates a project" do
scenario "Logged in User creates a project" do
login_with_oauth #this sets current_user
visit new_user_project_path
fill_in 'Title', :with => 'Colchasdadasdasoneta'
fill_in 'Description', :with => 'lalalssalsalas'
click_in 'Create Project'
end
end
As you can tell, I can't use this visit new_user_project_path because it needs a :user_id to be passed. My question is, how can I access the current_user? Or what is the correct way of testing this kind of stuff?
Your routes have problem. Instead of fixing the problem directly which is easy, better to double check your code logic.
You should not define user id as path param to create a new project. Instead, the project instance should be initiated by current_user.
To associate a new project with user, just do it in controller
#project = current_user.projects.new
In conclusion:
Change the controller code similar to above
Restore your routes to conventional resource.
Then, use your test as it is, though a bit more expectations should be appended.
Assuming login_with_oauth is a spec helper method that you wrote, you can have that return the user that it setup. Then you can do:
feature "User creates a project" do
scenario "Logged in User creates a project" do
user = login_with_oauth
visit new_user_project_path(user)
If that doesn't work, and login_with_oauth is something you don't control (say it comes from some library), you can fetch the first user out of the database. This is assuming no other users are in the db, but there shouldn't be because your tests should be clean and autonomous. Then do:
feature "User creates a project" do
scenario "Logged in User creates a project" do
login_with_oauth
user = User.first
visit new_user_project_path(user)

Session variables with Cucumber Stories

I am working on some Cucumber stories for a 'sign up' application which has a number of steps.
Rather then writing a Huuuuuuuge story to cover all the steps at once, which would be bad, I'd rather work through each action in the controller like a regular user. My problem here is that I am storing the account ID which is created in the first step as a session variable, so when step 2, step 3 etc are visited the existing registration data is loaded.
I'm aware of being able to access controller.session[..] within RSpec specifications however when I try to do this in Cucumber stories it fails with the following error (and, I've also read somewhere this is an anti-pattern etc...):
Using controller.session[:whatever] or session[:whatever]
You have a nil object when you didn't expect it!
The error occurred while evaluating nil.session (NoMethodError)
Using session(:whatever)
wrong number of arguments (1 for 0) (ArgumentError)
So, it seems accession the session store isn't really possible. What I'm wondering is if it might be possible to (and I guess which would be best..):
Mock out the session store etc
Have a method within the controller and stub that out (e.g. get_registration which assigns an instance variable...)
I've looked through the RSpec book (well, skimmed) and had a look through WebRat etc, but I haven't really found an answer to my problem...
To clarify a bit more, the signup process is more like a state machine - e.g. the user progresses through four steps before the registration is complete - hence 'logging in' isn't really an option (it breaks the model of how the site works)...
In my spec for the controller I was able to stub out the call to the method which loads the model based on the session var - but I'm not sure if the 'antipattern' line also applies to stubs as well as mocks?
Thanks!
I'll repeat danpickett in saying mocks should be avoided whenever possible in Cucumber. However if your app does not have a login page, or perhaps performance is a problem, then it may be necessary to simulate login directly.
This is an ugly hack, but it should get the job done.
Given /^I am logged in as "(.*)"$/ do |email|
#current_user = Factory(:user, :email => email)
cookies[:stub_user_id] = #current_user.id
end
# in application controller
class ApplicationController < ActionController::Base
if Rails.env.test?
prepend_before_filter :stub_current_user
def stub_current_user
session[:user_id] = cookies[:stub_user_id] if cookies[:stub_user_id]
end
end
end
mocks are bad in cucumber scenarios - they're almost kind of an antipattern.
My suggestion is to write a step that actually logs a user in. I do it this way
Given I am logged in as "auser#example.com"
Given /^I am logged in as "(.*)"$/ do |email|
#user = Factory(:user, :email => email)
#user.activate!
visit("/session/new")
fill_in("email", :with => #user.email)
fill_in("password", :with => #user.password)
click_button("Sign In")
end
I realize that the instance variable #user is kind of bad form—but I think in the case of logging in/out, having #user is definitely helpful.
Sometimes I call it #current_user.
Re. Ryan's solution - you can open up ActionController in you env.rb file and place it there to avoid putting in your production code base (thanks to john # pivotal labs)
# in features/support/env.rb
class ApplicationController < ActionController::Base
prepend_before_filter :stub_current_user
def stub_current_user
session[:user_id] = cookies[:stub_user_id] if cookies[:stub_user_id]
end
end
I don't know how much this relates to the original question anymore, but I decided to post anyway in the spirit of discussion...
We have a cucumber test suite that takes > 10 minutes to run so we wanted to do some optimization. In our app the login process triggers a LOT of extra functionality that is irrelevant to majority of the scenarios, so we wanted to skip that by setting the session user id directly.
Ryanb's approach above worked nicely, except that we were unable to log out using that approach. This made our multi-user stories fail.
We ended up creating a "quick login" route that is only enabled in test environment:
# in routes.rb
map.connect '/quick_login/:login', :controller => 'logins', :action => 'quick_login'
Here is the corresponding action that creates the session variable:
# in logins_controller.rb
class LoginsController < ApplicationController
# This is a utility method for selenium/webrat tests to speed up & simplify the process of logging in.
# Please never make this method usable in production/staging environments.
def quick_login
raise "quick login only works in cucumber environment! it's meant for acceptance tests only" unless Rails.env.test?
u = User.find_by_login(params[:login])
if u
session[:user_id] = u.id
render :text => "assumed identity of #{u.login}"
else
raise "failed to assume identity"
end
end
end
For us this ended up being simpler than working with the cookies array. As a bonus, this approach also works with Selenium/Watir.
Downside is that we're including test-related code in our application. Personally I don't think that adding code to make application more testable is a huge sin, even if it does add a bit of clutter. Perhaps the biggest problem is that future test authors need to figure out which type of login they should use. With unlimited hardware performance we obviously wouldn't be doing any of this.
Re: Ryan's solution:
Does not work with Capybara, unless small adaptation done:
rack_test_driver = Capybara.current_session.driver
cookie_jar = rack_test_driver.current_session.instance_variable_get(:#rack_mock_session).cookie_jar
#current_user = Factory(:user)
cookie_jar[:stub_user_id] = #current_user.id
(found here: https://gist.github.com/484787)
My understanding is that you get:
You have a nil object when you didn't expect it!
The error occurred while evaluating nil.session (NoMethodError)
when session[] is accessed before request has been instantiated. In your case, I'd imagine if you put webrats' visit some_existing_path before accessing session[] in your step defenition, the error will go away.
Now, unfortunately, session doesn't seem to persist across steps (at least, I couldn't find the way), so this bit of information doesn't help to answer your question :)
So, I suppose, Ryan's session[:user_id] = cookies[:stub_user_id]... is the way to go. Although, imo, test related code in the application itself doesn't sound right.
I use a testing-only sign-in solution like Prikka's, but I do it all in Rack instead of creating a new Controller and routes.
# in config/environments/cucumber.rb:
config.middleware.use (Class.new do
def initialize(app); #app = app; end
def call(env)
request = ::Rack::Request.new(env)
if request.params.has_key?('signed_in_user_id')
request.session[:current_user_id] = request.params['signed_in_user_id']
end
#app.call env
end
end)
# in features/step_definitions/authentication_steps.rb:
Given /^I am signed in as ([^\"]+)$/ do |name|
user = User.find_by_username(name) || Factory(:user, :username => name)
sign_in_as user
end
# in features/step_definitions/authentication_steps.rb:
Given /^I am not signed in$/ do
sign_in_as nil
end
module AuthenticationHelpers
def sign_in_as(user)
return if #current_user == user
#current_user = user
get '/', { 'signed_in_user_id' => (user ? user.to_param : '') }
end
end
World(AuthenticationHelpers)
#Ajedi32 I ran into the same issue (undefined method 'current_session' for Capybara::RackTest::Driver) and putting this in my step definition fixed the problem for me:
rack_test_browser = Capybara.current_session.driver.browser
cookie_jar = rack_test_browser.current_session.instance_variable_get(:#rack_mock_session).cookie_jar
cookie_jar[:stub_user_id] = #current_user.id
In my controller action, I referred to cookies[:stub_user_id], instead of cookie_jar[:stub_user_id]
Why don't you use FactoryGirl or (Fixjour or Fabricator) with Devise (or Authlogic) and SentientUser? Then you can simply sniff which user is already logged in!
#user = Factory(:user) # FactoryGirl
sign_in #user # Devise
User.current.should == #user # SentientUser
Another slight variation:
# In features/step_definitions/authentication_steps.rb:
class SessionsController < ApplicationController
def create_with_security_bypass
if params.has_key? :user_id
session[:user_id] = params[:user_id]
redirect_to :root
else
create_without_security_bypass
end
end
alias_method_chain :create, :security_bypass
end
Given %r/^I am logged in as "([^"]*)"$/ do |username|
user = User.find_by_username(username) || Factory(:user, :username => username)
page.driver.post "/session?user_id=#{user.id}"
end
After a lot of soul searching and web surfing, I finally opt'ed for a very simple and obvious solution.
Using cookies adds two problems. First you have code in the application specific for testing and second there is the problem that creating cookies in Cucumber is hard when using anything other than rack test. There are various solutions to the cookie problem but all of them are a bit challenging, some introduce mocks, and all of them are what I call 'tricky'. One such solution is here.
My solution is the following. This is using HTTP basic authentication but it could be generalized for most anything.
authenticate_or_request_with_http_basic "My Authentication" do |user_name, password|
if Rails.env.test? && user_name == 'testuser'
test_authenticate(user_name, password)
else
normal_authentication
end
end
test_authenticate does what ever the normal authenticate does except it bypasses any time consuming parts. In my case, the real authentication is using LDAP which I wanted to avoid.
Yes… it is a bit gross but it is clear, simple, and obvious. And… no other solution I've seen is cleaner or clearer.
Note, one feature is that if the user_name is not 'testuser', then the normal path is taken so they can be tested.
Hope this helps others...

Resources