Rspec2 + Rails4: Testing for displayed model fields in form partial - ruby-on-rails

Question
I'd like to write tests that check the model fields that are displayed in my "show" and "form" partials. I succeeded for "show", not for "form".
Main constrain: The solution must be able to loop through an Array that contains each names of the model fields.
I believe this case can be interesting for anyone that is trying to shorten his test script files, while having many fields, and having a complete control over what's displayed and what's not, so I'll put some efforts trying to find a solution, with your help if you please :)
Form view
Nothing fancy
= form_for #user do |f|
= f.select :field_1, options_from_collection_for_select ...
= f.text_field :field_2
...
Actual situation
I found an easy way for the "show" partial, here is how my spec file looks like:
def user_fields_in_show_view
[:field_1, :field_2, ..., :field_n]
end
it 'display fields' do
user_fields_in_show_view.each do |field|
User.any_instance.should_receive(field).at_least(1).and_call_original
end
render
end
This works well.
-
But the exact same technique does not work in the "form" partial, using the same code
def user_fields_in_form_view # List of fields need to be different, because of the associations
[:field_1_id, :field_2, ..., :field_n]
end
it 'display fields' do
user_fields_in_form_view.each do |field|
User.any_instance.should_receive(field).at_least(1).and_call_original
end
render
end
It whines like this:
Failure/Error: Unable to find matching line from backtrace
Exactly one instance should have received the following message(s) but didn't: field1_id, field_2, ..., field_n
# Backtrace is long and shows only rspec/mock, rspec/core, rspec/rails/adapters, and spork files
What I tried so far
1- I commented out the stub part of my tests and output rendered to the console, to manually check what's generated by my view, and yes the fields are correctly generated.
2- I replaced User.any_instance by the model I assign to the view, error is slightly different but it still not working
it 'display fields' do
user = create :user
assign :user, user
user_fields_in_form_view.each do |field|
user.should_receive(field).at_least(1).and_call_original
end
render
end
Gives:
Failure/Error: user.should_receive(field).at_least(1).and_call_original
(#<User:0x0000000506e3e8>).field_1_id(any args)
expected: at least 1 time with any arguments
received: 0 times with any arguments
3- I change the code so the it is inside the loop, like this:
user_fields_in_form_view.each do |field|
it 'display fields' do
user = create :user
assign :user, user
user.should_receive(field).at_least(1).and_call_original
render
end
end
Same result as above
And I run out of options. I suspect the internals of FormBuilder to play a bad trick on me but I can't figure it out, I'm not very knowledgeable with those yet. Thanks for reading

I usually try to write unit test as simple as possible. Loops in unit tests don't add much readability and are not very good practice in general. I'd rewrite the test like this:
it 'should display user name and email' do
# note: `build` is used here instead of `create`
assign :user, build(:user, first_name: 'John', last_name: 'Doe', email: 'jd#example.com')
render
rendered.should have_content 'John'
rendered.should have_content 'Doe'
rendered.should have_content 'jd#example.com'
end
Thus, we're not limiting the view in how it should render the first and the last name. For example, if our view uses the following (bad) code in order to render user's full name, then your test will fail, but my test will work just fine, because it tests the behaviour of the view, not its internals:
<%= user.attributes.values_at('first_name', 'middle_name').compact.join(' ') %>
Moreover, multiple assertions in one test is a bad smell too. Going one step further, I'd replace this test with three smaller ones:
it "should display user's first name" do
assign :user, build(:user, first_name: 'John')
render
expect(rendered).to include 'John'
end
it "should display user's last name" do
assign :user, build(:user, last_name: 'Doe')
render
expect(rendered).to include 'Doe'
end
it "should display user's email" do
assign :user, build(:user, email: 'jd#example.com')
render
expect(rendered).to include 'jd#example.com'
end
========
UPD: Let's make it more dynamic in order to avoid tons of repetition. Tis doesn't answers why your spec fails, but hopefully represents working tests:
%i(first_name last_name email).each do |field|
it "should display user's #{field}" do
user = build(:user)
assign :user, user
render
expect(rendered).to include user.public_send(field)
end
end
In order to make these tests more reliable, make sure that user factory doesn't contain repetitive data.

I am not quite sure how you build your form, but if you use the form_for, simple_form_for or formtastic_form_for helpers, actually you are using a different object. You write something like (assume the basic form_for)
= form_for #user do |f|
and all methods are relayed to object f. Now f.object will point to #user, but is it a copy of #user or #user itself, I don't know. So I would expect that User.any_instance should work.
Regardless, when doing a view test, it is not important how the contents of a field are set, it is important that the contents of a field are set correctly. Suppose you build your forms yourself, you switch to another library to build your forms, and all your tests break, because it retrieves the data differently. And that should not matter.
So I am with #DNNX, and in your view tests you should test the content of the rendered HTML and not how the data is retrieved.

Related

Rails test not working with model methods in views

When testing with rails, calling model methods within a view doesn't work. Is there a reason for this? The functions all work when accessing the page within the browser with no issue.
The view line causing problems:
<%= f.select :user_id, options_for_select(#project.assignment_options(current_user)),
{ include_blank: "Teammate to assign" }, class: "uk-select uk-margin-small-right" %>
The Test:
def current_user
#user
end
setup do
#user = users(:one)
sign_in #user
#project = projects(:one)
#project.owner_id = #user.id
#assignment = assignments(:one)
end
test "should get new" do
get new_project_assignment_url(#project.slug)
assert_response :success
end
The Error:
E
Error:
AssignmentsControllerTest#test_should_get_new:
ActionView::Template::Error: Couldn't find User without an ID
app/models/project.rb:33:in `owner'
app/models/project.rb:63:in `assignment_options'
app/views/assignments/_form.html.erb:6
app/views/assignments/_form.html.erb:2
app/views/assignments/new.html.erb:2
test/controllers/assignments_controller_test.rb:18:in `block in <class:AssignmentsControllerTest>'
When I do a "puts current_user.id" from within the view and test they match, so there is definitely a user id available on the page, but I am still hitting this issue. I have tried making tests for different pages that have different model methods, and they all fail with the same error.
The issue was that I wasn't saving the updated project. I needed to use #project.update. There was a strange behavior that when I updated the owner_id, the slug also updated for no apparent reason, but I worked around it by just creating a new project instead of using a fixture.

Failing Rails test based on lack of set session variable - how to set in Rails 5.2?

I have the following very simple test which has always passed.
test "should get index" do
sign_in #user
get companies_url
assert_response :success
end
However, I'm now receiving the following error.
Error:
CompaniesControllerTest#test_should_get_index:
ActionView::Template::Error: No route matches {:action=>"show", :controller=>"companies", :id=>nil}, missing required keys: [:id]
app/views/layouts/application.html.erb:53:in `_app_views_layouts_application_html_erb__2582383126246718868_70101260952060'
test/controllers/companies_controller_test.rb:16:in `block in <class:CompaniesControllerTest>'
What I've changed in my app is that I've built a sidebar (which I load via application.html.erb so it's loaded on all views (new, show, edit), which lets a user switch between various companies they "own" - which changes a session variable which we use to alter the content of the sidebar.
If we dig into the line that seems to be failing app/views/layouts/application.html.erb:53 this is how it looks:
<div class="list-group-item">
<%= link_to 'Company', company_path(session[:current_company]) unless current_user.companies.empty? %>
</div>
If I remove this link_to line then the tests pass.
My guess is that the show view is trying to load, including the sidebar which doesn't have a session[:current_company] set so the view crashes. However, in Rails 5.2 you cannot set/test session variables as far as I understand, so I'm wondering what the best way for me to set the testing up to make this pass? I do set a value for this session within my application controller a user signs in though:
def after_sign_in_path_for(resource_or_scope)
# Set a default current company scope for a use after signing in
session[:current_company] = current_user.companies.first.id unless current_user.companies.empty?
companies_path
end
Perhaps in the link_to from from within the sidebar I could add a default value to make sure we're always sending in a company, regardless of whether it's the session[:current_company] or not?
Altering the line to skip creating the link if there is no session variable present
<%= link_to 'Company', company_path(session[:current_company]) unless current_user.companies.empty? || !session[:current_company] %>
Seemed to do the trick and make all the tests pass. Would love some feedback on whether this is a good solution or not though! :)

Integration test: `assert_select 'a[href=?]'` fails for paginated pages

I have many integration tests that look something like:
first_page_of_users = User.page(1) # Using kaminari for pagination.
first_page_of_users.each do |user|
assert_select 'a[href=?]', user_path(user)
end
These tests fail with an error message such as:
Expected at least 1 element matching "a[href="/users/1"]", found 0..
I added puts #response.body to the tests to see the code that it responded with.
For all the users included in the response body it has correct hrefs, such as href="/users/75938613". However, the numbers behind /users/ are much higher, and "users1" is indeed not present (not just the url not present but the entire record isn't on that page). If I ensure that there are only a few fixtures so that no pagination applies, the tests pass. This makes me feel as if with pagination the order that first_page_of_users = User.page(1) applies differs from the order in records that first_page_of_users.each do |user| assert_select 'a[href=?]', user_path(user) expects...
What could be causing this error? It applies to different pages and models of my app.
Update: For another model even when I try
Organization.all.each do |org|
puts #response.body
assert_select 'a[href=?]', organization_path(org)
end
I get the error:
Expected at least 1 element matching "a[href="/organizations/fix0"]",
The organization with the name "fix0" is not present at all in the body that it reponds with. My (shortened) fixtures are below and they confirm there should not be any organization with that name. Any idea what's going on?
one:
name: abcd
activated: true
activated_at: 2015-04-16 11:38:53
two:
name: efgh
activated: true
activated_at: 2015-04-16 11:38:53
<% 35.times do |n| %>
organization_<%= n %>:
name: <%="fix#{n}" %>
activated: true
activated_at: <%= Time.zone.now %>
<% end %>
When integration testing, it's important to keep track of what should show up where and test just that. Because we have too many records for the first page in this case, anything that would land on later pages will cause the failing test. As you suspect, something like this should verify that the first page of results is present:
Organization.order('organizations.name','desc').page(1).each do |organization|
assert_select 'a[href=?]', organization_path(organization)
end
I recommend keeping a smaller data set for testing, because in this case you should also 'get' page two in your test and do the assertions above for 'page(2)' as well. This means this integration test has to issue two get requests (plus any setup you may need) and check the results, and making a habit of that can slow down your suite.

RSpec View testing: How to modify params?

I am trying to test my views with RSpec. The particular view that is causing me troubles changes its appearance depending on a url parameter:
link_to "sort>name", model_path(:sort_by => 'name') which results in http://mydomain/model?sort_by=name
My view then uses this parameter like that:
<% if params[:sort_by] == 'name' %>
<div>Sorted by Name</div>
<% end %>
The RSpec looks like this:
it "should tell the user the attribute for sorting order" do
#Problem: assign params[:sort_for] = 'name'
render "/groups/index.html.erb"
response.should have_tag("div", "Sorted by Name")
end
I would like to test my view (without controller) in RSpec but I can't get this parameter into my params variable. I tried assign in all different flavours:
assign[:params] = {:sort_by => 'name'}
assign[:params][:sort_by] = 'name'
...
no success so far. Every idea is appreciated.
If its a controller test then it would be
controller.stub!(:params).and_return {}
If its a helper test then it would be:
helper.stub!(:params).and_return {}
And its a view test it would be:
view.stub!(:params).and_return {}
If you get warning like below.
Deprecation Warnings:
Using `stub` from rspec-mocks' old `:should` syntax without explicitly enabling the syntax is deprecated. Use the new `:expect` syntax or explicitly enable `:should` instead. Called from /home/akbarbin/Documents/Office/projects/portfolio/spec/views/admin/waste_places/new.html.erb_spec.rb:7:in `block (2 levels) in <top (required)>'.
If you need more of the backtrace for any of these deprecations to
identify where to make the necessary changes, you can configure
`config.raise_errors_for_deprecations!`, and it will turn the
deprecation warnings into errors, giving you the full backtrace.
1 deprecation warning total
Finished in 4.86 seconds (files took 4.72 seconds to load)
You can change it into
allow(view).to receive(:params).and_return({sort_by: 'name'})
That's because you shouldn't be using params in your views.
The best way I see it to use an helper.
<div>Sorted by <%= sorted_by %></div>
And in one of your helper files
def sorted_by
params[:sorted_by].capitalize
end
Then you can test your helpers quite easily (because in helpers tests, you can define the params request.
The easy way is to just do this:
helper.params = {:foo => '1', :bar => '2'}
But in general it's better to be more integration-y and not "stub" values when it's feasible. So I prefer to use controller tests with integrate_views. Then you can specify your params to the get, and test that the entire flow works, from sending params to the controller, to having them processed by the controller, and finally to rendering.
I also generally prefer to pull out view logic into helpers, which can be easier to test.
For instance, say I have a helper called selection_list, which returns a Hash whose "selected_preset" key relies on params[:selected_preset], and defaults to 42 if an empty value is specified for the param.
Here's a controller test where we've called integrate_views (you could of course do the same thing with an actual view test, if you're into that).
describe '#show' do
describe 'selected_preset' do
it 'should default to 42 if no value was entered' do
get :show, :params => {:selected_preset => ''}
response.template.selection_list[:selected_preset].should == 42
This integration test will alert me if some part of this functionality breaks. But I also would ideally like to have some unit tests to help me pinpoint that breakage.
I'll start by having the helper use an instance variable instead of directly accessing params. I'll change the above code by adding a single line directly below the get, as follows:
describe '#show' do
describe 'selected_preset' do
it 'should default to 42 if no value was entered' do
get :show, :params => {:selected_preset => ''}
assigns[:selected_preset].should == 42 # check instance variable is set
response.template.selection_list[:selected_preset].should == 42
Now I also can easily perform a helper unit test:
describe MyHelper do
describe '#selection_list' do
it 'should include the selected preset' do
assigns[:selected_preset] = 3
helper.selection_list[:selected_preset].should == 3
Another method of setting view params:
controller.request.path_parameters[:some_param] = 'a value'

How to require a value is entered in a search form

I built a basic search form that queries one column in one table of my app. I followed episode 37 Railscast: http://railscasts.com/episodes/37-simple-search-form. Note I just posted another search related question, but it's on a completely different issue.
In my app, the search queries the zip code column of my profile table, and returns a list of profiles that contain the right zip code.
Here's my problem. Currently, when a user leaves the input blank and hits the submit button, the search displays all profiles on the site. I don't want this to happen. If the field is blank, I don't want the search to go through. I'd like to either do a flash notice or throw an error, explaining that the user needs to enter a zip code to proceed.
Here's my setup:
PROFILES CONTROLLER
def index
#profiles = Profile.search(params[:search])
end
PROFILE MODEL
def self.search(search)
if search
find(:all, :conditions => ['zip LIKE ?', "%#{search}%"])
else
find(:all)
end
end
PROFILE/INDEX.HTML.ERB
<% form_tag ('/profiles', :method => :get) do %>
<%= text_field_tag :search, params[:search], :maxlength => 5 %>
<%= submit_tag "Go", :name => nil %>
<% end %>
Thanks!
def index
#profiles = Profile.search(params[:search]) unless params[:search].blank?
end
You probably don't want to throw an error if the search field is blank, because the user will see that error the first time he comes to the index page. To properly handle that type of error message, you'll need to do one of several things.
Split the form generation and the actual search into two separate actions. In a RESTful app, this would typically be a new and create action. (new for the form, create for the actual search).
Add a check for a post, as opposed to a get. Only attempt the search, or throw the error, if it's a post. Otherwise, just show the form. You'll typically see this in older Rails examples (like pre- 2.0 tutorials).
Add some hidden field that says "Hey, I'm submitting a search." This is the same idea as checking for a post, but would still work if you wanted all gets for some reason.
My choice would be the first one. It'd roughly look like this.
def new
end
def create
if params[:search].blank?
flash.now[:error] = "Please enter a zip code to search for."
render :new
else
#profiles = Profile.search(params[:search])
render :show
end
end
In your views, new.html.erb (or .haml or whatever) would contain your search form and show.html.erb would contain your search results. Usually there's a search form partial that both of them would share.
You just need to check if it's blank before you do your search.
def index
if params[:search].blank?
flash[:error] = "Doh! You forgot the zip code."
else
#profiles = Profile.search(params[:search])
end
end
If returning all results is never a use case then you might want to remove that code from your model as well. Also if you're checking for a blank search in more than this one controller action you should move the logic into the model.
I actually found an answer to this that doesn't require me to make any changes to my current setup.
If you look in the search model above, which I copied from the Railscast, you see that Ryan included wildcards on both sides of the search query:
find(:all, :conditions => ['zip LIKE ?', "%#{search}%"])
I'm not familiar with sql syntax at all, so I missed that. It was those wildcards that was letting through the blank search and returning all results in the database. When I removed the "%'s" from that sql statement, the blank search now returns the same result as a search where we don't have a record matching the zip queried, which is the result I wanted.
Just wanted to share so others might catch this in the future.

Resources