Testing interaction requests sequences - ruby-on-rails

There best practices is do not use #instance variables and use let(:object). But how to write test of call sequences where each step require previous state but not clean state.
I'd like to write code like this:
describe "intearction" do
let(:user1) { ... }
let(:user2) { ... }
it "request" do
get "/api/v1/request", {user2}, token(user1)
expect(...).to ...
end
it "confirm" do
get "/api/v1/confirm", {user1}, token(user2)
expect(...).to ...
end
end
But that will not work. It may be worked only like this:
describe "intearction" do
let(:user1) { ... }
let(:user2) { ... }
it "all tests" do
# request
get "/api/v1/request", {user2}, token(user1)
expect(...).to ...
# confirm
get "/api/v1/confirm", {user1}, token(user2)
expect(...).to ...
end
end
The it become unusable and names of tests become comments or puts to console. The rspec subsystem loses its purpose. Using #instance variables is bad practice, is there any let-like definition function that does not create new plain variable per each test but per group of tests?
The sequences and api's exists not the first year. What is about your practice of testing sequences?

In tests when you need to prepare environment for the test you do it in before block. So here is how your tests might look like
describe "intearction" do
let(:user1) { ... }
let(:user2) { ... }
describe '#request' do
it "does something" do
get "/api/v1/request", {user2}, token(user1)
expect(...).to ...
end
end
describe '#confirm' do
context 'request was made before' do
before { get "/api/v1/request", {user2}, token(user1) }
it "does something as well" do
get "/api/v1/confirm", {user1}, token(user2)
expect(...).to ...
end
end
end
end

Related

Getting FactoryBot object attributes for API requests with RSpec

I am setting up RSpec request tests, and I have the following test:
require 'rails_helper'
RSpec.describe "ClientApi::V1::ClientContexts", type: :request do
describe "POST /client_api/v1/client_contexts" do
let(:client_context) { build :client_context }
it "creates a new context" do
post "/client_api/v1/client_contexts", params: {
browser_type: client_context.browser_type,
browser_version: client_context.browser_version,
operating_system: client_context.operating_system,
operating_system_version: client_context.operating_system_version
}
expect(response).to have_http_status(200)
expect(json.keys).to contain_exactly("browser_type", "browser_version", "operating_system", "operating_system_version")
# and so on ...
end
end
end
The corresponding factory is this:
FactoryBot.define do
factory :client_context do
browser_type { "Browser type" }
browser_version { "10.12.14-blah" }
operating_system { "Operating system" }
operating_system_version { "14.16.18-random" }
end
end
Now, obviously, that all seems a bit redundant. I have now three places in which I specify the attributes to be sent. If I ever want to add an attribute, I have to do it in all of these places. What I actually want to do is send the particular attributes that the Factory specifies via POST, and then check that they get returned as well.
Is there any way for me to access the attributes (and only these!) that I defined in the Factory, and re-use them throughout the spec?
I should prefix this with a warning that abstracting away the actual parameters from the request being made could be seen as detrimental to the overall test expressiveness. After all, now you'd have to look into the Factory to see which parameters are sent to the server.
You can simply get the Factory-defined attributes with attributes_for:
attributes_for :client_context
If you need more flexibility, you can implement a custom strategy that returns an attribute Hash from your Factory without creating the object, just building it.
Create a file spec/support/attribute_hash_strategy.rb:
class AttributeHashStrategy
def initialize
#strategy = FactoryBot.strategy_by_name(:build).new
end
delegate :association, to: :#strategy
def result(evaluation)
evaluation.hash
end
end
Here, the important part is evaluation.hash, which returns the created object as a Ruby Hash.
Now, in your rails_helper.rb, at the top:
require 'support/attribute_hash_strategy'
And below, in the config block, specify:
# this should already be there:
config.include FactoryBot::Syntax::Methods
# add this:
FactoryBot.register_strategy(:attribute_hash, AttributeHashStrategy)
Now, in the Spec, you can build the Hash like so:
require 'rails_helper'
RSpec.describe "ClientApi::V1::ClientContexts", type: :request do
describe "POST /client_api/v1/client_contexts" do
let(:client_context) { attribute_hash :client_context }
it "creates a new context" do
client = create :client
post "/client_api/v1/client_contexts",
params: client_context
expect(response).to have_http_status(200)
end
end
end
The attribute_hash method will be a simple Hash that you can pass as request parameters.

How to stub zendesk_api current_user for a spec?

