In app/models/car.rb the class method stock looks in part like this:
def self.stock
raise Exception, "Property amount not set" if Property.get(:amount).nil?
...
end
This Property is accessable by the user through all CRUD operations.
I now want to test that if that Property is truly deleted an standard expection should be thrown. Therefore i created in my rspec model the following example group
describe '.stock' do
describe 'if property is not set' do
before(:all) {Property.where(name: 'amount').map(&:destroy)}
it 'raises an exception' do
expect{Car.stock}.to raise_error (Exception)
end
after (:all) {Property.create! name: 'amount', value: 10}
end
describe 'if property is set' do
before (:all) do
create :car_listed
create :car_sold
end
it 'calculates the correct amount of sellable cars' do
amount = 10 - Car.where(state: "sold")
expect(Car.stock).to eq(amount)
end
end
end
I make sure all Properties with that name get deleted. In the it-block i then expect the exception to be trown. After the it-block i created the property again because other tests depend on it.
In the app are properties which are not going to change during tests. So database_cleaner does not truncate the properties table. It got set through a seeds file.
config.before(:suite) do
DatabaseCleaner.strategy = :truncation, {except: %w[properties]}
end
The test however fails with
Car
.stock
if property is set
calculates the correct amount of sellable cars
if property is not set
raises an exception (FAILED - 1)
Failures:
1) Car.stock if property is not set not set raises an exception
Failure/Error: expect{Car.stock}.to raise_error (Exception)
expected Exception but nothing was raised
# ./spec/models/car_spec.rb: `block (4 levels) in <top (required)>'
My question is now how do i have to delete this property properly ((:), so that my exception gets raised.
There are easier ways to test this that do not touch the database. Here's how to approach the problem using stubs:
describe '.stock' do
before do
allow(Property).to receive(:get).with(:amount).and_return(amount)
end
context 'when property amount is NOT set' do
let(:amount) { nil }
it 'raises an exception' do
expect { Car.stock }.to raise_error(Exception)
end
end
context 'when amount property is set' do
let(:amount) { 10 }
before do
create :car_listed
create :car_sold
end
it 'does NOT raise an exception' do
expect { Car.stock }.to_not raise_error(Exception)
end
it 'calculates the correct amount of sellable cars' do
expect(Car.stock).to eq(amount - 1)
end
end
NOTE: I'm unsure about that last test since you didn't include that code.
Related
I have a spec testing a model that looks like this:
RSpec.describe SomeModel, type: :model do
subject { described_class.new(test_amount: 99) }
describe 'validates a column' do
it 'does some validations' do
expect(subject).to validate_presence_of(:test_amount)
end
end
end
And a model that looks like this:
class SomeModel < ApplicationRecord
validates :test_amount, presence: true
end
And in the schema it's column looks like this with a not-null set:
t.integer "test_amount", default: 0, null: false
No matter what I do or where I put the code, test_amount is always nil when being tests and errors.
I've tried moving the test lines around, putting the subject in a before etc, but always the
database is throwing a non-null error and even if I raise in the model code
the test_amount value is not 99 it is nil. If I raise the test value in
a before like this:
before do
raise subject.test_amount
end
That does result in a 99, however if I remove this, it is always nil and throws an error when it gets to the expect part of the test.
What am I missing in order to get this test to work, I just cannot seem to get the test_amount to set to 99 when being tested in the actual test step.
The test always throws the error:
PG::NotNullViolation: ERROR: null value in column "test_amount" of relation "some_models" violates not-null constraint or similar, I have but in before-validations to check the value of test_amount and it does not get set.
Thanks for you help, I feel like there's something really basic I'm missing here.
First of all, verify that -
The record is persisted.
Once persisted, then check the attribute value.
Moreover, try moving this line inside the describe block -
subject { described_class.new(test_amount: 99) }
Any chance this is due to not running migrations in the test environment?
bundle exec rake db:prepare RAILS_ENV=test
# or for rack applications
bundle exec rake db:prepare RACK_ENV=test
Other than this I would think that because we aren't saving the record to the database. We wouldn't expect validations to be ran.
As per this documentation we are only expecting to run validations when Record#save or Record#save! has been called.
When running Record#new we are creating a new instance but not saving to our database.
Using the new method, an object can be instantiated without being saved:
When running Record#create we initialize the record and then save this to the database by calling Record#save.
You can write test cases for the presence validation with valid? and errors methods like below:
RSpec.describe SomeModel, type: :model do
subject { described_class.new(test_amount: test_amount) }
describe 'validates a column' do
context 'when valid test_amount'
let(:test_amount) { 99 }
it 'does not throw error' do
expect(subject.valid?).to eq(true)
expect(subject.errors[:test_amount].size).to eq(0)
end
end
context 'when invalid test_amount'
let(:test_amount) { nil }
it 'throws error' do
expect(subject.valid?).to eq(false)
expect(subject.errors[:test_amount].size).to eq(1)
end
end
end
end
Just learning rspec syntax and I noticed that this code works:
context "given a bad list of players" do
let(:bad_players) { {} }
it "fails to create given a bad player list" do
expect{ Team.new("Random", bad_players) }.to raise_error
end
end
But this code doesn't:
context "given a bad list of players" do
let(:bad_players) { {} }
it "fails to create given a bad player list" do
expect( Team.new("Random", bad_players) ).to raise_error
end
end
It gives me this error:
Team given a bad list of players fails to create given a bad player list
Failure/Error: expect( Team.new("Random", bad_players) ).to raise_error
Exception:
Exception
# ./lib/team.rb:6:in `initialize'
# ./spec/team_spec.rb:23:in `new'
# ./spec/team_spec.rb:23:in `block (3 levels) in <top (required)>'
My question is:
Why does this happen?
What is the difference between the former and later example exactly in ruby?
I am also looking for rules on when to use one over the other
One more example of the same but inverse results, where this code works:
it "has a list of players" do
expect(Team.new("Random").players).to be_kind_of Array
end
But this code fails
it "has a list of players" do
expect{ Team.new("Random").players }.to be_kind_of Array
end
Error I get in this case is:
Failure/Error: expect{ Team.new("Random").players }.to be_kind_of Array
expected #<Proc:0x007fbbbab29580#/Users/amiterandole/Documents/current/ruby_sandbox/tdd-ruby/spec/team_spec.rb:9> to be a kind of Array
# ./spec/team_spec.rb:9:in `block (2 levels) in <top (required)>'
The class I am testing looks like this:
class Team
attr_reader :name, :players
def initialize(name, players = [])
raise Exception unless players.is_a? Array
#name = name
#players = players
end
end
As has been mentioned:
expect(4).to eq(4)
This is specifically testing the value that you've sent in as the parameter to the method. When you're trying to test for raised errors when you do the same thing:
expect(raise "fail!").to raise_error
Your argument is evaluated immediately and that exception will be thrown and your test will blow up right there.
However, when you use a block (and this is basic ruby), the block contents isn't executed immediately - it's execution is determined by the method you're calling (in this case, the expect method handles when to execute your block):
expect{raise "fail!"}.to raise_error
We can look at an example method that might handle this behavior:
def expect(val=nil)
if block_given?
begin
yield
rescue
puts "Your block raised an error!"
end
else
puts "The value under test is #{val}"
end
end
You can see here that it's the expect method that is manually rescuing your error so that it can test whether or not errors are raised, etc. yield is a ruby method's way of executing whatever block was passed to the method.
In the first case, when you pass a block to expect, the execution of the block doesn't occur until it's time to evaluate the result, at which point the RSpec code can catch any error that are raised and check it against the expectation.
In the second case, the error is raised when the argument to expect is evaluated, so the expect code has no chance to get involved.
As for rules, you pass a block or a Proc if you're trying to test behavior (e.g. raising errors, changing some value). Otherwise, you pass a "conventional" argument, in which case the value of that argument is what is tested.
I am testing a scenario with CapyBara where I expect the record NOT to be valid.
Given "the customer is not an athlete" do
#customer = create(:athlete, :shop=> #shop, :first_name=>"Birglend", :last_name=>"Firglingham", :email=>"birglendfirglingham#gmail.com")
end
Then "I should not see that the customer is an athlete" do
expect(page).not_to have_css('.athlete-row')
end
But before it can get to the "Then", I get an "ActiveRecord::RecordNotSaved" exception. This is caused by a before_save callback that I have to check if the customer is an athlete. If they're not an athlete, the method returns false and I do not want the record to be saved.e.g.
before_save :check_athlete_status
def check_athlete_status
#return false unless self is an athlete
end
But I have a feeling this is not the correct way because this is an expected scenario and should not be throwing exceptions. How do I handle this?
You're trying to mix what a model test and a feature test do here.
There are a couple ways to test this.
I am going to drop the Given/Then syntax because I don't have any experience using that gem.
1) A model spec:
RSpec.describe Athlete, type: :model do
context 'with invalid status' do
let(:athlete) { FactoryGirl.build(:athlete, :shop=> #shop, :first_name=>"Birglend", :last_name=>"Firglingham", :email=>"birglendfirglingham#gmail.com") }
it "raises an exception" do
expect { athlete.save }.to raise_exception(ActiveRecord::RecordNotSaved)
end
end
end
2) You can also change the feature test to do whatever your Given is doing via the interface:
feature 'Admin creates athlete' do
given(:athlete) { FactoryGirl.build(:athlete, :shop=> #shop, :first_name=>"Birglend", :last_name=>"Firglingham", :email=>"birglendfirglingham#gmail.com") }
scenario 'with invalid status' do
# We're testing that your custom callback stops saving the record.
visit '/athletes/new'
select #shop.title, from: 'Shop'
fill_in 'First name', with: 'Birglend'
fill_in 'Last name', with: 'Firglingham'
fill_in 'Email', with: 'birglendfirglingham#gmail.com'
select 'Not an athlete' from: 'Status'
click_button 'Create Athlete'
expect(page).to have_content 'There was an error creating the athlete'
# I'd argue that these next lines, what you're trying to test, really shouldn't be tested here. What you should be testing is that the form provides feedback that the athlete record wasn't created.
click_link 'Athletes'
expect(page).not_to have_css('.athlete-row')
end
end
I don't know what your interface is supposed to be doing, so I just made something up above. Hope that helps you go down the right path.
I think you should just use validations rather than a before_save. You generally want to avoid before and after create/save callbacks unless you really need them. That said you can expect the call to raise an exception if that is the desired behavior.
My application is propagating a ActiveRecord::RecordNotFound to the controller when a model can't retrieve a record.
Here is the DB query:
def self.select_intro_verse
offset = rand(IntroVerse.count)
IntroVerse.select('line_one', 'line_two', 'line_three', 'line_four', 'line_five').offset(offset).first!.as_json
end
first! raises a ActiveRecord::RecordNotFound if no record is found, and I am able to rescue it and render an appropriate template in my controller.
This is the expected behavior so I would like to have a test for it in my specs. I wrote:
context "verses missing in database" do
it "raises an exception" do
expect(VerseSelector.select_intro_verse).to raise_exception
end
end
When I run rspec:
1) VerseSelector verses missing in database raises an exception
Failure/Error: expect(VerseSelector.select_intro_verse).to raise_exception
ActiveRecord::RecordNotFound:
ActiveRecord::RecordNotFound
To me the test fails even though the exception is raised! How can I make my test pass?
Look in documentation for rspec-expectations#expecting-errors this:
expect(VerseSelector.select_intro_verse).to raise_exception
should be except syntax for exception must be lambda or proc:
expect { VerseSelector.select_intro_verse }.to raise_exception(ActiveRecord::RecordNotFound)
I have a database table with a certain field which should be impossible to update once it has been inserted to the database. How do I tell my model that it shouldn't allow updating of a certain field?
You want to use attr_readonly:
Attributes listed as readonly will be used to create a new record but update operations will ignore these fields.
class Customer < ActiveRecord::Base
attr_readonly :your_field_name
end
Here's my related solution to a similar problem - we have fields that we want a user to be able to set themselves, we don't require them on creation of the record, but we do NOT want to them be changed once they are set.
validate :forbid_changing_some_field, on: :update
def forbid_changing_some_field
return unless some_field_changed?
return if some_field_was.nil?
self.some_field = some_field_was
errors.add(:some_field, 'can not be changed!')
end
The thing that surprised me, though, was that update_attribute still works, it bypasses the validations. Not a huge deal, since updates to the record are mass assigned in practice - but I called that out in the tests to make it clear. Here's some tests for it.
describe 'forbids changing some field once set' do
let(:initial_some_field) { 'initial some field value' }
it 'defaults as nil' do
expect(record.some_field).to be nil
end
it 'can be set' do
expect {
record.update_attribute(:some_field, initial_some_field)
}.to change {
record.some_field
}.from(nil).to(initial_some_field)
end
context 'once it is set' do
before do
record.update_attribute(:some_field, initial_some_field)
end
it 'makes the record invalid if changed' do
record.some_field = 'new value'
expect(record).not_to be_valid
end
it 'does not change in mass update' do
expect {
record.update_attributes(some_field: 'new value')
}.not_to change {
record.some_field
}.from(initial_some_field)
end
it 'DOES change in update_attribute!! (skips validations' do
expect {
record.update_attribute(:some_field, 'other new value')
}.to change {
record.some_field
}.from(initial_some_field).to('other new value')
end
end
end