How to properly structure jobs that only call methods in rails - ruby-on-rails

I have several after_create methods (mostly to deliver emails or messages on the platform) and they call jobs which eventually call methods, but my code is starting to be structured like this which doesn’t really look nice
class Message < ApplicationRecord
after_create: :deliver_message_job
def deliver_message_job
DeliverMessageJob.perform_later self.id
end
def deliver_message
# logic to deliver message
end
end
and in the job, I just call the method
class DeliverMessageJob < ApplicationJob
queue_as :default
def perform(message_id)
Message.find(message_id).deliver_message
end
end
Is there a better way to go about structuring this?

If you want to continue using the callback, then you can pass a block that does what the Message#deliver_message_job method is doing for you so that you don't need to write out that method since it offers you little but a handle for the callback.
class Message < ApplicationRecord
after_create { |msg| DeliverMessageJob.perform_later(msg.id) }
# . . .
end
If you want to get rid of the Message#deliver_message method, then maybe you could put the logic for delivering the message into DeliverMessageJob#perform instead. This may make more sense semantically since it's purpose is clearly just to deliver a message.
class DeliverMessageJob < ApplicationJob
queue_as :default
def perform(message_id)
message = Message.find(message_id)
# Copy over/refactor the Message#deliver_message logic.
# Do some stuff with the message to deliver it . . .
end
end
It is worth asking yourself "Should a message know how to deliver itself?" The answer is probably either "No, it shouldn't" or "It's not important", in which case just let DeliverMessageJob worry about the details. This way, you have removed a couple of methods from the Message model and slimmed it out a bit, and made your classes neater and simpler.

Related

Rspec model callback

