I have a few RSpec controller tests. Some work, some don't, and I'm trying to figure out how on Earth to fix them up and make them more efficient
Ideally, I would like to see if I can get each spec into the following form
subject { ... }
it { ... }
it { ... }
it { ... }
Note that for all of my controller specs I've written macros for the actual controller actions. The macros are all tested and all work, and the names make it fairly obvious what they do.
My "Create" test:
formats ||= ["html", "js"]
formats.each do |format|
context "valid attributes" do
subject { do_post_create( :customer, valid_attributes, format ) }
its(:response_code) { should eq(302)}
it { should redirect_to admin_customer_path(Customer.find_by_id(???))}
it { expect { subject }.to change(Customer, :count).by(1) }
end
context "invalid attributes" do
subject { do_post_create( :customer, invalid_attributes, format ) }
its(:response_code) { should eq(200)}
it { should render_template :new }
it { expect { subject }.to_not change(Customer, :count).by(1) }
end
end
In that spec, I've been trying to figure out some way to get the ID of the newly created object from the post statement. I've tried "Customer.last", but that doesn't seem to work. Any thoughts?
My "Update" spec:
formats ||= ["html", "js"]
formats.each do |format|
context "valid attributes" do
let(:object) { FactoryGirl.create(:customer) }
subject { do_put_update( class_to_symbol(model), object.id, attributes, format ) }
its(:response_code) { should eq(302)}
it "does alter #{model}" do
do_put_update( class_to_symbol(model), object.id, attributes, format )
assigns(:customer).should eq(object)
flash[:notice].should =~ /Success/
object.reload
attributes.each do |key, value|
object.send(key.to_s).should eq(value)
end
end
end
context "invalid attributes" do
let(:object) { FactoryGirl.create("customer") }
let(:invalid_attributes) { {:username => "!"} }
subject { do_put_update( class_to_symbol(model), object.id, invalid_attributes, format ) }
its(:response_code) { should eq(200)}
it "does not alter #{model}" do
do_put_update( class_to_symbol(model), object.id, invalid_attributes, format )
assigns(:customer).should eq(object)
flash[:notice].should =~ /Fail/
object.reload
attributes.each do |key, value|
object.send(key.to_s).should_not eq(value)
end
end
end
end
In the Update test, I would like to try to express the second block in a more concise way, ideally in a way that I can use the same "subject" statement for all of the tests. Is that possible?
I think you're over-thinking these specs. Instead of trying to force every spec into a predefined format (subject/it/...) write the specs so that they clearly document what should happen, then try to refactor the code afterwards.
Case in point: the use of the implicit subject for controller actions. subject and its are meant to be used with an object, not a method, and only really make sense when used that way. So for example, this makes sense:
subject { [1, 2, 3, 4] }
its(:size) { should == 4 }
Here, it's absolutely clear what is being tested: a 4-element array has a size of 4.
However, when you write:
subject { do_post_create( :customer, valid_attributes, format ) }
its(:response_code) { should eq(302)}
it's not really clear where you are getting that response code from without inspecting the do_post_create action. You say that the names of the macros "make it fairly obvious what they do", but they don't make it fairly obvious what they will return, and this is key for using the implicit subject because it's the return value that becomes the subject.
It would be much clearer just to write:
it "responds with a 302" do
do_post_create(:customer, valid_attributes, format)
response.should eq(302)
end
I also don't recommend mixing specs with and without implicit subjects, since it makes it yet more confusing what you are actually testing. In your invalid attributes context block, for example, you set a subject, but then in your second spec you actually test assignment of customer (assigns(:customer).should eq(object)), so basically the subject is irrelevant for this test. (However by setting the subject here and then not using it you are actually sending a PUT request twice (through do_put_update), which is bound to cause problems -- again, another reason not to be making requests in a subject block.)
I could go on, but I think you get the picture. Making specs short and sweet is great if you can do it without hurting readability, but in this case I think you've gone overboard.
Just my two cents, hope it helps.
p.s. In case the views above seem a bit extreme, read the documentation for implicit subjects, where you'll see that they actually recommend against using implicit subjects at all in public-facing tests:
While the examples below demonstrate how subject can be used as a user-facing concept, we recommend that you reserve it for support of custom matchers and/or extension libraries that hide its use from examples.
Related
I have an issue with my #attributes variable. I would like it to be accessible to keep my code dry, but currently, I have to restate the variable and set it to "values" to get my rspec test to work. What is a better way to do this without duplicating the values.
ref: Unexpected nil variable in RSpec
Shows that it is not accessible in describe, but there needs be another solution. When would "specify" be appropriate? I have not used it.
describe "When one field is missing invalid " do
before(:each) do
#user = create(:user)
#attributes = {"has_car"=>"true", "has_truck"=>"true", "has_boat"=>"true", "color"=>"blue value", "size"=>"large value"}
end
values = {"has_car"=>"true", "has_truck"=>"true", "has_boat"=>"true", "color"=>"blue value", "size"=>"large value"}
values.keys.each do |f|
p = values.except(f)
it "returns invalid when #{f.to_s} is missing" do
cr = CarRegistration::Vehicle.new(#user, p)
cr.valid?
end
end
end
Update based on comments:
I would also like to use the values array hash in other tests. If I put it in the loop as stated, I would still have to repeat it in other places. Any other recommendations?
Update: I tried using let(),
describe "When one field is missing" do
let(:user) {Factorybot.create(:user)}
let(:attributes) = {{"has_car"=>"true", "has_truck"=>"true", "has_boat"=>"true", "color"=>"blue value", "size"=>"large value"}}
attributes do |f|
p = attributes.except(f)
it "returns invalid when #{f.to_s} is missing" do
cr = CarRegistration::Vehicle.new(user, p)
cr.valid?
end
end
end
but get the following error.
attributes is not available on an example group (e.g. a describe or context block). It is only available from within individual examples (e.g. it blocks) or from constructs that run in the scope of an example (e.g. before, let, etc).
In either of your snippets, you don't need attributes inside of your specs. It is data to generate specs. As such, it must live one level above.
describe "When one field is missing" do
let(:user) { Factorybot.create(:user) }
attributes = { "has_car" => "true", "has_truck" => "true", "has_boat" => "true", "color" => "blue value", "size" => "large value" }
attributes do |f|
p = attributes.except(f)
it "returns invalid when #{f.to_s} is missing" do
cr = CarRegistration::Vehicle.new(user, p)
cr.valid?
end
end
end
As you seem to have recognized, based on the other SO post you linked to, you can't refer to your instance variables out in your describe block. Just set it as a local variable as you've done.
Using let
describe "When one field is missing" do
let(:user) {Factorybot.create(:user)}
let(:attributes) = {{"has_car"=>"true", "has_truck"=>"true", "has_boat"=>"true", "color"=>"blue value", "size"=>"large value"}}
## The variables are used INSIDE the it block.
it "returns invalid when a key is missing" do
attributes do |f|
p = attributes.except(f)
cr = CarRegistration::Vehicle.new(user, p)
expect(cr.valid?).to eq(true) # are you testing the expectation? Added this line.
end
end
end
Personally I don't like writing test (like the above) which could fail for multiple reasons. Sergio is correct. But if you want to use let you have to make use of it from WITHIN the it block - this example shows that.
I have a method call in a ruby model that looks like the following:
Contentful::PartnerCampaign.find_by(vanityUrl: referral_source).load.first
Within the models spec.rb file, I'm trying to mock that call and get a value by passing in a param. But I'm having trouble figuring out the correct way of calling it.
At the top of my spec.rb file I have:
let(:first_double) {
double("Contentful::Model", fields {:promotion_type => "Promotion 1"})
}
Within the describe block I've tried the following:
expect(Contentful::PartnerCampaign).to receive_message_chain(:find_by, :load, :first).
and_return(first_double)
expect(Contentful::PartnerCampaign).to receive_message_chain(:find_by, :load, :first).with(vanityUrl: 'test_promo_path').
and_return(first_double)
expect(Contentful::PartnerCampaign).to receive_message_chain(:find_by => vanityUrl: 'test_promo_path', :load, :first).
and_return(first_double)
As you can probably guess, none of these are working. Does anyone know the correct way to do this sort of thing? Is it even possible?
Generally speaking, I prefer not to use stub chains, as they are often a sign that you are violating the Law of Demeter. But, if I had to, this is how I would mock that sequence:
let(:vanity_url) { 'https://vanity.url' }
let(:partner_campaigns) { double('partner_campaigns') }
let(:loaded_partner_campaigns) { double('loaded_partner_campaigns') }
let(:partner_campaign) do
double("Contentful::Model", fields {:promotion_type => "Promotion 1"}
end
before do
allow(Contentful::PartnerCampaign)
.to receive(:find_by)
.with(vanity_url: vanity_url)
.and_return(partner_campaigns)
allow(partner_campaigns)
.to receive(:load)
.and_return(loaded_partner_campaigns)
allow(loaded_partner_campaigns)
.to receive(:first)
.and_return(partner_campaign)
end
This is what I would do. Notice that I split the "mocking" part and the "expecting" part, because usually I'll have some other it examples down below (of which then I'll need those it examples to also have the same "mocked" logic), and because I prefer them to have separate concerns: that is anything inside the it example should just normally focus on "expecting", and so any mocks or other logic, I normally put them outside the it.
let(:expected_referral_source) { 'test_promo_path' }
let(:contentful_model_double) { instance_double(Contentful::Model, promotion_type: 'Promotion 1') }
before(:each) do
# mock return values chain
# note that you are not "expecting" anything yet here
# you're just basically saying that: if Contentful::PartnerCampaign.find_by(vanityUrl: expected_referral_source).load.first is called, that it should return contentful_model_double
allow(Contentful::PartnerCampaign).to receive(:find_by).with(vanityUrl: expected_referral_source) do
double.tap do |find_by_returned_object|
allow(find_by_returned_object).to receive(:load) do
double.tap do |load_returned_object|
allow(load_returned_object).to receive(:first).and_return(contentful_model_double)
end
end
end
end
end
it 'calls Contentful::PartnerCampaign.find_by(vanityUrl: referral_source).load.first' do
expect(Contentful::PartnerCampaign).to receive(:find_by).once do |argument|
expect(argument).to eq({ vanityUrl: expected_referral_source})
double.tap do |find_by_returned_object|
expect(find_by_returned_object).to receive(:load).once do
double.tap do |load_returned_object|
expect(load_returned_object).to receive(:first).once
end
end
end
end
end
it 'does something...' do
# ...
end
it 'does some other thing...' do
# ...
end
If you do not know about ruby's tap method, feel free to check this out
I think you need to refactor the chain in two lines like this:
model = double("Contentful::Model", fields: { promotion_type: "Promotion 1" })
campaign = double
allow(Contentful::PartnerCampaign).to receive(:find_by).with(vanityUrl: 'test_promo_path').and_return(campaign)
allow(campaign).to receive_message_chain(:load, :first).and_return(model)
Then you can write your spec that will pass that attribute to find_by and check the chain.
I am testing method last_photo:
def last_photo
#last_photo ||= user_updates.latest.where("photo_front IS NOT NULL and photo_front != ''").first.try(:photo_front)
end
Spec:
context "instance method" do
let(:user) { create :user }
context "last photo" do
before { create_list(:user_update, 3, user: user) }
let(:user_updates){ user.user_updates }
describe "#last_photo" do
subject { user.last_photo }
it { should eq user_updates.latest.first.photo_front }
end
end
end
the test should be successful. But there are strange error.
Attached GIST.
The answer is pretty simple really:
expected: #<PhotoUploader:0x00000007e34868 ...
got: #<PhotoUploader:0x00000007ebc100 ...
The values might be the same, but the objects are different in memory. Since you're doing a comparison on the objects, rspec expects the objects to be the exact same.
Now, user.user_updates and user_updates are two different variables in memory. You should do a comparison on the values.
Rspec implicitly defined subject documentation says:
While the examples below demonstrate how subject can be used as a
user-facing concept, we recommend that you reserve it for support of
custom matchers and/or extension libraries that hide its use from
examples.
Does it mean, that I should try to never call "subject." directly in my specs? If yes, what should I use instead as a subject object?
Compare these 2 examples:
describe "User" do
subject { User.new(age: 42) }
specify { subject.age.should == 42 }
its(:age) { should == 42 }
end
describe "User" do
let(:user) { User.new(age: 42) }
specify { user.age.should == 42 }
end
UPDATE
There is a cool feature in Rspec - named subject:
Here is an example from David Chelimsky:
describe CheckingAccount, "with a non-zero starting balance" do
subject(:account) { CheckingAccount.new(Money.new(50, :USD)) }
it { should_not be_overdrawn }
it "has a balance equal to the starting balance" do
account.balance.should eq(Money.new(50, :USD))
end
end
When you use user instead of a subject it's more readable (IMHO).
But subject lets you use nice extension its(:age).
Just wondering how to test that actionmailer requests are actually sent to the delayed_job que in rspec.
I would have assumed it was quite simple, but my delayed_job queue doesn't seem to be incrementing. Code below:
Controller:
def create
#contact = Contact.new(params[:contact])
if #contact.save
contactmailer = ContactMailer
contactmailer.delay.contact_message(#contact)
redirect_to(contacts_url)
else
render :action => "new"
end
Spec:
it "queues mail when a contact is created" do
expectedcount = Delayed::Job.count + 1
Contact.stub(:new).with(mock_contact()) { mock_contact(:save => true) }
post :create, :contact => mock_contact
expectedcount.should eq(Delayed::Job.count)
end
Both before and after the call to the controller, the Delayed::Job.count returns 0. I've tried taking the conditional out of the controller, but I still can't get the delayed job count to increment.
Any suggestions appreciated - cheer
You can also test what the jobs will do by running them or turning off queuing.
Tweak config whenever you want (i.e. in a before :each block).
Delayed::Worker.delay_jobs = false
or perform your saved jobs
Delayed::Worker.new.work_off.should == [1, 0]
I have been using this method happily for a while. For one thing, using the new any_instance support in RSpec, you can test your delayed methods effects directly. However, I've found tests that use work_off to be slow.
What I usually do now is:
mock_delay = double('mock_delay').as_null_object
MyClass.any_instance.stub(:delay).and_return(mock_delay)
mock_delay.should_receive(:my_delayed_method)
Then I have a separate spec for my_delayed_method. This is much faster, and probably better unit testing practice -- particularly for controllers. Though if you're doing request specs or other integration-level specs, then you probably still want to use work_off.
I think your mock object is somehow introducing an error -- it's hard to tell exactly how without seeing the definition of the mock_contact method.
In any case, you might try something along these lines:
it "queues mail when a contact is created" do
Contact.stub(:new) { mock_model(Contact,:save => true) }
Delayed::Job.count.should == 0
post :create, {}
Delayed::Job.count.should == 1
end
or the sexier version (caveat: I always end up doing it the non-sexy way):
it "queues mail when a contact is created" do
Contact.stub(:new) { mock_model(Contact,:save => true) }
expect {
post :create, {}
}.to change(Delayed::Job.count).by(1)
end
You can also follow the convention (from Railscast 275) of
ActionMailer::Base.deliveries.last.to.should == user.email
but instead do this:
Delayed::Job.last.handler.should have_content(user.email)
This thread is a bit old, but here is my go at it:
Create a function expect_jobs
def expect_jobs n, time = nil
expect(Delayed::Job.count).to eq(n)
Timecop.travel(time) unless time.nil?
successes, failures = Delayed::Worker.new.work_off
expect(successes).to eq(n)
expect(failures).to eq(0)
expect(Delayed::Job.count).to eq(0)
Timecop.travel(Time.now) unless time.nil?
end
Then simply call it before checking if the callback has done its job. eg:
it "sends a chapter to the admin user" do
post :chapter_to_user, { chapter: #book.chapters.first}
expect_jobs(1)
SubscribeMailer.should have(1).delivery
SubscribeMailer.deliveries.should have(1).attachment
end
This seems to work on my side, and allows me to run both my delayed jobs and my methods.
#zetetic I think we have to pass block in change method here.
It shoulb be like this:
it "queues mail when a contact is created" do
Contact.stub(:new) { mock_model(Contact,:save => true) }
expect {
post :create, {}
}.to change { Delayed::Job.count }.by(1)
end