I have a model method which I am trying to write a spec for. The method is like this:
def my_method
puts current_user.user_attirbute
end
Where current_user is provided by an authentication gem, zendesk_api-1.14.4. To make this method testable, I changed it to this:
def my_method(user_attribute = nil)
if user_attribute = nil
user_attribute = current_user.user_attribute
end
puts user_attribute
end
This refactor works and is testable, but doesn't seem like a good practice. Ideally the gem would provide some sort of test helper to help stub/mock the current_user, but I haven't been able to find anything. Any suggestions?
You can go simple way and just test returning of proper value by current_user#user_attribute method. Example:
describe '#my_method' do
subject { instance.my_method } # instance is an instance of your class where #my_method is defined
let(:user) { instance_spy(ZendeskAPI::User, user_attribute: attr) }
let(:attr} { 'some-value' }
before do
allow(instance).to receive(:current_user).and_return(user)
end
it { is_expected.to eq(attr) }
end
But I would go with VCR cassette(vcr gem is here) because it is related 3rd party API response - to minimize a risk of false positive result. Next example demonstrates testing with recorded response(only in case if #current_user method performs a request to zendesk):
describe '#my_method', vcr: { cassette_name: 'zendesk_current_user' } do
subject { instance.my_method }
it { is_expected.to eq(user_attribute_value) } # You can find the value of user_attribute_value in recorded cassette
end
P.S. I assumed that you put puts in your method for debugging. If it is intentional and it is part of the logic - replace eq with output in my example.

Create RSpec context inside a function

In order to avoid repeating myself a lot in my Rspec tests, I'd like to write a function like this
def with_each_partner(&block)
PARTNER_LIST.each do |partner|
context "with partner #{partner.name}" { yield partner }
end
end
I have such a function and it works in the sense that all the tests run with the correct value for partner supplied, but they don't print during output as being part of the "with partner X" context: instead if I have a test like this:
describe Thing do
subject { Thing.new(partner) }
with_each_partner do |partner|
it 'does its thing' do
expect(subject.do).to eq 'its thing'
end
end
end
I end up with output like this:
Thing
does its thing
Instead of the desired output, which is like:
Thing
with partner X
does its thing
with partner Y
does its thing
How can I get RSpec to correctly work with the context created in my function?
TL;DR: do this:
def with_each_partner(&block)
PARTNER_LIST.each do |partner|
context "with partner #{partner.name}" do
class_exec(&block)
end
end
end
Explanation
RSpec's DSL works by evaluating the blocks with a changed self -- this is how it is a method within a describe or context block, but not outside of it. When you yield, the provided block is evaluated with the original self that was self at the point the block was defined. What that means is that with your original with_each_partner definition, this code:
describe Thing do
subject { Thing.new(partner) }
with_each_partner do |partner|
it 'does its thing' do
expect(subject.do).to eq 'its thing'
end
end
end
Is really being evaluated like this:
describe Thing do
subject { Thing.new(partner) }
outer_example_group = self
with_each_partner do |partner|
outer_example_group.it 'does its thing' do
expect(subject.do).to eq 'its thing'
end
end
end
...and so the individual examples are defined on the outer example group, not on the "with partner #{partner.name}" nested group.
class_exec evaluates the provided block in the context of the class/module. In this case, the class is the example group subclass that RSpec has generated for your context. Using class_exec ensures that when it is called, the receiver is your nested context example group rather than the outer example group, creating the result you want.

How can I DRY up my Rspec/Capybara suite?

In my suite I have this in many it blocks:
let(:user) { create(:user) }
let(:plan) { Plan.first }
let(:subscription) { build(:subscription, user: user ) }
it "something" do
subscription.create_stripe_customer
subscription.update_card valid_card_data
subscription.change_plan_to plan
login_as user
end
How could I DRY this up so I don't have to duplicate all these lines across many files?
You can also create a method like
def prepare_subscription
subscription.create_stripe_customer
subscription.update_card valid_card_data
subscription.change_plan_to plan
end
And in your it block like so:
it "something" do
prepare_subscription
login_as user
end
You ain't checking value for that spec so it always green.
If you need prepare some data before test then you could put that code into helper and call it when needed in (for example) before block.
If you need check spec passing again and again then you could use shared examples.

Clean Rspec matcher for change(Model, :count).by(1)

I'm working hard trying to keep my spec files as clean as possible. Using 'shoulda' gem and writing customized matchers that follow the same pattern.
My question is about creating a custom matcher that would wrap expect{ post :create ... }.to change(Model, :count).by(1) and could be used in the same example groups with other 'shoulda' matchers. Details bellow:
Custom matcher (simplified)
RSpec::Matchers.define :create_a_new do |model|
match do |dummy|
::RSpec::Expectations::ExpectationTarget.new(subject).to change(model, :count).by(1)
end
end
Working example
describe 'POST create:' do
describe '(valid params)' do
subject { -> { post :create, model: agency_attributes } }
it { should create_a_new(Agency) }
end
end
This work OK as long as I use a subject lambda and my matcher is the only one in the example group.
Failing examples
Failing example 1
Adding more examples in the same group makes the other matcher fail because subject is now a lambda instead of an instance of the Controller.
describe 'POST create:' do
describe '(valid params)' do
subject { -> { post :create, model: agency_attributes } }
it { should create_a_new(Agency) }
it { should redirect_to(Agency.last) }
end
end
Failing example 2
The 'shoulda' matcher expect me to define a before block, but this become incompatible with my custom matcher
describe 'POST create:' do
describe '(valid params)' do
before { post :create, agency: agency_attributes }
it { should create_a_new(Agency) }
it { should redirect_to(Agency.last) }
end
end
Expected result
I am looking for a way to write my custom matcher that would fit in the same example group as other matchers, meaning my custom matcher should use the before block to execute the controller action, the "failing example #2" above is the way I would like to write my specs. Is it possible?
Thanks for reading
I do not think there is a way you can get your failing examples passing.
This is because change really needs a lambda, since it needs to perform your count twice (once before, and once after calling it). That's the reason I tend not to use it (or use it in context isolation).
What I usually do, instead of using the count matcher, is checking three things:
The record is persisted. If I assign the model to #model, then I use expect(assigns(:model)).to be_persisted
The record is an instance of the expected model (though might not seem useful, it is
quite descriptive when using an STI). expect(assigns(:model)).to be_a(Model).
Check the last record in DB is the same as the one I just create `expect(assigns(:model)).to eq(Model.last)``
And that's the way I usually test the change matcher without using it. Of course, you can now create your own matcher
RSpec::Matchers.define :create_a_new do |model|
match do |actual|
actual.persisted? &&
actual.instance_of?(Participant) &&
(Participant.last == actual)
end
end

Resources