Rspec test passing only when there's a PUTS in the model - ruby-on-rails

The puts statement must be having some kind of weird effect that I'm not seeing here...
I have an Order model. There's a callback on the model where the callback requires the model to be fully committed; i.e., I need to use an after_commit. However, the determinant of if the callback should run or not requires ActiveRecord::Dirty and therefore requires a before_save (or after_save, but I use before_save based on some other non-essential info).
I have combined the two thusly:
class Order
# not stored in DB, used solely to help the before_save to after_commit transition
attr_accessor :calendar_alert_type, :twilio_alerter
before_save
if self.calendar_alert_type.nil?
if self.new_record?
self.calendar_alert_type = "create, both"
elsif self.email_changed?
self.calendar_alert_type = "update, both"
elsif self.delivery_start_changed? || self.delivery_end_changed? || (type_logistics_attributes_modified.include? "delivery")
self.calendar_alert_type = "update, start"
elsif self.pickup_start_changed? || self.pickup_end_changed? || (type_logistics_attributes_modified.include? "pickup")
self.calendar_alert_type = "update, end"
end
end
puts "whatever"
end
after_commit do
if self.calendar_alert_type.present?
calendar_alert(self.calendar_alert_type)
end
end
end
def calendar_alert(alert_info)
puts "whatever"
alert_type = alert_info.split(",")[0].strip
start_or_end = alert_info.split(",")[1].strip
if start_or_end == "both"
["start","end"].each do |which_end|
Calendar.send(alert_type, which_end, self.id)
end
else
Calendar.send(alert_type, start_or_end, self.id)
end
end
All of the private methods and the ActiveRecord::Dirty statements are working appropriately. This is an example of a spec:
it "email is updated" do
Calendar.should_receive(:send).with("update", "start", #order.id).ordered
Calendar.should_receive(:send).with("update", "end", #order.id).ordered
find("[name='email']").set("nes#example.com")
find(".submit-changes").click
sleep(1)
end
it "phone is updated" do
... #same format as above
end
Literally all the specs like the above pass ONLY when EITHER puts statements is present. I feel like I'm missing something very basic here, just can't put my finger on it. It's super weird because the puts statement is spitting out random text...
*Note, I'm totally aware that should_receive should be expect_to_receive and that I shouldn't use sleep and that expectation mocks on feature tests aren't good. Working on updating the specs separately from bad code days, but these shouldn't be causing this issue... (feel free to correct me)

This behavior depends on your Rails version. Before Rails 5 you can return anything except false value to keep on running. A false will abort the before_* callback chain. puts 'whatever' returns a nil. So every thing works. Your if block seems to return a false (custom implemation for calendar_alert_type?). In this case the chain is holded.
With Rails 5 you have to throw(:abort) to stop callback handling.

Related

Rails/Ruby extract if block to helper or guard

In my Rails app I need to implement authentication for web app, I need to use an external resource to make it work. To do so I'm using custom Devise Strategies. After a tremendous amount of work, I finally managed to implement a code that covers all scenarios - the code is working but unfortunately my eyes bleed when I see the code below:
module Devise
module Strategies
class AwareLogin < Authenticatable
def authenticate!
# some logic
# (...)
if login.valid_password?(password) && aware_response.success?
success!(login)
elsif login.valid_password?(password) && !aware_response.success?
success!(login)
elsif login.id.nil? && aware_response.success?
login.set_user_tokens(aware_response)
success!(login)
elsif !login.valid_password?(password) && !aware_response.success?
raise ActiveRecord::Rollback
elsif !login.valid_password?(password) && aware_response.success?
fail!(:aware_auth)
end
rescue SupervisorRollback => s
#user_to_rollback = s.user_id
raise ActiveRecord::Rollback
end
end
end
end
end
end
Is there any way to replace that if block by something clearer like guard or even maybe external helper instead?
You can consolidate the logic a bit but given that the branches perform different actions you will still need some of the branching.
My recommended consolidation
def authenticate!
begin
if login.valid_password?(password) || (set_token = login.id.nil? && aware_response.success?)
login.set_user_tokens(aware_response) if set_token
success!(login)
else
aware_response.success? ? fail!(:aware_auth) : raise(ActiveRecord::Rollback)
end
rescue SupervisorRollback => s
#user_to_rollback = s.user_id
raise ActiveRecord::Rollback
end
end
Reasoning:
Your first 2 conditions only differ in their check of aware_response.success?; however whether this is true or false they perform the same action so this check is not needed.
Third branch performs 1 extra step of setting a token. Since this branch is unreachable unless !login.valid_password?(password) we have simply added an or condition to the first branch to conditionally set the token if this condition is true
The 4th and 5th conditions can be reduced to an else because we checked if login.valid_password?(password) is true in the first branch thus reaching this branch means it is false. Now the only difference is how we respond to aware_response.success? which I just converted to a ternary.

Making assertions from non-test-case classes

Background
I have a rails model that contains an ActiveRecord::Enum. I have a view helper that takes a value of this enum, and returns one of several possible responses. Suppose the cases were called enum_cases, for example:
enum_cases = [:a, :b, :c]
def foo(input)
case input
when :a then 1
when :b then 2
when :c then 3
else raise NotImplementedError, "Unhandled new case: #{input}"
end
end
I want to unit-test this code. Checking the happy paths is trivial:
class FooHelperTests < ActionView::TestCase
test "foo handles all enum cases" do
assert_equal foo(:a), 1
assert_equal foo(:b), 2
assert_equal foo(:c), 3
assert_raises NotImplementedError do
foo(:d)
end
end
end
However, this has a flaw. If new cases are added (e.g. :z), foo will raise an error to bring our attention to it, and add it as a new case. But nothing stops you from forgetting to update the test to test for the new behaviour for :z. Now I know that's mainly the job of code coverage tools, and we do use one, but just not to such a strict level that single-line gaps will blow up. Plus this is kind of a learning exercise, anyway.
So I amended my test:
test "foo handles all enum cases" do
remaining_cases = enum_cases.to_set
tester = -> (arg) do
remaining_cases.delete(arg)
foo(arg)
end
assert_equal tester.call(:a), 1
assert_equal tester.call(:b), 2
assert_equal tester.call(:c), 3
assert_raises NotImplementedError do
tester.call(:d)
end
assert_empty remaining_cases, "Not all cases were tested! Remaining: #{remaining_cases}"
end
This works great, however it's got 2 responsibilities, and it's a pattern I end up copy/pasting (I have multiple functions to test like this):
Perform the actual testing of foo
Do book keeping to ensure all params were exhausitvely checked.
I would like to make this test more focused by removing as much boiler plate as possible, and extracting it out to a place where it can easily be reused.
Attempted solution
In another language, I would just extract a simple test helper:
class ExhaustivityChecker
def initialize(all_values, proc)
#remaining_values = all_values.to_set
#proc = proc
end
def run(arg, allow_invalid_args: false)
assert #remaining_values.include?(arg) unless allow_invalid_args
#remaining_values.delete(arg)
#proc.call(arg)
end
def assert_all_values_checked
assert_empty #remaining_values, "Not all values were tested! Remaining: #{#remaining_values}"
end
end
Which I could easily use like:
test "foo handles all enum cases" do
tester = ExhaustivityChecker.new(enum_cases, -> (arg) { foo(arg) })
assert_equal tester.run(:a), 1
assert_equal tester.run(:b), 2
assert_equal tester.run(:c), 3
assert_raises NotImplementedError do
tester.run(:d, allow_invalid_args: true)
end
tester.assert_all_values_checked
end
I could then reuse this class in other tests, just by passing it different all_values and proc arguments, and remembering to call assert_all_values_checked.
Issue
However, this breaks because I can't call assert and assert_empty from a class that isn't a subclass of ActionView::TestCase. Is it possible to subclass/include some class/module to gain access to these methods?
enum_cases must be kept up to date when the production logic changes violating the DRY principle. This makes it more likely for there to be a mistake. Furthermore it is test code living in production, another red flag.
We can solve this by refactoring the case into a Hash lookup making it data driven. And also giving it a name describing what it's associated with and what it does, these are "handlers". I've also turned it into a method call making it easier to access and which will bear fruit later.
def foo_handlers
{
a: 1,
b: 2,
c: 3
}.freeze
end
def foo(input)
foo_handlers.fetch(input)
rescue KeyError
raise NotImplementedError, "Unhandled new case: #{input}"
end
Hash#fetch is used to raise a KeyError if the input is not found.
Then we can write a data driven test by looping through, not foo_handlers, but a seemingly redundant expected Hash defined in the tests.
class FooHelperTests < ActionView::TestCase
test "foo handles all expected inputs" do
expected = {
a: 1,
b: 2,
c: 3
}.freeze
# Verify expect has all the cases.
assert_equal expect.keys.sort, foo_handlers.keys.sort
# Drive the test with the expected results, not with the production data.
expected.keys do |key|
# Again, using `fetch` to get a clear KeyError rather than nil.
assert_equal foo(key), expected.fetch(value)
end
end
# Simplify the tests by separating happy path from error path.
test "foo raises NotImplementedError if the input is not handled" do
assert_raises NotImplementedError do
# Use something that obviously does not exist to future proof the test.
foo(:does_not_exist)
end
end
end
The redundancy between expected and foo_handlers is by design. You still need to change the pairs in both places, there's no way around that, but now you'll always get a failure when foo_handlers changes but the tests do not.
When a new key/value pair is added to foo_handlers the test will fail.
If a key is missing from expected the test will fail.
If someone accidentally wipes out foo_handlers the test will fail.
If the values in foo_handlers are wrong, the test will fail.
If the logic of foo is broken, the test will fail.
Initially you're just going to copy foo_handlers into expected. After that it becomes a regression test testing that the code still works even after refactoring. Future changes will incrementally change foo_handlers and expected.
But wait, there's more! Code which is hard to test is probably hard to use. Conversely, code which is easy to test is easy to use. With a few more tweaks we can use this data-driven approach to make production code more flexible.
If we make foo_handlers an accessor with a default that comes from a method, not a constant, now we can change how foo behaves for individual objects. This may or may not be desirable for your particular implementation, but its in your toolbox.
class Thing
attr_accessor :foo_handlers
# This can use a constant, as long as the method call is canonical.
def default_foo_handlers
{
a: 1,
b: 2,
c: 3
}.freeze
end
def initialize
#foo_handlers = default_foo_handlers
end
def foo(input)
foo_handlers.fetch(input)
rescue KeyError
raise NotImplementedError, "Unhandled new case: #{input}"
end
end
Now individual objects can define their own handlers or change the values.
thing = Thing.new
puts thing.foo(:a) # 1
puts thing.foo(:b) # 2
thing.foo_handlers = { a: 23 }
puts thing.foo(:a) # 23
puts thing.foo(:b) # NotImplementedError
And, more importantly, a subclass can change their handlers. Here we add to the handlers using Hash#merge.
class Thing::More < Thing
def default_foo_handlers
super.merge(
d: 4,
e: 5
)
end
end
thing = Thing.new
more = Thing::More.new
puts more.foo(:d) # 4
puts thing.foo(:d) # NotImplementedError
If a key requires more than a simple value, use method names and call them with Object#public_send. Those methods can then be unit tested.
def foo_handlers
{
a: :handle_a,
b: :handle_b,
c: :handle_c
}.freeze
end
def foo(input)
public_send(foo_handlers.fetch(input), input)
rescue KeyError
raise NotImplementedError, "Unhandled new case: #{input}"
end
def handle_a(input)
...
end
def handle_b(input)
...
end
def handle_c(input)
...
end

Updating the record in two Active Record Callbacks

Ok, stumbled upon this weirdness. I have this in my user model.
after_create :assign_role, :subscribe_to_basic_plan
def assign_role
self.role = 1
self.save
end
def subscribe_to_basic_plan
self.customer_id = "hello"
self.save
end
(code is simplified for illustration purposes)
When I create my user and check it in the console I get role: 1, customer_id: nil. But!, if I remove saving from the first callback everything works fine.
after_create :assign_role, :subscribe_to_basic_plan
def assign_role
self.role = 1
end
def subscribe_to_basic_plan
self.customer_id = "hello"
self.save
end
produces role: 1, customer_id: "hello". So seems like it only reads the first .save in the callbacks. I would like to understand what is the exact behaviour and why. I spent a lot of time trying to pinpoint this and wouldn't want to stumble on something similar again.
EDIT:
Maybe this is helpful. When I use self.save! in subscribe_to_basic_plan I get an error and the record is not saved at all. Putting self.save! in the assign_role doesn't change anything, so the problem is definitely with the second .save.
This answer is theoretical since I'd need to see the full model code to be sure.
Most likely your first save in assign_role is failing for some reason. When it fails and returns falls that causes rails to skip all callbacks after it. Then your second callback never runs at all.
Possible solutions in my preferred order:
Don't use callbacks. Have your controller set those values before you save the model.
Use before_create so you aren't doing 3 saves of the exact same model in a row.
Combine your two callbacks into one callback with only one save.
Save using save(validate: false) in case it is failing on validation.

Rails CanCan, failing unless I have a Rails.logger.info --- why?

If I have this:
can [:manage], GroupMember do |group_member|
wall_member.try(:user_id) == current_user.id
Rails.logger.info 'XXXX'
end
CanCan works properly but if I remove the logger, it fails:
can [:manage], GroupMember do |group_member|
wall_member.try(:user_id) == current_user.id
end
Any ideas what's going on here with CanCan? or my code? :) thanks
From the fine manual:
If the conditions hash does not give you enough control over defining abilities, you can use a block along with any Ruby code you want.
can :update, Project do |project|
project.groups.include?(user.group)
end
If the block returns true then the user has that :update ability for that project, otherwise he will be denied access. The downside to using a block is that it cannot be used to generate conditions for database queries.
Your first block:
can [:manage], GroupMember do |group_member|
wall_member.try(:user_id) == current_user.id
Rails.logger.info 'XXXX'
end
Will always return a true value because Rails.logger.info 'XXXX' returns "XXXX\n" (info is just a wrapper for add and you have to read the source to see what add returns as it isn't very well documented). Without the Rails.logger.info call, the block returns just:
wall_member.try(:user_id) == current_user.id
and that must be false for you.

Bypassing validation with machinist

I am writing my specs on my model Thing which has a date field which should be after the date of creation, thus I use the validate_timeliness plugin like that
validate_date :date, :after Time.now
I want to be able to add some Things with an anterior date but validation fails. I want to bypass the validation when creating some Things with the machinist factory.
Any clue ?
Shouldn't your validation ensure that the date is after the created_at attribute?? Rather than Time.now???
You shouldn't be trying to use invalid data in your tests, what you probably should do instead is fudge the created at time.
#thing = Thing.make(:created_at => 1.day.ago)
The only reason to try and put a time in the past in your spec surely should be to test that the validation is indeed working ..
#thing = Thing.make_unsaved(:date => 1.day.ago)
#thing.should have(1).error_on(:date)
Is there a reason why you want to do this? What are you trying to test??
If you call your_obj.save with a Boolean parameter =true like this: some_obj.save!(true), than all validations would be skipped. This is probably the undocumented ActiveRecord feature that is widely used in my company :)
Hmm, there's no straightforward way to do with Machinist itself. But you can try to trick it ... in spec/spec_helper, redefine the Thing model before the Machinist blueprints are loaded.
class Thing
def before_validation
self.date = 1.hour.from_now
end
end
You can catch the exception thrown by the validation. If you require the following code in your spec_helper after requiring machinist. To use it you can add a false as the first argument to #make.
module Machinist
module ActiveRecordExtensions
module ClassMethods
def make_with_skip_validation(*args, &block)
validate = !(!args.pop if ( (args.first == true) || (args.first == false) ))
begin
make_without_skip_validation(*args, &block)
rescue ActiveRecord::RecordInvalid => invalid
if validate
raise invalid
else
invalid.record.save(false)
end
end
end
alias_method_chain :make, :skip_validation
end
end
end

Resources