Getting FactoryBot object attributes for API requests with RSpec - ruby-on-rails

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.

Related

Inside an RSpec matcher, how to make an instance available that gets created inside a model callback

I try to test a model. After the model under test is created, it creates an object from another model and sends out an email based on the other object's ID.
# model under test
class AccountabilityBuddy < ApplicationRecord
after_save_commit :send_consent_inquiry
def send_consent_inquiry
consent_tracker = BuddyConsent.create!(accountability_buddy_id: self.id)
BuddyMailer.with(buddy: self, consent_link: consent_tracker.id).buddy_request.deliver_later
end
...
This is what I've tried so far:
# the model test
RSpec.describe AccountabilityBuddy, type: :model do
subject(:buddy) { build(:accountability_buddy) }
describe 'after_save callback' do
let(:saving) do
buddy.save!
buddy.reload
end
it 'sends an email to the buddy' do
# how that works: https://stackoverflow.com/questions/22988968/testing-after-commit-with-rspec-and-mocking/30901628#comment43748375_22989816
expect { saving }
.to have_enqueued_job(ActionMailer::MailDeliveryJob)
.with('BuddyMailer', 'buddy_request', 'deliver_now', params: {buddy: buddy, consent_link: BuddyConsent.last.id }, args: [])
# Test fails, BuddyConsent.last.id is nil
end
it 'creates a new BuddyConsent' do
expect { saving }.to(change { BuddyConsent.all.count }.from(0).to(1))
# Test passes
end
end
...
The first test fails because the BuddyConsent.last.id is nil. I know that BuddyConsent is generated correctly, because the ID is available inside the let block:
let(:saving) do
buddy.save!
buddy.reload
BuddyConsent.last.id # => "8be42055-112c-4ba6-bd1b-61b73946fb6e"
end
How do I make it available inside the matcher and why is it out of context?

Rails 5 Rspec receive with ActionController::Params

I have just upgraded to Rails 5. In my specs I have the following
expect(model).to receive(:update).with(foo: 'bar')
But, since params no longer extends Hash but is now ActionController::Parameters the specs are failing because with() is expecting a hash but it is actually ActionController::Parameters
Is there a better way of doing the same thing in Rspec such as a different method with_hash?
I can get around the issue using
expect(model).to receive(:update).with(hash_including(foo: 'bar'))
But that is just checking if the params includes that hash, not checking for an exact match.
You could do:
params = ActionController::Parameters.new(foo: 'bar')
expect(model).to receive(:update).with(params)
However it still smells - you should be testing the behaviour of the application - not how it does its job.
expect {
patch model_path(model), params: { foo: 'bar' }
model.reload
}.to change(model, :foo).to('bar')
This is how I would test the integration of a controller:
require 'rails_helper'
RSpec.describe "Things", type: :request do
describe "PATCH /things/:id" do
let!(:thing) { create(:thing) }
let(:action) do
patch things_path(thing), params: { thing: attributes }
end
context "with invalid params" do
let(:attributes) { { name: '' } }
it "does not alter the thing" do
expect do
action
thing.reload
end.to_not change(thing, :name)
expect(response).to have_status :bad_entity
end
end
context "with valid params" do
let(:attributes) { { name: 'Foo' } }
it "updates the thing" do
expect do
action
thing.reload
end.to change(thing, :name).to('Foo')
expect(response).to be_successful
end
end
end
end
Is touching the database in a spec inheritenly bad?
No. When you are testing something like a controller the most accurate way to test it is by driving the full stack. If we in this case had stubbed out #thing.update we could have missed for example that the database driver threw an error because we where using the wrong SQL syntax.
If you are for example testing scopes on a model then a spec that stubs out the DB will give you little to no value.
Stubbing may give you a fast test suite that is extremely brittle due to tight coupling and that lets plenty of bugs slip through the cracks.
I handled this by creating in spec/rails_helper.rb
def strong_params(wimpy_params)
ActionController::Parameters.new(wimpy_params).permit!
end
and then in a specific test, you can say:
expect(model).to receive(:update).with(strong_params foo: 'bar')
It's not much different from what you're already doing, but it makes the awkward necessity of that extra call a little more semantically meaningful.
#max had good suggestions about how to avoid this altogether, and I agree they switched away from a hash to discourage using them with hashes interchangeably.
However, if you still want to use them, as a simple hack for more complex situations (for instance if you expect using a a_hash_including), you can try using something like this:
.with( an_object_satisfying { |o|
o.slice(some_params) == ActionController::Parameters.new(some_params)
})

