How to test uniqueness of Coupon/Promo-Codes? - ruby-on-rails

I have a Model PromoCode which has a .generate! method, that calls .generate which generates a String using SecureRandom.hex(5) and saves it to the database:
class PromoCode < ActiveRecord::Base
class << self
def generate
SecureRandom.hex 5
end
def generate!
return create! code: generate
end
end
end
Now I want to write a spec that test the uniqueness of the generated string. The .generate method should be called as long as a non existent PromoCode has been generated.
I'm not sure how to do this since I can't really stub out the .generate method to return fixed values (because then it would be stuck in an infinite loop).
This is the passing spec for the model so far:
describe PromoCode do
describe ".generate" do
it "should return a string with a length of 10" do
code = PromoCode.generate
code.should be_a String
code.length.should eql 10
end
end
describe ".generate!" do
it "generates and returns a promocode" do
expect {
#promo = PromoCode.generate!
}.to change { PromoCode.count }.from(0).to(1)
#promo.code.should_not be_nil
#promo.code.length.should eql 10
end
it "generates a uniq promocode" do
end
end
end
Any directions appreciated.

Rspec's and_return method allows you to specify multiple return values that will be cycled through
For example you could write
PromoCode.stub(:generate).and_return('badcode1', 'badcode2', 'goodcode')
Which will cause the first call to generate to return 'badcode1', the second 'badcode2' etc... You can then check that the returned promocode was created with the correct code.
If you want to be race condition proof you'll want a database uniqueness constraint, so your code might actually want to be
def generate!
create!(...)
rescue ActiveRecord::RecordNotUnique
retry
end
In your spec you would stub the create! method to raise the first time but return a value the second time

And what about something like: create a PromoCode, save the result, and try to create a new PromoCode with the code of the previous PromoCode object:
it "should reject duplicate promocode" do
#promo = PromoCode.generate!
duplicate_promo = PromoCode.new(:code => #promo.code)
duplicate_promo.should_not be_valid
end
Also, this is model level, I am assuming you have a key in the database that will save you from race conditions...

If you saves the promocode in database you would have added validations there in the model for uniq promocode. So you can test the same in rspec too.
Like this,
it { should validate_uniqueness_of(:promocode) }

This answer is based on your comment:
I need to make sure that generate! generates a code - no matter what,
until a unique code has been generated.
I feel like you might have a hard time testing this correctly. Unit testing "no matter what" situations with indefinite loops can be a bit of a tricky subject.
I'm not sure how to do this since I can't really stub out the .generate method to return fixed values (because then it would be stuck in an infinite loop).
One possibility to consider might be if instead of doing either one or the other, you tried both? (That is, find a way make it return a fixed number under certain circumstances, and eventually trigger the actual random number. An instance variable counter might help; set it to a random number, count it down, when it's greater than zero return the fixed number, or something along those lines). This still doesn't feel like a perfect test, though, or even a very good one for that matter.
It might be worth looking more into means of generating similar strings with high probability of them being unique, and having some mathematical proof of it being that way. I'm not saying this is the most practical idea either, but if you really need to prove (as you're trying to do with tests), it might be the more probably solution.

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).

rails minitest stubbing to assert alternate method