I have this model that my senior dev wrote:
class Thing < ActiveRecord::Base
after_commit on: :create do
SomeMobule.some_method(self)
end
end
I'm wondering how to test this callback.
I've known from the wise of the internet that you can do this:
(in model)
class Thing < ActiveRecord::Base
after_commit :do_something
def do_something
# doing stuff
end
end
(in spec)
it 'fires do_something after commit' do
expect(#instance).to receive(:do_something)
#instance.save
end
But I have no idea how to deal with this callback block.
Method name can be presented in symbol, easy, but what is another module's method name like in symbol? Or there's some other way to receive?
This might come from my lack of Ruby knowledge or for that matter general programming knowledge, and I have no idea even how to pursue the answer on the internet.
You can just test that SomeModule.some_method(self) is called.
let(:thing) { Thing.new }
it 'calls SomeModule.do_something after commit' do
expect(SomeModule).to receive(:do_something).with(thing)
thing.save
end
Which is fine if SomeModule.do_something is an application boundary such as a client to an external API.
If its not the test is very low value from a BDD standpoint - it only tests how the pieces are glued together - not the actually behaviour. A better test would be to test that the expected behaviour is triggered when you save the model.
# a really contrived example
it 'becomes magical when it is saved' do
expect do
thing.save
thing.reload
end.to change(thing, :magical).from(false).to(true)
end

Storing data in a job for use when it is performed

I need to store information at the time an Active Job is scheduled for use when it is later performed. I would like to save this information in the Active Job itself, but I'm not sure if that's possible.
Here's a simplified version of what I'm trying, which reproduces a bug I see:
class TestJob < ActiveJob::Base
queue_as :default
attr_reader :save_for_later
def initialize(info)
#save_for_later = info
end
def perform()
logger.info(#save_for_later)
end
end
class CollectionsController < ApplicationController
def schedule_test_job
TestJob.perform_later(Date.new)
end
end
When I call schedule_test_job in the Collections controller, I get an error:
undefined method `map' for nil:NilClass
and the perform action is not called.
I'm assuming I need to persist the information I'm trying to save elsewhere in my database, but I'd like to know if there is a proper way to accomplish what I'm doing here. I also don't understand where the error thrown is coming from.
Actually all the job parameters are passed to perform, so you have to write:
class TestJob < ActiveJob::Base
def perform(date)
logger.info(date)
end
end
TestJob.perform_later(Date.new)
The second problem is that Date is not AFAIK serializable by ActiveJob. But you can easily pass a string and then parse the date ;)

Ruby Metaprogramming Q: Calling an external class method on after_save

I have the following classes:
class AwardBase
class AwardOne < AwardBase
class Post < ActiveRecord::Base
The Post is an ActiveRecord, and the Award has a can_award? class method which takes a post object and checks to see if it meets some criteria. If yes, it updates post.owner.awards.
I know I can do this using an Observer pattern (I tested it and the code works fine). However, that requires me to add additional code to the model. I'd like not to touch the model at all if possible. What I'd like to do is run the Award checks like this (the trigger will be invoked at class load time):
class AwardOne < AwardBase
trigger :post, :after_save
def self.can_award?(post)
...
end
end
The intention with the above code is that it should automatically add AwardOne.can_award? to Post's after_save method
So essentially what I'm trying to do is to get the trigger call be equivalent to:
class Post < ActiveRecord::Base
after_save AwardOne.can_award?(self)
...
end
which is basically:
class Post < ActiveRecord::Base
after_save :check_award
def check_award
AwardOne.can_award?(self)
end
end
How can I do this without modifying the Post class?
Here's what I've done (which does not appear to work):
class AwardBase
def self.trigger (klass, active_record_event)
model_class = klass.to_class
this = self
model_class.instance_eval do
def award_callback
this.can_award?(self)
end
end
model_class.class_eval do
self.send(active_record_event, :award_callback)
end
end
def self.can_award? (model)
raise NotImplementedError
end
end
The above code fails with the error:
NameError (undefined local variable or method `award_callback' for #<Post:0x002b57c04d52e0>):
You should think about why you want to do it this way. I would argue it is even worse than using the observer pattern. You are violating the principle of least surprise (also called principle of least astonishment).
Imagine that this is a larger project and I come as a new developer to this project. I am debugging an issue where a Post does not save correctly.
Naturally, I will first go through the code of the model. I might even go through the code of the posts controller. Doing that there will be no indication that there is a second class involved in saving the Post. It would be much harder for me to figure out what the issue is since I would have no idea that the code from AwardOne is even involved.
In this case it would actually be most preferable to do this in the controller. It is the place that is easiest to debug and understand (since models have enough responsibilities already and are generally larger).
This is a common issue with metaprogramming. Most of the time it is better to avoid it precisely because of principle of least surprise. You will be glad you didn't use it a year from now when you get back to this code because of some issue you need to debug. You will forget what "clever" thing you have done. If you don't have a hell-of-a-good reason then just stick to the established conventions, they are there for a reason.
If nothing else then at least figure out a way to do this elegantly by declaring something in the Post model. For example by registering an awardable class method on ActiveRecord::Base. But the best approach would probably be doing it in the controller or via a service object. It is not the responsibility of AwardOne to handle how Post should be saved!
Because you are adding award_callback as class method. I bet it will be registered if you grep class methods.
So change your code like below. It should work fine.
model_class.class_eval do ## Changed to class_eval
def award_callback
this.can_award?(self)
end
end
Let me give a detailed example if it sounds confusing.
class Test
end
Test.instance_eval do
def class_fun
p "from class method "
end
end
Test.class_eval do
def instance_fun
p "from instance method "
end
end
Test.methods.grep /class_fun/
# => [:class_fun]
Test.instance_methods.grep /instance_fun/
# => [:instance_fun]
Test.class_fun
# => "from class method "
Test.new.instance_fun
# => "from instance method "

Adding callbacks for model classes in separate file (RoR)

I have a Message model class (which inherits from ActiveRecord::Base). For a particular deployment, I would like to have a separate file which modifies Message by adding a callback. So, instead of doing:
# app/models/message.rb
class Message < ActiveRecord::Base
before_save :foo
def foo
puts 'foo!'
end
end
I would like to be able to do:
# app/models/message.rb
class Message < ActiveRecord::Base
end
# config/initializers/fixes.rb
Message
class Message
before_save :foo
def foo
puts 'foo!'
end
end
Problem is, it works when I start the script/console, but when I start it using script/server it usually doesn't. That is the worst part, it isn't that it never works. Sometimes I start the server and it works, sometimes it doesn't, and that is without making any changes to the source.
I am restarting the server itself as (as far as I know) the initializers are run only once and don't get reloaded if modified.
I know the 'sometimes' works is very vague, but I have spent hours here without any luck. Perhaps someone has had a similar issue, or can come up with a different idea to add the callback.
Why not put those into a module and import it?
class Message < ActiveRecord::Base
include Message::Callbacks
end
In another file you can define whatever you like, such as message/callbacks.rb:
module Message::Callbacks
def self.included(base)
base.class_eval do
before_save :foo
end
end
def foo
# ...
end
end
The downside to this is it's more work to make the methods protected.
Why not use observers? (http://api.rubyonrails.org/classes/ActiveRecord/Observer.html)
For example, you'd do something like this:
class MessageObserver < ActiveRecord::Observer
def before_save(message)
puts 'you win at ruby!'
end
end

How would you test observers with rSpec in a Ruby on Rails application?

Suppose you have an ActiveRecord::Observer in one of your Ruby on Rails applications - how do you test this observer with rSpec?
You are on the right track, but I have run into a number of frustrating unexpected message errors when using rSpec, observers, and mock objects. When I am spec testing my model, I don't want to have to handle observer behavior in my message expectations.
In your example, there isn't a really good way to spec "set_status" on the model without knowledge of what the observer is going to do to it.
Therefore, I like to use the "No Peeping Toms" plugin. Given your code above and using the No Peeping Toms plugin, I would spec the model like this:
describe Person do
it "should set status correctly" do
#p = Person.new(:status => "foo")
#p.set_status("bar")
#p.save
#p.status.should eql("bar")
end
end
You can spec your model code without having to worry that there is an observer out there that is going to come in and clobber your value. You'd spec that separately in the person_observer_spec like this:
describe PersonObserver do
it "should clobber the status field" do
#p = mock_model(Person, :status => "foo")
#obs = PersonObserver.instance
#p.should_receive(:set_status).with("aha!")
#obs.after_save
end
end
If you REALLY REALLY want to test the coupled Model and Observer class, you can do it like this:
describe Person do
it "should register a status change with the person observer turned on" do
Person.with_observers(:person_observer) do
lambda { #p = Person.new; #p.save }.should change(#p, :status).to("aha!)
end
end
end
99% of the time, I'd rather spec test with the observers turned off. It's just easier that way.
Disclaimer: I've never actually done this on a production site, but it looks like a reasonable way would be to use mock objects, should_receive and friends, and invoke methods on the observer directly
Given the following model and observer:
class Person < ActiveRecord::Base
def set_status( new_status )
# do whatever
end
end
class PersonObserver < ActiveRecord::Observer
def after_save(person)
person.set_status("aha!")
end
end
I would write a spec like this (I ran it, and it passes)
describe PersonObserver do
before :each do
#person = stub_model(Person)
#observer = PersonObserver.instance
end
it "should invoke after_save on the observed object" do
#person.should_receive(:set_status).with("aha!")
#observer.after_save(#person)
end
end
no_peeping_toms is now a gem and can be found here: https://github.com/patmaddox/no-peeping-toms
If you want to test that the observer observes the correct model and receives the notification as expected, here is an example using RR.
your_model.rb:
class YourModel < ActiveRecord::Base
...
end
your_model_observer.rb:
class YourModelObserver < ActiveRecord::Observer
def after_create
...
end
def custom_notification
...
end
end
your_model_observer_spec.rb:
before do
#observer = YourModelObserver.instance
#model = YourModel.new
end
it "acts on the after_create notification"
mock(#observer).after_create(#model)
#model.save!
end
it "acts on the custom notification"
mock(#observer).custom_notification(#model)
#model.send(:notify, :custom_notification)
end

Resources