RSpec returns all objects to be 'nil'

I am new to Rspec. I am writing a test case to cover some action in a model. Here is my rspec code
test_cover_image_spec.rb
require 'spec_helper'
describe Issue do
before :each do
#issue = Issue.joins(:multimedia).uniq.first
binding.pry
end
describe '#release_cover_image' do
context 'While making an issue open' do
it 'should make issue cover in S3 accessible' do
put :update, :id => #issue.id, :issue => #issue.attributes = {:open => '1'}
end
end
end
end
#issue always returns nil. In my debugger also, Issue.all returns an empty array.
Tests usually run in isolation. That means each test needs to set up the objects before running. After the test run common test configurations delete all created data from the test database. That means you need to create your test data before you can use it.
For example like this:
require 'spec_helper'
describe Issue do
# pass all attributes to create a valid issue
let(:issue) { Issue.create(title: 'Foo Bar') }
describe '#release_cover_image' do
context 'While making an issue open' do
it 'should make issue cover in S3 accessible' do
put :update, id: issue.id, issue: { open: '1' }
expect(issue.reload.open).to eq('1')
end
end
end
end
To make this work you have to populate the test database first.
Check out factory_girl gem - it is most often used for easy generating test data.
So (general idea is that) you will have to create few factories:
issues_factory.rb
multimedia_factory.rb
And use them, to generate the issue object prior the test run.
If you're not going to use factory_girl then anyway you should change from creating an issue in before block to using let:
let(:issue) { Issue.create }

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

Testing gems within a Rails App

I'm attempting to test that my service is calling Anemone.crawl correctly. I have the following code:
spider_service.rb
class SpiderService < BaseService
require 'anemone'
attr_accessor :url
def initialize(url)
self.url = url
end
def crawl_site
Anemone.crawl(url) do |anemone|
end
end
end
spider_service_spec.rb
require 'spec_helper'
require 'anemone'
describe SpiderService do
describe "initialize" do
let(:url) { mock("url") }
subject { SpiderService.new(url) }
it "should store the url in an instance variable" do
subject.url.should == url
end
end
describe "#crawl_site" do
let(:spider_service) { mock("spider service") }
let(:url) { mock("url") }
before do
SpiderService.stub(:new).and_return(spider_service)
spider_service.stub(:crawl_site)
Anemone.stub(:crawl).with(url)
end
subject { spider_service.crawl_site }
it "should call Anemone.crawl with the url" do
Anemone.should_receive(:crawl).with(url)
subject
end
end
end
And here's the error that I'm getting, and can't understand, since I can call the service in the Rails console and I get back data from Anemone when I provide a valid URL:
Failures:
1) SpiderService#crawl_site should call Anemone.crawl with the url
Failure/Error: Anemone.should_receive(:crawl).with(url)
(Anemone).crawl(#<RSpec::Mocks::Mock:0x82bdd454 #name="url">)
expected: 1 time
received: 0 times
# ./spec/services/spider_service_spec.rb:28
Please tell me I've forgotten something silly (I can blame lack of coffee then, instead of general incompetence!)
Thank you for your time,
Gav
Your subject calls a method on the mock object that you're created (mock("spider_service")), not a real SpiderService object. You've also stubbed the call on the mock spider service to do nothing, so calling it in the subject will do nothing, hence why your test fails.
Also, you've stubbed new (although you never call it) on SpiderService to return a mock object. When you're testing SpiderService you'll want to have real instances of the class otherwise method calls will not behave as they would on a real instance of the class.
The following should achieve what you want:
describe "#crawl_site" do
let(:spider_service) { SpiderService.new(url) }
let(:url) { mock("url") }
before do
Anemone.stub(:crawl).with(url)
end
subject { spider_service.crawl_site }
it "should call Anemone.crawl with the url" do
Anemone.should_receive(:crawl).with(url)
subject
end
end
You might also want to move the require 'anenome' outside of the class definition so it is available elsewhere.

Resources