RSpec: factory not persisting after attribute changes? - ruby-on-rails

Im fairly new to RSpec but I am running into an issue (This is on Rails 4 fwiw). I have a simple model/factory test:
context "Scopes" do
let (:widget) {create(:core_widget)}
it ":active" do
puts "Last Created widget:"
puts pp(Core::Widget.last)
widget.type = "active"
widget.object_status_id = 15
puts "After attribute change:"
puts pp(Core::Widget.last)
#puts pp(widget.attributes)
expect(Core::Widget.active.ids).to include(widget.id)
end
end
This is testing out a very simple scope:
scope :active, -> { where(type: 'active', object_status_id:
[25, 15])
Pretty basic. However I noticed that checking (via the puts of the factory objectdoes NOT show the attribute changes (Changing.typetoactiveand.object_status_idto15`) when I re-print it?
I was told that let is lazily evaluated, and I understand that....but when I view the different object_id's when printing they are completely different. let should still have the same object reference in the same it block right?
Initially I thought it was a problem because I was doing build_stubbed on the factory creation. I also tried let! because I thought maybe that was the problem. Neither worked.

I think what is happening here is that you are updating the attributes of the model in memory without saving the changes to your database. Then when your active scope is called, a query is made to the database but since your changes haven't been saved to the database yet, the expected record is not found.
I would recommend checking out the various update* functions as a way to persist your changes to the database, or make sure you call save to save your changes.
For example, you can update your test to:
it ":active" do
puts "Last Created widget:"
puts pp(Core::Widget.last)
widget.type = "active"
widget.object_status_id = 15
widget.save! # Add this line here to explicitly save the record to the DB
puts "After attribute change:"
puts pp(Core::Widget.last) # Now this should find the changed record in the DB as expected
expect(Core::Widget.active.ids).to include(widget.id)
end

Related

Is there a way I can force a record to not be destroyed when running a feature test in RSpec? (Rails 6)

For context, I have a controller method called delete_cars. Inside of the method, I call destroy_all on an ActiveRecord::Collection of Cars. Below the destroy_all, I call another method, get_car_nums_not_deleted_from_portal, which looks like the following:
def get_car_nums_not_deleted_from_portal(cars_to_be_deleted)
reloaded_cars = cars_to_be_deleted.reload
car_nums = reloaded_cars.car_numbers
if reloaded_cars.any?
puts "Something went wrong. The following cars were not deleted from the portal: #{car_nums.join(', ')}"
end
car_nums
end
Here, I check to see if any cars were not deleted during the destroy_all transaction. If there are any, I just add a puts message. I also return the ActiveRecord::Collection whether there are any records or not, so the code to follow can handle it.
The goal with one of my feature tests is to mimic a user trying to delete three selected cars, but one fails to be deleted. When this scenario occurs, I display a specific notice on the page stating:
'Some selected cars have been successfully deleted from the portal, however, some have not. The '\
"following cars have not been deleted from the portal:\n\n#{some_car_numbers_go_here}"
How can I force just one record to fail when my code executes the destroy_all, WITHOUT adding extra code to my Car model (in the form of a before_destroy or something similar)? I've tried using a spy, but the issue is, when it's created, it's not a real record in the DB, so my query:
cars_to_be_deleted = Car.where(id: params[:car_ids].split(',').collect { |id| id.to_i })
doesn't include it.
For even more context, here's the test code:
context 'when at least one car is not deleted, but the rest are' do
it "should display a message stating 'Some selected cars have been successfully...' and list out the cars that were not deleted" do
expect(Car.count).to eq(100)
visit bulk_edit_cars_path
select(#location.name.upcase, from: 'Location')
select(#track.name.upcase, from: 'Track')
click_button("Search".upcase)
find_field("cars_to_edit[#{Car.first.id}]").click
find_field("cars_to_edit[#{Car.second.id}]").click
find_field("cars_to_edit[#{Car.third.id}]").click
click_button('Delete cars')
cars_to_be_deleted = Car.where(id: Car.first(3).map(&:id)).ids
click_button('Yes')
expect(page).to have_text(
'Some selected cars have been successfully deleted from the portal, however, some have not. The '\
"following cars have not been deleted from the portal:\n\n#{#first_three_cars_car_numbers[0]}".upcase
)
expect(Car.count).to eq(98)
expect(Car.where(id: cars_to_be_deleted).length).to eq(1)
end
end
Any help with this would be greatly appreciated! It's becoming quite frustrating lol.
One way to "mock" not deleting a record for a test could be to use the block version of .to receive to return a falsy value.
The argument for the block is the instance of the record that would be :destroyed.
Since we have this instance, we can check for an arbitrary record to be "not destroyed" and have the block return nil, which would indicate a "failure" from the :destroy method.
In this example, we check for the record of the first Car record in the database and return nil if it is.
If it is not the first record, we use the :delete method, as to not cause an infinite loop in the test (the test would keep calling the mock :destroy).
allow_any_instance_of(Car).to receive(:destroy) { |car|
# use car.delete to prevent infinite loop with the mocked :destroy method
if car.id != Car.first.id
car.delete
end
# this will return `nil`, which means failure from the :destroy method
}
You could create a method that accepts a list of records and decide which one you want to :destroy for more accurate testing!
I am sure there are other ways to work around this, but this is the best we have found so far :)
If there is a specific reason why the deletion might fail you can simulate that case.
Say you have a RaceResult record that must always refer to a valid Car and you have a DB constraint enforcing this (in Postgres: ON DELETE RESTRICT). You could write a test that creates the RaceResult records for some of your Car records:
it 'Cars prevented from deletion are reported` do
...
do_not_delete_cars = Car.where(id: Car.first(3).map(&:id)).ids
do_not_delete_cars.each { |car| RaceResult.create(car: car, ...) }
click_button('Yes')
expect(page).to have_text(...
end
Another option would be to use some knowledge of how your controller interacts with the model:
allow(Car).to receive(:destroy_list_of_cars).with(1,2,3).and_return(false) # or whatever your method would return
This would not actually run the destroy_list_of_cars method, so all the records would still be there in the DB. Then you can expect error messages for each of your selected records.
Or since destroy_all calls each record's destroy method, you could mock that method:
allow_any_instance_of('Car').to receive(:destroy).and_return(false) # simulates a callback halting things
allow_any_instance_of makes tests brittle however.
Finally, you could consider just not anticipating problems before they exist (maybe you don't even need the bulk delete page to be this helpful?). If your users see a more generic error, is there a page they could filter to verify for themselves what might still be there? (there's a lot of factors to consider here, it depends on the importance of the feature to the business and what sort of things could go wrong if the data is inconsistent).

Can/should Ruby on Rails tests persist to the database?

I have a DailyQuestionSet model and a method in it that (is supposed to) go to the database, see if a DailyQuestionSet already exists for that day, if so, return it, or if not, create a new one, save it to the database and return it.
This seems to work when I call it from a Controller but not from a Ruby on Rails automated test of the model. I am not sure if I'm going crazy or missing something.
When
class DailyQuestionSet < ApplicationRecord
def DailyQuestionSet.get_today_dailyquestionset
#dailyquestionset = nil
#questionlist = nil
#dailyquestionset_list = DailyQuestionSet.where('posed_date BETWEEN ? AND ?', DateTime.now.beginning_of_day, DateTime.now.end_of_day).all
if #dailyquestionset_list.empty?
#dailyquestionset = DailyQuestionSet.create!(posed_date: DateTime.now)
#dailyquestionset.save!
else
raise "disaster"
end
return #dailyquestionset
end
end
class DailyQuestionSetTest < ActiveSupport::TestCase
test "make sure today's daily_question_set has been deleted by the setup function" do
# later add function to delete today's daily_question_set so we can create it and then make sure get_today_dailyquestionset has created it once and then we can refer back to the same row
end
test "create daily_question_set and make sure it has questions" do
#dailyquestionset = DailyQuestionSet.get_today_dailyquestionset
....
end
test "create daily_question_set and make sure it has the same questions" do
#dailyquestionset = DailyQuestionSet.get_today_dailyquestionset
....
end
end
What I thought this would do is add a row to the daily_question_sets table in the database every time I run the first test, and then retrieve that row when I run the second test.
But when I look at the test database there's no row in there being created. I think maybe Rails is not committing the transaction to the database?
Or, put more simply, the raise "disaster" exception never gets thrown because get_today_dailyquestionset always returns a new DailyQuestionSet and never gets the one it (should have) created from the database.
I think I might be fundamentally misunderstanding testing in Rails. Should I be messing around with the DB in model tests at all?
Test database is erased on each run and each individual test runs it queries as transactions so you can't share objects from one test to the other.
If you need objects shared between tests use a setup block https://guides.rubyonrails.org/testing.html#putting-it-together or just run both queries one after the other on the same individual test.
It depends on how your tests are set up. Most likely, you have a separate test database for when the test suite is being run. Try running sqlite3 <app-name>_test, from the command line, if you are using the default Rails database solution. If you wanted to view the development database, you would run sqlite3 <app-name>_development.

Rails database changes aren't persisting through tests

I'm writing tests for my current rails project and I'm running into an issue whereby changes made to the test database through a post call aren't persisting long enough to test the changes.
Basically, I have objects which have a barcode assigned to them. I have put in a form whereby a user can scan in several barcodes to change multiple objects at a time. This is the code.
objecta_controller.rb:
def change_many
#barcodes = params[:barcodes].split()
#objects = ObjectA.order("barcode").where(barcode: #barcodes)
#objects.each do |b|
if can? :change, b
b.state_b()
end
end
end
(Note: #barcodes is a string of barcodes seperated by whitespace)
objecta_controller_test.rb:
test "change object" do
sign_in_as(:admin_staff)
b = ObjectA.new(
barcode: "PL123456",
current_status: "state_a")
post :change_many, { barcodes: b.barcode }
assert_equal("state_b", b.current_status, "Current status incorrect: #{b.to_s}")
end
Using byebug, I've ascertained that the objects do change state in the change_many method, but once it gets back to the test the object's state reverts back to its old one and the test fails.
First off, you are holding an in memory object not yet saved to the database, so first:
Add b.save before your post
Second, your in memory object will not automatically reflect changes in the database. You have to tell it to refresh its state, so:
Add b.reload before your assert

Unclear whether values are being saved to db

Part of a create method in my controller is:
if #organization.relationships && #organization.relationships.where('member = ?', true).any?
#organization.users.where('member = ?', true).each do |single|
single.create_digest
debugger
end
end
As you can see I've been testing with the debugger. In the debugger I'm experiencing the following strange behaviour. single and organization.users both display the details/values of the same user. However these values differ between when I examine using single and when I use organization.users in the debugger. For single the user does have values for activation_digest and activation_sent_at, while they are nil when I look at organization.users.
Can anyone explain this behaviour? The nil values are a problem since single isn't available outside the if statement. It's not clear to me whether the value have or have not been saved to the db.
P.S. The model method being used:
def create_digest
create_activation_digest
update_attribute(:activation_digest, self.activation_digest)
update_columns(activation_sent_at: Time.zone.now)
end
When you query for models in Rails (users in your case), each distinct query gives back separate copies of those models.
So for example, the users returned by this query:
#organization.users.where('member = ?', true)
Will be separate copies of the users that are returned by this slightly different query (I assume this is what you run in the debugger):
#organization.users
If you modify one copy of the user and save the modifications to the database, it will not automatically propagate those modifications to the other copy of the user. The other copy still has the (now out of date) data that was returned when you first ran the query.
To verify that the changes were actually persisted to the database, you can force Rails to refresh the user object with the latest data from the database by calling reload. For example:
# These are two different in-memory copies of the same user
user = #organization.users.where('member = ?', true).first
user_copy = User.find(user.id)
user.create_digest
# The copy is now out of date
user_copy.activation_sent_at # => nil
# Refresh the copy from the database
user_copy.reload
user_copy.activation_sent_at # => 2015-08-02 21:00:50 -0700

Generated string won't save to database... but tack "A" on the end and it will

In my application, these "planners" (essentially, article ideas) follow predetermined templates, written in Markdown, with some specific syntax here:
Please write your answer in the following textbox: [...]
Please write your answer in the following textarea:
...So here, on line, you should write one thing.
...Here, on line 2, you should write another.
...
...
...
Essentially, [...] is a text input, and a group of lines starting with ... are a textarea. That's not really the issue - it's just to explain what part of this code is doing.
On actions new and edit, the standard planner form is displayed, with the correct fields based on the template (for new) or current planner body (for edit). On save, the template's fields are filled in with params[:fields], and the resulting Markdown is saved as the planner's body. The code, I'd hope, is now possible to follow, knowing this context. Only relevant controller code is provided, and it uses make_resourceful.
class Staff::PlannersController < StaffController
make_resourceful do
actions :all
before :create do
find_planner_format
if #planner_format
current_object.body = fields_in_template #planner_format.body
else
flash[:error] = 'Planner format not found!'
redirect_to staff_planners_path
end
current_object.user = #current_user
end
before :update do
current_object.body = fields_in_template(current_object.body)
end
end
private
def fields_in_template(template)
fields = params[:fields] || {}
if fields[:inline]
template.gsub! /\[\.\.\..*\]/ do
"[...#{fields[:inline].shift}]"
end
end
if fields[:block]
template.gsub! /^\.{3}.*(\n\.{3}.*)*$/ do
fields[:block].shift.split("\n").collect { |line|
"...#{line}"
}.join("\n")
end
end
current_object.body = template
end
end
And now, the mystery: in the update action, changes to the body are not saved. After debugging, I've determined that the issue does not lie only in current_object.save, since the following before :update code does what you would expect:
before :update do
current_object.body = 'test string'
end
In fact, even this gets the expected result:
before :update do
current_object.body = fields_in_template(current_object.body) + 'a'
end
So now, the question: why is Rails so insistent that it not save the result of the function - and even then, only when it comes from update? More debugging showed that the object attribute is set, and even claims to save successfully, but reloading the object after save reverts the changes.
At first it looked like the resulting string was just a "poisoned" variable of sorts, and that rebuilding the string by appending "a" removed that strange state. However, the following code, which ought to add an "a" and remove it again, also failed to save.
before :update do
new_body = fields_in_template(current_object.body) + 'a'
new_body.slice! -1
current_object.body = new_body
end
This is just bizarre to me. What am I doing wrong here, and what can I possibly do to debug further? (Or if you happen to instantly see my mistake, that'd be nice, too...)
EDIT: After checking SQL logs (not sure why I didn't think to do this earlier), it would seem that Rails doesn't seem to acknowledge the new body attribute as actually being different, even though checking the string in the debugger confirms that it is. As such, Rails doesn't even run an UPDATE query unless something else is modified, in which case body is not included.
Got it! Sometimes it just helps to state the question out loud...
The deal is, I had forgotten that, when passing current_object.body to fields_in_template, it was being passed by reference. As such, all gsub! methods were running directly on current_object.body, so Rails acknowledged no real "changes" by the time I set body to what had just been set.
The solution:
def fields_in_template(template)
template = template.dup
# ...
end
Thanks for letting me talk to myself, and mission accomplished!
I'm not a Ruby programmer but does adding an 'a' convert the type of the variable to string? Maybe your variable is of the wrong type without adding 'a'.

Resources