I just upgraded to RSpec 2.13 and am seeing two different warnings when running my specs:
/Users/peterbrown/.rvm/gems/ruby-2.0.0-p0/gems/rspec-core-2.13.0/lib/rspec/core/memoized_helpers.rb:120: warning: method redefined; discarding old subject
/Users/peterbrown/code/classy_enum/spec/classy_enum/active_record_spec.rb:63: warning: previous definition of subject was here
I suspect it has something to do with how I am looping over an array and defining a new subject each time:
describe DefaultDog do
context "with valid breed options" do
[:golden_retriever, 'golden_retriever', Breed::GoldenRetriever.new, Breed::GoldenRetriever].each do |option|
subject { DefaultDog.new(:breed => option) }
it { should be_valid }
its(:breed) { should be_a(Breed::GoldenRetriever) }
end
end
end
Is there a problem with defining a subject in a loop like this?
Update:
I should also note that I'm running my specs with the -w option:
RSpec::Core::RakeTask.new(:spec) do |t|
t.ruby_opts = "-w"
end
Try putting the context within your loop:
describe DefaultDog do
[:golden_retriever, 'golden_retriever', Breed::GoldenRetriever.new, Breed::GoldenRetriever].each do |option|
context "with valid breed option #{option}" do
subject { DefaultDog.new(:breed => option) }
it { should be_valid }
its(:breed) { should be_a(Breed::GoldenRetriever) }
end
end
end
I believe that writing tests that involve iterating over collections this way means that you will execute a separate test for every item in the collection, versus just performing one test that loops through a collection (hence redefining the subject). I think this is an understated gotcha in RSpec that caused me much confusion until fairly recently.
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.
There are some articles (e.g. [1]) regarding solving flaky acceptance tests when using Capybara which advocates using e.g.
.to have_text("foo")
instead of
.to eql("foo")
In one of my tests I have .to match(/foo/) and every once in a while this fails. I assume that the match matcher is not in the same category as e.g. the have_text matcher and doesn't wait. The documentation doesn't mention anything regarding this.
Is there any regex matcher so that I can check e.g.
expect(next_url).to match(/foo/)
?
Versions used (not changeable):
capybara: 2.7.x
spec-rails: 3.6.x
[1] https://www.urbanbound.com/make/fix-flaky-feature-tests-by-using-capybaras-apis-properly
The docs for have_text link to the assert_text docs - https://www.rubydoc.info/gems/capybara/Capybara/Node/Matchers#assert_text-instance_method which show that it takes either a string
expect(page).to have_text('Something')
or a Regexp
expect(page).to have_text(/foo/)
As the article you linked to implies, if you find yourself using any non capybara provided matcher with information returned from Capybara you're probably doing something wrong, and setting yourself up for flaky tests.
If you have a page where elements have a delay appearing on the page, you can define a 'wait' method in 'capybara_helpers.rb'
def wait_for timeout = 10, &block
Timeout.timeout(timeout) do
loop do
condition = yield
if (condition)
break true
end
end
end
rescue Timeout::Error
raise "Condition not true in #{timeout} seconds"
end
After that, you can use 'wait_for' method like this:
wait_for { page.has_css?('.class', text: 'Something') }
I recently started to test with rspec, so I can strongly be mistaken, correct me if there is a better way
I create two related models
let(:user) {FactoryGirl.create :user}
let!(:participation) {FactoryGirl.create :participation, user: user}
and before one of the tests change one of the related objects
context "when" do
before {participation.prize = 100}
it "" do
binding.pry
end
end
But inside it
participation.prize => 100
user.participatons.select(:prize) => nil
what am I doing wrong ? and how to fix it?
When you say user.participations.select(:prize), you're making a query to the db to get values in the user's participations' prize columns. But when you say before {participation.prize = 100} you're only setting the prize attribute on the participation object. Try saving the participation before the select line:
participation.prize # => 100
participation.save
user.participatons.select(:prize) # => nil
Another possible issue is that user.participations has been memoized by a previous call. Ensure that user.participations.first == participation. If it doesn't, check
1) puts participation.user_id and
2) puts user.participations, user.reload.participations
Lastly, a better way of setting up the test so that you run into this issue less often is something along the lines of:
# let(:price) { 0 } # default price. Optional so that tests won't throw errors if you forget to set it in a context/describe block.
let(:user) {FactoryGirl.create :user}
let!(:participation) {FactoryGirl.create :participation, user: user, price: price}
# ...
context "when ..." do
let(:price) { 100 }
it "" do
binding.pry
end
end
This way, the price is set when you create the model. Following this pattern generally means running into this problem less.
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.
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.