RSpec it_behaves_like reduces debugging visibility - ruby

I have the following code:
context 'user doesnt exists with that email' do
let(:params) { original_params.merge(login: "nouser#example.org") }
it_behaves_like '404'
it_behaves_like 'json result'
it_behaves_like 'auditable created'
end
It is dry because I can use these elements in other contexts as well:
context 'user exists with that email' do
it_behaves_like '200'
it_behaves_like 'json result'
end
My shared_example is:
...
RSpec.shared_examples "json result" do
specify 'returns JSON' do
api_call params, developer_header
expect { JSON.parse(response.body) }.not_to raise_error
end
end
...
The benefits are that the spec is more readable and is dry. The spec failure points to the shared_example file rather than the original spec. It is hard to debug.
The following error occurs at login_api_spec:25, but this is rspecs output:
rspec ./spec/support/shared_examples/common_returns.rb:14 # /api/login GET /email user doesnt exists with that email behaves like 401 returns 401
Any good advice how to proceed to write both dry and easy to debug rspec?
Without shared examples the code would be much longer and not as easy to read:
context 'user doesnt exists with that email' do
let(:params) { original_params.merge(login: "nouser#example.org") }
specify "returns 404" do
api_call params, developer_header
expect(response.status).to eq(404)
end
specify 'returns JSON' do
api_call params, developer_header
expect { JSON.parse(response.body) }.not_to raise_error
end
specify 'creates an api call audit' do
expect do
api_call params, developer_header
end.to change{ EncoreBackend::ApiCallAudit.count }.by(1)
end
end
I have thousands of RSpec tests like this so it is very beneficial to write the specs with shared examples because it is fast to write, but the debugging is hard.

Amongst the detailed errors there is description like this:
Shared Example Group: "restricted_for developers" called from ./spec/api/login_api_spec.rb:194
This tells the exact place of the error

Related

Why am I getting error "#let or #subject called without a block"?

I wonder how to pass code to shared examples.
I have these specs (snipped)
let(:property) { subject.send(type) }
context 'when one or both depending properties are not set' do
shared_examples_for 'not set' do |init_proc|
it "imperial.value returns nil" do
init_proc.call
expect(property.imperial.value).to eq(nil)
end
# 3 similar examples snipped
end
context "when neither is set" do
include_examples 'not set', Proc.new { }
end
context "when #{type1.inspect} is not set" do
include_examples 'not set', # error in this line
Proc.new { subject.send("#{type2}=", [100, :imperial]) }
end
context "when #{type2.inspect} is not set" do
include_examples 'not set',
Proc.new { subject.send("#{type1}=", [100, :imperial]) }
end
end
What is surprising me is the error I get:
RuntimeError:
#let or #subject called without a block
I thought this would work. It's as if Proc.new calls subject right away, instead when I call init_proc.call. Is that what's going on?
If not, then what's causing the error?
PS. Question is not how to get this code to work. I got this code to work, I am only interested in explanation why I got the error before.

Rspec: How do I make my passing tests fail?

I have a client object in a ruby gem that needs to work with a web service. I am testing to verify that it can be properly initialised and throws an error if all arguments are not passed in.
Here are my specs:
describe 'Contentstack::Client Configuration' do
describe ":access_token" do
it "is required" do
expect { create_client(access_token: nil) }.to raise_error(ArgumentError)
end
end
describe ":access_key" do
it "is required" do
expect { create_client(access_key: nil) }.to raise_error(ArgumentError)
end
end
describe ":environment" do
it "is required" do
expect { create_client(environment: nil) }.to raise_error(ArgumentError)
end
end
end
Here is the gem code:
module Contentstack
class Client
attr_reader :access_key, :access_token, :environment
def initialize(access_key:, access_token:, environment:)
#access_key = access_key
#access_token = access_token
#environment = environment
validate_configuration!
end
def validate_configuration!
fail(ArgumentError, "You must specify an access_key") if access_key.nil?
fail(ArgumentError, "You must specify an access_token") if access_token.nil?
fail(ArgumentError, "You must specify an environment") if environment.nil?
end
end
end
and here is the spec_helper method:
def create_client(access_token:, access_key:, environment:)
Contentstack::Client.new(access_token: access_token, access_key: access_key, environment: environment)
end
The problem is: I can't find a way to make these tests fail before they pass. These tests always pass because ruby throws an ArgumentError by default. I don't understand if this is the right approach to TDD. How do I get into a red-green-refactor cycle with this scenario?
create_client raises the ArgumentError, because it expects three keyword arguments and you are passing only one: (maybe you should have tested your helper, too)
def create_client(access_token:, access_key:, environment:)
# intentionally left empty
end
create_client(access_key: nil)
# in `create_client': missing keywords: access_token, environment (ArgumentError)
You could use default values in your helper to overcome this:
def create_client(access_token: :foo, access_key: :bar, environment: :baz)
Contentstack::Client.new(access_token: access_token, access_key: access_key, environment: environment)
end
create_client(access_key: nil)
# in `validate_configuration!': You must specify an access_key (ArgumentError)
Finally, you could be more specific regarding the error message:
expect { ... }.to raise_error(ArgumentError, 'You must specify an access_key')
please refer to the answer by Stefan, it’s way more proper
The proper way would be to mock Client#validate_configuration! to do nothing, but here it might be even simpler. Put in your test_helper.rb:
Client.prepend(Module.new do
def validate_configuration!; end
end)
Frankly, I do not see any reason to force tests to fail before they pass in this particular case. To follow the TDD, you should have been running tests before validate_configuration! implementation. Then those tests would have failed.
But since you have implemented it in advance, there is no need to blindly thoughtless follow the rule “test must fail before pass.”