A method replies upon two distinct APIs for geolocation, the second serving as a backup:
def admin_create
#user_object = User.create!(user_params)
set_area(#user_object)
end
def set_area(resource)
do_geocode_lookup(resource)
if !resource.lon
do_alternate_geocode_lookup(resource)
end
end
Finding a data set that returns an empty lon/lat set is challenging (and orthodoxy pushes one to write the test first),
so.. Is there a way to stub the test so that
do_geocode_lookup returns empty lon lat values
do_alternate_geocode_lookup(resource) method gets invoked? and thus tested?
sign_in operator_user
post admin_create_users_url, params: {user: { [...] } }
assert[...]
Using Mocha you can stub out a method so that any instance of an object returns a specified value.
For example....
Account.any_instance.stubs(:reviews_enabled).returns(:true)
It also allows you test that a method was called...
Account.any_instance.expects(:review_started)
The fact you are altering the passed in resource instead of returning something makes things a bit trickier, but I'd be tempted to have do_geocode_lookup return true if it finds something, and false otherwise, to make things a little easier to test.
def set_area(resource)
found = do_geocode_lookup(resource)
unless found
found = do_alternate_geocode_lookup(resource)
end
found
end
Then you could do something like this in your test...
Geocoder.any_instance.stubs(:do_geocode_lookup).returns(:false)
Geocoder.any_instance.expects(:do_alternate_geocode_lookup)
result = sut.set_area(user)
If the expected method isn't called, you get an "expected method wasn't called" error. You can get far more sophisticated with the stubs and expects, but in this case you shouldn't have to.

Rails: How to write model spec for this method?

I've just started to take on my first model spec task at work. After writing a lot of feature specs, I find it hard to get into the different perspective of writing model specs (not taking the context into consideration). I'll take a method of the Order model as an example, to explain which difficulties I am experiencing:
def update_order_prices
self.shipping_price_cents = SHIPPING_PRICE_CENTS unless shipping_price_cents
return if order_lines.empty?
self.total_price_cents = calculate_order_price
self.total_line_items_price_cents = calculate_total_order_line_price
self.total_tax_cents = calculate_tax_amount
end
EDIT TL;DR
I am totally happy with an answer that simply writes me a spec for this method. The rest of the post just shows what I tried so far but is not necessary to answer this question.
First approach:
At first I didn't know what to test for. I tried to find out when and where the method was called and to find a scenario where I would know what the attributes that are touched in this method should be equal to. Put short, I spent a lot of time trying to understand the context. Then a coworker said that I should test methods in model specs self-contained, independent from the context. I should just make sure I identify all cases. So for this method that would be:
it sets shipping price cents to default (if not done already)
it returns early if order_lines is empty
it sets values if order_line is set
Current approach:
I tried writing the tests for these points but still questions arise:
Test 1
it 'sets shipping price cents to default (if not done already)' do
order.shipping_price_cents = nil
order.update_order_prices
expect(order.shipping_price_cents).to eq(Order::SHIPPING_PRICE_CENTS)
end
I am confident I got this one right, but feel free to prove me wrong. I set shipping_price_cents to nil to trigger the code that sets it, call the tested method on the cents to be equal to the default value as defined in the model.
Test 2
it 'returns early if order_lines is empty' do
expect(order.update_order_prices).to eq(nil)
end
So here I want to test that the method returns early when there is no object in the order_lines association. I didn't have a clue how to do that so I went into the console, took an order, removed the order_lines associated with it, and called the method to see what would be returned.
2.3.1 :011 > o.order_lines
=> #<ActiveRecord::Associations::CollectionProxy []>
2.3.1 :012 > o.update_order_prices
=> nil
Then did the same for an order with associated order_line:
2.3.1 :017 > o.update_order_prices
=> 1661
So I tested for 'nil' to be returned. But it doesn't feel like I am testing the right thing.
Test 3
it 'sets (the correct?) values if order_line is set' do
order_line = create(:order_line, product: product)
order = create(:order, order_lines: [order_line])
order.update_order_prices
expect(order.total_price_cents).to eq(order.calculate_order_price)
expect(order.total_line_items_price_cents).to eq(order.calculate_order_line_price)
expect(order.total_tax_cents).to eq(order.calculate_tax_amount)
end
I simply test that the attributes equal what they are set to, without using actual values, as I shouldn't look outside. If I wanted to test for an absolute value, I would have to investigate outside of this function which then wouldn't test the method but also status of the Order object etc.?
Running the tests
Failures:
1) Order Methods: #update_order_prices sets (the correct?) values if order_line is set
Failure/Error: expect(order.total_price_cents).to eq(order.calculate_order_price)
NoMethodError:
private method `calculate_order_price' called for #<Order:0x007ff9ee643df0>
Did you mean? update_order_prices
So, the first two tests passed, the third one didn't. At this point I feel a bit lost and would love hear how some experienced developers would write this seemingly simple test.
Thanks
I guess you have to spec against the exact values you are expecting after update_order_prices.
Let's say you set up your order and order lines to have a total price of 10 euros then I'd add the following expectation
expect(order.total_price_cents).to eq(1000)
Same for the other methods. Generally I try to test against specific values. Also as you are relying on the result of a private method you only care about the result.

Multiple should statements in one rspec it clause - bad idea?

Here's my rspec test:
it "can release an idea" do
james.claim(si_title)
james.release(si_title)
james.ideas.size.should eq 0
si_title.status.should eq "available"
end
Are the two should lines at the end a really bad idea? I read somewhere that you should only test for one thing per it block, but it seems silly to make a whole test just to make sure that the title status changes (the same function does both things in my code).
My interpretation of this isn't so much that there should be precisely one assertion / call to should per spec, but that there should only be one bit of behaviour tested per spec, so for example
it 'should do foo and bar' do
subject.do_foo.should be_true
subject.do_bar.should be_true
end
is bad - you're speccing 2 different behaviours at the same time.
on the other hand if your 2 assertions are just verifying different aspects of one thing then I'm ok with that, for example
it 'should return a prime integer' do
result = subject.do_x
result.should be_a(Integer)
result.foo.should be_prime
end
For me it wouldn't make too much sense to have one spec that checks that it returns an integer and a separate one that it returns a prime.
Of course in this case, the be_prime matcher could easily do both of these checks - perhaps a good rule of thumb is that multiple assertions are ok if you could sensibly reduce them to 1 with a custom matcher (whether actually doing this is actually worthwhile probably depends on your situation)
In your particular case, it could be argued that there are 2 behaviours at play - one is changing the status and other is mutating the ideas collection. I would reword your specs to say what the release method should do -
it 'should change the status to available'
it 'should remove the idea from the claimants ideas'
At the moment those things always happen simultaneously but I would argue they are separate behaviours - you could easily imagine a system where multiple people can claim/release an idea and the status only change when the last person releases the idea.
I have the same problem...
It ought to be one should per it (my boss says) by it makes testing time consuming and, as you said, silly.
Tests require good sense and flexibility or they might end up enslaving you.
Anyway, I agree with your test.
My policy is to always consider multiple assertions to be a sign of a potential problem and worth a second thought, but not necessarily wrong. Probably 1/3 of the specs I write end up having multiple assertions in them for one reason or another.
One of the problems with having multiple assertions is that when one fails, you can't see if the other one passed or not. This can sometimes be worked around by building an array of results and asserting the value of the array.
In the case you are asking about, I don't think that multiple assertions are a problem, but I do see something else that seems an issue to me. It looks like your spec may be overly coupled.
I think that you're trying to assert the behavior of whatever kind of a thing james is, but the test is also depending on behavior of si_title in order to tell us what was done to it (by way of its eventual #status value). What I would usually do instead is make si_title a test-double and use #should_receive to directly specify the messages that it should expect.
I think Frederick Cheung gave a very good answer (+1) especially the why, but I'd also like to give a comparison bit of code for you to look at which uses the its, lets, before and context syntax:
context "Given a si_title" do
let(:james) { James.new } # or whatever
before do
james.claim(si_title)
james.release(si_title)
end
context "That is valid" do
let(:si_title) { Si_title.new } # or whatever
describe "James' ideas" do
subject { james.ideas }
its(:size) { should == 0 }
its(:whatever) { should == "Whatever" }
end
describe "Si title" do
subject { si_title }
its(:status) { should == "available" }
end
end
context "That is invalid" do
# stuff here
end
end
I would even go further and make the expected values lets and then make the examples shared_examples, so that they could then be use to check the different aspects (null arguments, invalid arguments, an incorrect object… ), but I believe that this is a far better way to spec your intentions and still cut down on any repetition. Taking the example from Frederick's answer:
it 'should return a prime integer' do
result = subject.do_x
result.should be_a(Integer)
result.foo.should be_prime
end
Using RSpec's syntax to full effect you get this:
let(:result) { 1 }
subject{ result }
its(:do_x) { should be_a(Integer) }
its(:foo) { should be_prime }
Which means you can check multiple aspects of a subject.

Rails assert that form is valid

What's the best practices way to test that a model is valid in rails?
For example, if I have a User model that validates the uniqueness of an email_address property, how do I check that posting the form returned an error (or better yet, specifically returned an error for that field).
I feel like this should be something obvious, but as I'm quickly finding out, I still don't quite have the vocabulary required to effectively google ruby questions.
The easiest way would probably be:
class UserEmailAddressDuplicateTest < ActiveSupport::TestCase
def setup
#email = "test#example.org"
#user1, #user2 = User.create(:email => #email), User.new(:email => #email)
end
def test_user_should_not_be_valid_given_duplicate_email_addresses
assert !#user2.valid?
end
def test_user_should_produce_error_for_duplicate_email_address
# Test for the default error message.
assert_equal "has already been taken", #user2.errors.on(:email)
end
end
Of course it's possible that you don't want to create a separate test case for this behaviour, in which case you could duplicate the logic in the setup method and include it in both tests (or put it in a private method).
Alternatively you could store the first (reference) user in a fixture such as fixtures/users.yml, and simply instantiate a new user with a duplicate address in each test.
Refactor as you see fit!
http://thoughtbot.com/projects/shoulda/
Shoulda includes macros for testing things like validators along with many other things. Worth checking out for TDD.
errors.on is what you want
http://api.rubyonrails.org/classes/ActiveRecord/Errors.html#M002496
#obj.errors.on(:email) will return nil if field is valid, and the error messages either in a String or Array of Strings if there are one or more errors.
Testing the model via unit tests is, of course, step one. However, that doesn't necessarily guarantee that the user will get the feedback they need.
Section 4 of the Rails Guide on Testing has a lot of good information on functional testing (i.e. testing controllers and views). You have a couple of basic options here: check that the flash has a message in it about the error, or use assert_select to find the actual HTML elements that should have been generated in case of an error. The latter is really the only way to test that the user will actually get the message.

Resources