I have two models Page Article. For every article created a page gets created with the attributes of article. As follows:
class Article
after_save :article_page_create
def article_page_create
site = Site.find_by(data_proxy_id: self.data_proxy_id)
page = Page.where(entity_id: self.id)
if page.blank?
if article_type == 'StaticPage'
Page.create(entity_id: self.id, url: "/static/#{self.url_part}", page_type: 'static_page')
else
Page.create(entity_id: self.id, url: self.url, page_type: 'article_page')
end
else
return page.update(url: self.url) unless article_type == 'StaticPage'
page.update(url: "/static/#{self.url_part}")
end
end
end
I am trying test cases for the first time. So far this is how far I got.
article_spec.rb
require 'rails_helper'
RSpec.describe Article, type: :model do
context 'validation tests' do
it 'ensures article attrs presence' do
page = Page.create(entity_id: self.id, url: "/static/#{self.url_part}", page_type: 'static_page')
expect(page).to eq(true)
end
end
end
I just wanted know is this the way to test my after_save method. Correct me if I am wrong, please.
Hmmmm, I think I can help out here.
When testing callbacks, you need to test two assumptions:
Is it being called for the correct event?
Is it doing what it's supposed to be doing?
And remember, you want to try make sure your tests cover one specific case.
Your specs should be reading:
On saving an article,
I expect the class to receive a callback method
On saving a new article,
I expect the number of Page elements to increase by one
On saving an old article,
I expect an existing page to be updated
You can continue to flesh out based on the article types etc.
E.g.
it 'triggers a callback to create a page on save' do
expect(my_class_instance).to receive(:article_page_create)
#article.save
end
context 'when creating a new page' do
it "creates a new article" do
# expect number of pages to change by 1
end
end
context 'when updating an old page' do
it 'updates the corresponding article' do
# expect attribs to be correct for corresponding page
end
end
Related
How to create multiple scenarii without to have to reinit context between 2 tests with RSpec ? I don't need context reinitialisation (that is very slow), but I need to check multiple things for the same given context. The example below works, but it's a bad example : the context is reinitialized. If I do before(:all), it doesnt works because of the stubs. Any idea ?
feature 'Aids page' do
context 'No active user' do
before(:each) do
create_2_different_aids
disable_http_service
visit aids_path
end
after(:each) do
enable_http_service
end
scenario 'Should display 2 aids NOT related to any eligibility' do
display_2_aids_unrelated_to_eligibility
end
scenario 'Should not display breadcrumb' do
expect(page).not_to have_css('.c-breadcrumb')
end
end
end
feature specs often have more than one expect in the same scenario. They are not like unit tests where each it should test only one thing... they are more like what a user actually does on a page: "go here, click on this thing, check I can see that thing, click there, check that the thing changes" etc... so you can just do something like this:
feature 'Aids page' do
context 'No active user' do
scenario 'Sees aids not related to eligibility' do
create_2_different_aids
disable_http_service
visit aids_path
expect(page).not_to have_css('.c-breadcrumb')
display_2_aids_unrelated_to_eligibility
enable_http_service
end
end
end
Alternatively... it's possible to use either shared setup (as you have already done). That is fairly common.
Found. Actually there is a hack you can use to initialize your test only once, as the example shown below :
feature 'Aides page' do
context 'No active user' do
that = nil
before do
if !that
create_2_different_aids
disable_http_service
visit aides_path
that = Nokogiri::HTML(page.html)
end
end
after do
enable_http_service
end
scenario 'Should display 2 aids NOT related to any eligibility' do
display_2_aids_unrelated_to_eligibility(that)
end
scenario 'Should not display breadcrumb' do
expect(that.css('.c-breadcrumb').size).to eq(0)
end
end
end
I have a User class with a save method which makes a change to one of the user instance attributes. Specifically, each user has an options hash that gets one of its values deleted during the save process.
I have an rspec test with 2 context groups. Each group creates a new #user object using FactoryGirl.build(:user). When I call #user.save in the first context group, the attribute change occurs as expected. However, the second time that FactoryGirl.build(:user) gets called, it doesn't return a User object according to the FactoryGirl definition. It returns a user object with an options hash that is missing the same value that gets deleted during the save process. This object is not valid, and as a result #user.save fails the second time.
UPDATE: I tried changing the variable names and I still have the same problem. The issue seems to be with the FactoryGirl :user factory being modified somehow during the first example, resulting in the second example failing.
Below is a simplified version of my code. Whichever context group is executed second ("with avatar" or "without avatar") when run randomly by Rspec is the one that fails. I have used puts in both cases to confirm that the second #user has a bad options hash, and causes the test to fail.
describe "save" do
context "with avatar" do
before(:context) do
#user = FactoryGirl.build(:user)
puts #user
#save_result = #user.save
end
after(:context) do
delete_user(#user)
end
it "should return true" do
expect(#save_result).to be true
end
end
context "without avatar" do
before(:context) do
#user = FactoryGirl.build(:user, avatar: nil)
puts #user
#save_result = #user.save
end
after(:context) do
delete_user(#user)
end
it "should return true" do
expect(#save_result).to be true
end
end
end
I suspect that the options hash gets reused.
According to the FactoryGirl readme, when you want to add a hash attribute to a FactoryGirl definition and that hash is dynamic (i.e. not the same among all created instances), you need to wrap it in a block:
Instead of:
factory :user do
options { option1: 1, option2: 2 }
end
You need to do:
factory :user do
options { { option1: 1, option2: 2 } }
end
I have two models Article and ArticleVote. When I destroy an article vote (user cancels his vote), I want article's score to be changed. So I made a callback. Here is what my ArticleVote model looks like:
class ArticleVote < ActiveRecord::Base
belongs_to :article
belongs_to :user
before_destroy :before_destroy
validates :value, inclusion: {in: [1, -1]}
def self.upvote(user, article)
cast_vote(user, article, 1)
end
def self.downvote(user, article)
cast_vote(user, article, -1)
end
private
def self.cast_vote(user, article, value)
vote = ArticleVote.where(user_id: user.id, article_id: article.id).first_or_initialize
vote.value = value
vote.save!
article.score += value
article.save!
end
def before_destroy
article.score -= value
article.save
end
end
My ArticleVote#destroy test fails:
context '#destroy' do
let(:user) { FactoryGirl.create(:user) }
let(:article) { FactoryGirl.create(:article) }
it 'changes article score by negative vote value' do
ArticleVote.upvote(user, article)
expect{ ArticleVote.where(user: user, article: article).first.destroy }.to change{ article.score }.by -1
end
end
Failures:
1) ArticleVote voting #destroy should change article score by nevative vote value
Failure/Error: expect{ ArticleVote.where(user: user, article: article).first.destroy }.to change{ article.score }.by -1
result should have been changed by -1, but was changed by 0
# ./spec/models/article_vote_spec.rb:32:in `block (4 levels) in '
When I change my test to this, it passes:
context '#destroy' do
let(:user) { FactoryGirl.create(:user) }
let(:article) { FactoryGirl.create(:article) }
it 'changes article score by nevative vote value' do
ArticleVote.upvote(user, article)
vote = ArticleVote.where(user: user, article: article).first
expect{ vote.destroy }.to change{ vote.article.score }.by -1
end
end
Shouldn't these two be equivalent? Shouldn't my article and vote.article reference to same instance?
In your first test you are creating new Article object in the memory. Rails is not going to check attribute values in db every time you call article.score as it would make everything extremely slow - those value are stored in the memory (it is kind-of caching the results). Hence article.score is not going to change at any point. You need to tell rails to reload all the attributes from the database - use article.reload.score within change block.
Additional explanation:
Let say we did:
model_1 = Model.where(<condition>).first
model_2 = Model.where(<some condition>).first
Both model_1 and model_2 are created from some row in the database, however they are different objects in the memory. Hence when you do:
model_1.some_attribute = 'new value'
model_1.save
model_2.some_attribute #=> 'old_value'
The reason is performance - Rails is not going to check the database whether given attribute has changed or not within database. model_2 did the sql query when it was created and will not re-check until you tell it to do so.
However in most cases there is no point in creating two duplicate objects in the memory and it is the best practice not to do so. It is not always as obvious where those obejcts are created. In case of your first test, the problem is that ArticleVote.where(user: user, article: article).first.article is a duplicate of your original article object, hence your before_save callback follows same pattern as model_1, model_2 example.
Best way to avoid such a problems is a proper use of associations, including inverse_of option and using model.associations.where(...) in place of AssocatedClass.where(model: model, ...) or model.association.create(...) in place of 'AssociationClass.create(model: model, ...)
When writing tests using RSpec, I regularly have the need to express something like
Klass.any_instance_with_id(id).expects(:method)
Main reason is that in my test, I often have the object that should receive that method call available, but due to the fact that ActiveRecord, when loading the object with that id from the database, will create a different instance, I can't put the "expects" on my own instance
Sometimes I can stub the find method to force ActiveRecord to load my instance, sometimes I can stub other methods, but having that "any_instance_with_id" would make life so much easier...
Can't image I'm the first having this problem... So if any of you found a "workaround", I'd be glad to find out!
Example illustrating the need:
controller spec:
describe 'an authorized email' do
let(:lead) { create(:lead, status: 'approved') }
it "should invoice its organisation in case the organisation exceeds its credit limit" do
lead.organisation.expects :invoice_leads
get :email
end
end
controller:
def email
leads = Lead.approved
leads.each do |lead|
lead.organisation.invoice_leads if lead.organisation.credit_limit_exceeded?
end
redirect_to root_path
end
It seems weird to me you need that for specs.
You should take the problem one level higher: when your app tries to retrieve the record.
Example:
#code
#user = User.find(session[:user_id])
# spec
let(:fake_user) { mock_model 'User', method: false }
it 'description' do
User.should_receive(:find).and_return fake_user
fake_user.expects(:method)
#...
end
Order/invoice example:
let(:order) { mock_model 'order', invoice: invoice }
let(:invoice) { mock_model 'Invoice', 'archive!' => false }
My Rails models: task has_many positions.
Scenario: When I create a new position, it should create itself a task. I'd like to test that, and I'm doing it like this:
context "creating a new position" do
let(:position) { create :position, name: 'Read some books', :task => nil }
it "should create a simple task" do
Task.find_by_name('Read some books').should be_nil # First should
position # Execute let() block (FactoryGirl is lazy evaluating)
Task.find_by_name('Read some books').should_not be_nil # Second (more relevant) should
end
end
So how should I improve my test? The first "should" simply makes sure that there isn't already a Task, so we can be sure that creating the Position creates the Task. But this violates the "only one should per it block" principle. So what about this?
context "creating a new position" do
let(:position) do
position = create :position, name: 'Read some books', :task => nil
Task.delete_all
position
end
it "should create a simple task" do
position # Execute let() block (FactoryGirl is lazy evaluating)
Task.find_by_name('Read some books').should_not be_nil
end
end
Or should I simply count on the fact that there shouldn't be such a task anyways (because a clean test db wouldn't have one)? Thanks for your opinions.
Update (Solution)
After some research I found the change matcher of RSpec:
let(:position) { create :position, name: 'Read some books', :task => nil }
it "should create a simple task" do
# Thanks to FactoryGirl's lazy evaluation of let(), the position doesn't yet exist in the first place, and then after calling position in the expect{} block, it is created.
expect { position }.to change{ Task.count(conditions: { name: 'Read some books' }) }.by(1)
end
What to Test
I will not address in detail whether the tests themselves are useful to any degree. To me, they seem to be exercising basic database functions rather than application logic, which is of marginal utility, but only you can really decide what's important to test.
Be Specific
In the example you give, there's no real reason to use a let block, which memoizes the variable. If only one test needs the record, instantiate it just in that specific test. For example:
context 'creating a new position' do
it 'should be nil when the position record is missing' do
Task.find_by_name('Read some books').should be_nil
end
it 'should successfully create a position' do
create :position, name: 'Read some books', :task => nil
Task.find_by_name('Read some books').should_not be_nil
end
end
Alternatively, if you're trying to test how your application behaves when a record is missing, then go ahead and memoize a variable or create a record in a before block, but explicitly delete the record in that one specific test.
Multiple Contexts
Finally, if you're finding that you have too much state to set up in individual tests, that's usually a clue that you should consider splitting your tests into different contexts. For example, you might want to separate tests into one context that checks behavior when a record doesn't exist, and a separate context for when records do exist.
Like all things testing, it's an art more than a science. Your mileage may vary.
RSpec 2.11 allows you to pass a block to change, and it expects the return value of the block to be the thing that changes. I would expect this to work for you:
expect { position }.to change { Task.where(:name => 'Read some books').count }.from(0).to(1)