Where and how to expect on Rspec / VCR stubbed response?

I'm writing a client in ruby for an API that we're using. We use rspec and VCR with webmock to mock request/responses to the API.
What is the best or appropriate way to test the response back from the API when the response payload is really large?
Is there a best practice around where to put the large expected payload and how to test this?
require 'spec_helper'
describe Service::API, vcr: true do
describe '.method' do
it 'returns valid response' do
#returns large body payload
response = subject.method
expect(response).to eq ???
end
end
end
You shouldn't be testing the payload, you need to test that the method does what you expect with that payload. VCR is going to take care of storing the payload. You may need to assert that you send what you expect to the API, and assert what you do with the result. You should also test the fail scenarios, like the API times out, or network error etc. But the payload itself you shouldn't really need to touch in the test.
You will probably find that it helps to break the test down into scenarios;
given a call with the correct params
given a bad call e.g. missing resource at the API end
given a network error
I tend to find it easier to just write some of the obvious scenarios and then start from the assert for each one. Something like below, somewhat abridged and using RSpec but you should get the idea;
describe Transaction do
# Create a real object in the api.
let(:transaction) { create :transaction }
describe '.find', :vcr do
context 'given a valid id' do
subject { Transaction.find(transaction.id) }
it 'returns a Transaction object' do
is_expected.to eq(transaction)
end
end
context 'given an invalid transaction_id' do
subject { Transaction.find('my-bad-transaction') }
it 'should rescue Gateway::NotFoundError' do
is_expected.to be_nil
end
it 'raises no NotFoundError' do
expect { subject }.to raise_error(Gateway::NotFoundError)
end
end
end
end

Avoid duplication while testing model attributes in RSpec

I have a several models and in this models I have attributes that I don't want to be blank / empty.
I want to heavily test my models on these restrictions using RSpec and Factory Girl.
However I end up with code duplication:
user_spec:
it 'is invalid if blank' do
expect {
FactoryGirl.create(:user, nickname => '')
}.to raise_error(ActiveRecord::RecordInvalid)
end
message_spec:
it 'is invalid if blank' do
expect {
FactoryGirl.create(:message, :re => '')
}.to raise_error(ActiveRecord::RecordInvalid)
end
How can I factor it ?
RSpec provides several ways to do so, such as Shared Examples.
1. Create a file in your [RAILS_APP_ROOT]/support/
Based on your example, you could name this file not_blank_attribute.rb. Then, you just have to move your duplicated code in and adapting it to make it configurable :
RSpec.shared_examples 'a mandatory attribute' do |model, attribute|
it 'should not be empty' do
expect {
FactoryGirl.create(model, attribute => '')
}.to raise_error(ActiveRecord::RecordInvalid)
end
end
2. Use it_behaves_like function in your specs
This function will call the shared example.
RSpec.describe User, '#nickname' do
it_behaves_like 'a mandatory attribute', :User, :nickname
end
Finally, it outputs:
User#nickname
behaves like a mandatory attribute
should not be empty

Simplifying rspec unit tests

A lot of times in unit test in rspec having to specify both a context and a let is somewhat cumbersome and seems unnecessary. For example:
context 'type = :invalid' do
let(:type) { :invalid }
it { expect { subject }.to raise_error(ArgumentError) }
end
It would be nicer (in aggregate over lots of tests) if I could do something like:
let_context type: :invalid do
it { expect { subject }.to raise_error(ArgumentError) }
end
The method would define a context and let(s) for me and the context's argument would be something like type = :invalid or let(:type) { :invalid } because I don't have anything else to say other that the fact that this variable has changed.
A lot of times in unit test in rspec having to specify both a context and a let is somewhat cumbersome
Sounds like you might want to use a RSpec shared context.
UPDATE
RSpec provides a DSL for the syntax you're suggesting: a shared example. For example:
RSpec.shared_examples "some thang" do |type|
it { expect { subject }.to raise_error(ArgumentError) }
end
RSpec.shared_examples "a thang" do
include_examples "some thang", :invalid
# Or whatever is more appropriate for your domain
# I.e., If you're testing subclass behavior use it_should_behave_like()
end
actually you could get less lines by following some http://betterspecs.org recommendations:
context 'type = :invalid' do
let(:type) { :invalid }
it { expect{ subject }.to raise_error(ArgumentError)
end
Your variant could be read as
it raises an error expect subject to raise error
While this is much cleaner
it expect subject to raise_error
Nevertheless it is pretty off topic :)
UPD
Oh. Really you can't pass two blocks to method, so below example is not valid Ruby :)
context(:type) { :invalid } do
it{ expect{ subject }.to raise_error(ArgumentError)
end
While your example
let_context type: :invalid do
...
end
Won't do lazy execution, like let does

Resources