I have a class with following transaction:
# frozen_string_literal: true
class InactivateEmployee
include ServiceResult
def call(id)
begin
ActiveRecord::Base.transaction do
employee = Employee.find(id)
employee.update(is_active: false)
if employee.tasks.any?
employee.tasks.delete_all
end
response(code: 204, value: employee)
rescue ActiveRecord::ActiveRecordError
raise ActiveRecord::Rollback
end
rescue ActiveRecord::Rollback => e
response(code: 422, errors: e)
end
end
end
where ServiceResult is:
# frozen_string_literal: true
# ServiceResult should be included in each Service Class to have a unified returned object from each service
ServiceResultResponse = Struct.new(:success?, :response_code, :errors, :value, keyword_init: true)
module ServiceResult
def response(code:, errors: nil, value: nil )
ServiceResultResponse.new(
success?: code.to_s[0] == '2',
response_code: code,
errors: errors,
value: value
)
end
end
Question 1:
Is this code ok? what could be improved?
Question 2
How to test this transaction with use of Rspec? how to simulate in my test that destroy_all raise and error? i tried sth like that - but it does not work....
before do
allow(ActiveRecord::Associations::CollectionAssociation).to receive(:delete_all).and_return(ActiveRecord::ActiveRecordError.new)
end
Question 1: Is this code ok? what could be improved?
First and foremost, call should not be determining response codes. That wields together making the call with a specific context. That's someone else's responsibility. For example, 422 seems inappropriate, the only possible errors here are not finding the Employee (404) or an internal error (500). In general if you're rescuing ActiveRecordError you could probably be rescuing something more specific.
Does this need to be an entire service object? It's not using a service. It's only acting on Employee. If it's a method of Employee it can be used on any existing Employee object.
class Employee
def deactivate!
# There's no need for the find to be inside the transaction.
transaction do
# Use update! so it will throw an exception if it fails.
update!(is_active: false)
# Don't check first, it's an extra query and a race condition.
tasks.delete_all
end
end
end
Something else is responsible for catching errors and determining response codes. Probably the controller. Generic errors like a database failure should be handled higher up, probably by a default template.
begin
employee = Employee.find(id)
employee.deactivate!
rescue ActiveRecord::RecordNotFound
render status: :not_found
end
render status: :no_content
In ServiceResult you're checking success with code.to_s[0] == '2', use math or a Range instead. The caller should not be doing that at all, but it does because you have a module returning a Struct which can't do anything for itself.
ServiceResult should a class with a success? method. It's more flexible, more obvious what's happening, and doesn't pollute the caller's namespace.
class ServiceResult
# This makes it act like a Model.
include ActiveModel::Model
# These will be accepted by `new`
# You had "errors" but it takes a single error.
attr_accessor :code, :error, :value
def success?
(200...300).include?(code)
end
end
result = ServiceResult.new(code: 204, error: e)
puts "Huzzah!" if result.success?
I question if it's needed at all. It seems to be usurping the functionality of render. Is it an artifact of InactivateEmployee trying to do too much and having to pass its interpretation of what happened around?
Question 2 How to test this transaction with use of Rspec? how to simulate in my test that destroy_all raise and error?
Now that you're not doing too much in a single method, it's much simpler.
describe '#deactivate!' do
context 'with an active employee' do
# I'm assuming you're using FactoryBot.
let(:employee) { create(:employee, is_active: true) }
context 'when there is an error deleting tasks' do
before do
allow(employee.tasks).to receive(:delete_all)
# Exceptions are raised, not returned.
.and_raise(ActiveRecord::ActiveRecordError)
end
# I'm assuming there's an Employee#active?
it 'remains active' do
# same as `expect(employee.active?).to be true` with better diagnostics.
expect(employee).to be_active
end
end
end
end
Related
I got a method to update the person by id:
def update_person(id)
handle_exceptions do
person = Person.find(id)
#...other
end
end
When this id doesn't exist, the handle_exception should be called. But how could I test it? The test I wrote is:
context 'not found the proposals' do
subject {controller.send(:update_person, 3)}
before do
allow(Person).to receive(:find).and_raise(ActiveRecord::RecordNotFound)
allow(subject).to receive(:handle_exceptions)
end
it 'calls handle_exceptions' do
expect(subject).to have_received(:handle_exceptions)
end
end
But it not works, I got a failure said:
Failure/Error: expect(subject).to have_received(:handle_exceptions)
({:message=>"Not Found", :status=>:not_found}).handle_exceptions(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
The handle_exceptions method is
def handle_exceptions
yield
rescue ActiveRecord::RecordNotFound => e
flash[:warning] = 'no record found'
Rails.logger.error message: e.message, exception: e
#error_data = { message: 'no record found', status: :not_found }
end
The problem is that you are calling the method under test in the subject block.
subject {controller.send(:update_person, 3)}
This is actually called before the example runs and before the before block.
context 'not found the proposals' do
before do
allow(subject).to receive(:handle_exceptions)
end
it 'calls handle_exceptions' do
controller.send(:update_person, "NOT A VALID ID")
expect(subject).to have_received(:handle_exceptions)
end
end
But as far as tests go this one is not good. You're testing the implementation of update_person and not the actual behavior. And you're calling the method with update_person.send(:update_person, 3) presumably to test a private method.
You should instead test that your controller returns a 404 response code when try to update with an invalid id. Also why you insist on stubbing Person.find is a mystery since you can trigger the exception by just passing an invalid id. Only stub when you actually have to.
After couple days working, I realized the reason I'm confused about it is I didn't figure out about 'who called this function', and I think it's the most important thing to know before test it. For the method like this:
class User::Controller
def methodA
methodB
end
def methodB
// ...
end
The mistake that I made is I thought the methodB is called by methods, but it's not. It's called by the controller, and that's the reason that I can't make the test works. There's so many things need to learn, and I hope there's one day that I won't have a mistake like this and be able to help others.
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
For example, if I run test.update_attributes prop1: 'test', prop2: 'test2' when prop1 and prop2 have validations that prevent those values, test.prop1 will still be 'test' and test.prop2 will still be 'test2'. Why is this happening and how can I fix it?
According to the Rails docs for update_attributes, it's an alias of update. Its source is as follows:
# File activerecord/lib/active_record/persistence.rb, line 247
def update(attributes)
# The following transaction covers any possible database side-effects of the
# attributes assignment. For example, setting the IDs of a child collection.
with_transaction_returning_status do
assign_attributes(attributes)
save
end
end
So, it's wrapped in a DB transaction which is why the rollback happens. However, let's check out assign_attributes. According to its source:
# File activerecord/lib/active_record/attribute_assignment.rb, line 23
def assign_attributes(new_attributes)
...
_assign_attribute(k, v)
...
end
That is defined as:
# File activerecord/lib/active_record/attribute_assignment.rb, line 53
def _assign_attribute(k, v)
public_send("#{k}=", v)
rescue NoMethodError
if respond_to?("#{k}=")
raise
else
raise UnknownAttributeError.new(self, k)
end
end
So, when you call test.update_attributes prop1: 'test', prop2: 'test', it basically boils down to:
test.prop1 = 'test'
test.prop2 = 'test'
test.save
If save fails the validations, our in-memory copy of test still has the modified prop1 and prop2 values. Hence, we need to use test.reload and the issue is resolved (i.e. our DB and in-memory versions are both unchanged).
tl;dr Use test.reload after the failed update_attributes call.
Try wrapping it in an if-statement:
if test.update(test_params)
# your code here
else
# your code here
end
This is working as designed. For example the update controller method usually looks like this:
def update
#test = Test.find(params[:id])
if #test.update(test_attributes)
# redirect to index with success messsage
else
render :edit
end
private
def test_attributes
# params.require here
end
end
The render :edit will then re-display the form with an error message and the incorrect values filled in for the user to correct. So you actually do want the incorrect values to be available in the model instance.
Scenario
Have a race case where concurrency can cause a duplicate key error. Take for example:
def before_create_customer_by_external_id
end
def create_customer_from_external_id(external_id = nil)
#customer = current_account.customers.create!(external_id: external_id || #external_id)
end
def set_new_or_old_customer_by_external_id
if #customer.blank?
before_create_customer_by_external_id
create_customer_from_external_id
end
rescue ActiveRecord::RecordInvalid => e
raise e unless Customer.external_id_exception?(e)
#customer = current_account.customers.find_by_external_id(#external_id)
end
The Test
Now, to test the race case (based on the answer to Simulating race conditions in RSpec unit tests) we just need to monkey patch before_create_customer_by_external_id to call create_customer_from_external_id.
The Question
How can you do this without overriding the whole class and getting a "method not found" error?
After some digging, I came up with the following solution:
context 'with race condition' do
it 'should hit race case and do what is expected' do
ControllerToOverride.class_eval do
def before_create_new_customer_by_external_id
create_customer_from_external_id
end
end
# ...expect...
ControllerToOverride.class_eval do
undef before_create_new_customer_by_external_id
end
end
end
I verified that it was hitting the race case by using a code coverage tool and debug statements.
Happy to know if there's a cleaner way here.
Edit 2020-04-24
Per the comment, we should undef this method so it doesn't affect subsequent tests. Ref: https://medium.com/#scottradcliff/undefining-methods-in-ruby-eb7fba21f63f
I did not verify this, as I no longer have this test suite. Please let me know if it does/does not work.
A step on from monkey patching the class is to create an anonymous subclass:
context "with race condition" do
controller(ControllerToOverride) do
def before_create_customer_by_external_id
end
end
it "should deal with it " do
routes.draw { # define routes here }
...
end
end
This is not so very different to your solution but keeps the monkeypatch isolated to that context block.
You may not need the custom routes block - rspec sets up some dummy routes for the rest methods (edit, show, index etc)
If this context is inside a describe ControllerToOverride block then the argument to controller is optional, unless you have turned off config.infer_base_class_for_anonymous_controllers
I'm using the gem Responders but I'm not able to show errors that I create on my models using erros.add(:base, 'Error message').
On my controller, before the respond_with #app, I debugged the #app object and it has errors #app.errors.any? returns true
On my view, when I check the flash and #app objects, none has the error
App controller
# app_controllers.rb
def destroy
#app = current_company.apps.find(params[:id])
#app.destroy
respond_with #app
end
App model
# app.rb
before_destroy :destroy_on_riak
# ...
def destroy_on_riak
# SOME CODE HERE
rescue Exception => e
errors.add(:base, I18n.t("models.app.could_not_destroy", :message => e.message))
return false
end
App view
# apps.html.haml
-flash.each do |name, msg|
%div{:class => "flash #{name}"}
=content_tag :p, msg if msg.is_a?(String)
This is the #app object before the #app.destroy
"#<ActiveModel::Errors:0x00000004fa0510 #base=#<App id: 34, ...>, #messages={}>"
This is the #app object after the #app.destroy
"#<ActiveModel::Errors:0x00000004fa0510 #base=#<App id: 34, ...>, #messages={:base=>[\"Não foi possível excluir a aplicação: undefined method `get_or_new' for #<App:0x00000004f824c0>\"]}>"
I have removed what's inside the #base= for simplicity.
jefflunt is correct, when one calls #valid? it clears the errors array: see https://github.com/rails/rails/blob/4a19b3dea650351aa20d0cad64bf2d5608023a33/activemodel/lib/active_model/validations.rb#L333
The validators are designed to 100% determine the validity of your object, not when you add errors yourself for later use.
While ActiveRecord does override #valid?, it still calls super: https://github.com/rails/rails/blob/4a19b3dea650351aa20d0cad64bf2d5608023a33/activerecord/lib/active_record/validations.rb#L58
If you want to add errors and have them persist I recommend something like this:
def failures
#failures ||= []
end
validate :copy_failures_to_errors
def copy_failures_to_errors
#failures.each { |f| errors.add(*f) }
end
Then modify your rescue:
def destroy_on_riak
# SOME CODE HERE
rescue Exception => e
failures.push([:base, I18n.t("models.app.could_not_destroy", :message => e.message)])
return false
end
I know this seems convoluted and I know there are even examples online where people use or recommend errors.add(:base, ...), but it is not a safe way to store and retrieve errors for later.
Also, just a recommendation, but I advise you to rescue StandardError and not Exception unless you absolutely must. Out of memory errors and stuff like that are Exceptions, but every normal error you would ever want to rescue should inherit from StandardError. Just FYI.
The mystery seems to be either (a) you might not be calling the right method, or (b) the .errors hash doesn't contain what you think it contains.
Wrong method?
In your controller you're calling #app.destroy, but the method that adds the errors is called destroy_on_riak
Are you sure you don't mean to type this?
# app_controllers.rb
def destroy
#app = current_company.apps.find(params[:id])
#app.destroy_on_riak # <= The offending line?
respond_with #app
end
Or is there a before_destroy callback missing from your code sample that in turn calls destroy_on_riak? From the code included I don't see anywhere that the destroy_on_riak method ever gets called, so this is just a guess.
Unexpected contents of .errors hash?
If that's not the problem, then when #app.errors.any? is returning true, then at that point in the code print the contents of #app.errors to your log so you can see what's wrong.
I will give you some hints:
Hint 1
Your form may be calling valid? on the #app object. The valid? method clears the errors array on the instance.
It is not correct to use the errors array/construct outside of the validations context. But this is MHO.
Hint 2
According to Responders gem (which I have never used in the past), your locale just needs to have the correct configuration. Example:
flash:
actions:
create:
notice: "{resource_name} was successfully created"
update:
notice: "{resource_name} was successfully updated"
destroy:
notice: "{resource_name} was successfully destroyed"
alert: "{resource_name} could not be destroyed"
Does it?
I'd have to agree with #p.mastinopoulos on this. This should really be handled with the builtin validations. Sounds like you are in need of building a custom validator.
http://api.rubyonrails.org/classes/ActiveModel/Validations/ClassMethods.html#method-i-validate
Try replacing your before_destroy with a validate:
validate :destroy_on_riak, on: :destroy
Haven't tried this, but if it doesn't work, you may want to consider creating a custom Validator as referenced in the docs.