I have an ActiveRecord Transaction that I am trying to test, but I'm having some difficulty understanding exactly how to write my Rspec test. Here is an example of what I'm going for:
it "does not change the model count" do
expect(Model.count).to be(0)
expect {
MyClass.my_method(arg1, arg2)
}.to raise_error
expect(Model.count).to be(0)
end
While my_method is running, there are several objects being saved to the DB. I want to raise an exception while this method is running in order to invoke the transaction rollback.
What is the best way to go about raising this exception?
EDIT:
I appreciate everyone taking the time to give me input. My goal was to test a transaction rollback. I was calling two different methods inside a transaction, and I wanted to make sure that if an error arose in the second method the data written to the DB in the first method did not persist.
I got this to work as I needed, though I recognize it's contrived.
class MyClass
def self.save_my_values(arg1, arg2)
parsed = parse(arg1)
ActiveRecord::Base.transaction do
some_method(parsed, arg2)
my_method(parsed, arg2)
end
end
end
And here is the code in the spec:
context "when there is an error" do
before do
allow(MyClass).to receive(:my_method).and_raise(StandardError)
end
it "does not change the model count" do
expect(Model.count).to be(0)
expect {
MyClass.my_method(arg1, arg2)
}.to raise_error(StandardError)
expect(Model.count).to be(0)
end
end
I'll be refining it further, but this was the starting point I was looking for. Thank you again!
Related
I want to test my send method logic in my Client class. I have this so far in my client_spec file. How can i raise and error and test that rescue is called and the error is logged in my spec file.
I am new to rspec but I believe i can use a test double for logging instead of calling actual logger.
client_spec.rb
describe Client do
describe '#send' do
let (:subject) {Client.new}
it 'raises and logs the exception'
//how to test raising and logging of the error
end
end
end
client.rb
class Client
include HTTParty
base_uri "https://www.example.com"
format :json
def send
begin
response = HTTParty.get(url)
if response.successful?
response
else
raise 'invalid response'
end
rescue HTTParty::Error => e
logger.warn(e.message)
end
end
end
yes, you can stub both HTTParty and logger, something like
let(:logger) { double('Logger') }
let(:error) { HTTParty::Error.new('foo') }
before do
allow(Logger).to receive(:new) { logger }
end
(Not sure what kind of logger are you using)
and then you can tell HTTParty to raise the kind of error you're expecting, like:
context 'when there is a HTTParty error' do
before do
allow(HTTParty).to receive(:get).and_raise(error)
end
it 'logs the error' do
expect(logger).to receive(:warn).with('foo')
subject.send
end
end
Ok, so, the expect before the send is a way to test if the tested method will "trigger" or perform additional operations, it's a way to say "when I execute foo, I expect that "this" happened". There is a way to declare way around, like:
subject.send
expect(logger).to have_received(:warn).with('foo')
But I think this way is newer than the one I proposed, I just got really used to use the proposed one.
About mocks and stubs, yeah, you don't test them because those objects are out of the scope of the class you're testing on. So, Logger should have its own set of tests, the same for HTTParty, so, you "simulate" their behaviors in order to test your class, that way you'll remove the dependency between the test and other libraries (or classes).
When your unit test is done, then you can move to an integration test, and test (sorry for the redundancy) that the whole stack (or endpoint or controller or "flow") is doing what you're expecting to do.
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.
I want to test that a class receives a class-method call in RSpec:
describe MyObject do
it "should create a new user" do
expect(User).to receive(:new)
MyObject.new.doit
end
end
class MyObject
def doit
u = User.new
u.save
end
end
The problem is that the expectation does not halt execution. It simply stubs the class method .doit and continues execution.
The effect of the expectation is to ensure that User.new returns nil. So when we get to the next line which is User.save it then fails because there is no user object to call .save on.
I would like execution to halt as soon as the RSpec expectation has been satisfied - how can I do that?
nb
This is just an illustrative example - while an expect to change would work for User.new, it's not this actual code that I need to test
There is a great method for this and_call_original:
expect(User).to receive(:new).and_call_original
based on your test description, you're testing that a record was created, in those cases I would suggest you to do this:
expect {
MyObject.new.doit
}.to change{User.count}
or if you want to make sure it only created one:
expect {
MyObject.new.doit
}.to change{User.count}.by(1)
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 have a class which performs several database operations, and I want to write a unit test which verifies that these operations are all performed within a transaction. What's a nice clean way to do that?
Here's some sample code illustrating the class I'm testing:
class StructureUpdater
def initialize(structure)
#structure = structure
end
def update_structure
SeAccount.transaction do
delete_existing_statistics
delete_existing_structure
add_campaigns
# ... etc
end
end
private
def delete_existing_statistics
# ...
end
def delete_existing_structure
# ...
end
def add_campaigns
# ...
end
end
Rspec lets you assert that data has changed in the scope of a particular block.
it "should delete existing statistics" do
lambda do
#structure_updater.update_structure
end.should change(SeAccount, :count).by(3)
end
...or some such depending on what your schema looks like, etc. Not sure what exactly is going on in delete_existing_statistics so modify the change clause accordingly.
EDIT: Didn't understand the question at first, my apologies. You could try asserting the following to make sure these calls occur in a given order (again, using RSpec):
EDIT: You can't assert an expectation against a transaction in a test that has expectations for calls within that transaction. The closest I could come up with off the cuff was:
describe StructureUpdater do
before(:each) do
#structure_updater = StructureUpdater.new(Structure.new)
end
it "should update the model within a Transaction" do
SeAccount.should_receive(:transaction)
#structure_updater.update_structure
end
it "should do these other things" do
#structure_updater.should_receive(:delete_existing_statistics).ordered
#structure_updater.should_receive(:delete_existing_structure).ordered
#structure_updater.should_receive(:add_campaigns).ordered
#structure_updater.update_structure
end
end
ONE MORE TRY: Another minor hack would be to force one of the later method calls in the transaction block to raise, and assert that nothing has changed in the DB. For instance, assuming Statistic is a model, and delete_existing_statistics would change the count of Statistic in the DB, you could know that call occurred in a transaction if an exception thrown later in the transaction rolled back that change. Something like:
it "should happen in a transaction" do
#structure_updater.stub!(:add_campaigns).and_raise
lambda {#structure_updater.update_structure}.should_not change(Statistic, :count)
end